/* test_dotagents.c — standalone test suite for dotagents.h (the .agents * protocol layer). Zero dependencies: compiles to a single TU with no * linked libraries (frontmatter.h provides everything it needs). */ #include "dotagents.h" #include #include #include #include #include #include static int failures = 0; static int checks = 0; #define CHECK(cond, msg) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ } \ } while (0) static char *tmpdir(const char *tag) { char path[256]; snprintf(path, sizeof path, "/tmp/llshc-da-%s-%d", tag, (int)getpid()); /* rm -rf */ char cmd[300]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", path); if (system(cmd) != 0) {} /* mkdir -p */ snprintf(cmd, sizeof cmd, "mkdir -p '%s'", path); if (system(cmd) != 0) exit(1); return fm_strdup(path); } static void wfile_bin(const char *dir, const char *rel, const char *content) { char path[512]; snprintf(path, sizeof path, "%s/%s", dir, rel); char *slash = strrchr(path, '/'); if (slash) { char dirpart[512]; size_t n = (size_t)(slash - path); memcpy(dirpart, path, n); dirpart[n] = '\0'; char cmd[600]; snprintf(cmd, sizeof cmd, "mkdir -p '%s'", dirpart); if (system(cmd) != 0) exit(1); } FILE *f = fopen(path, "w"); if (!f) exit(1); fputs(content, f); fclose(f); } static int count_warns(const dotagents *d, const char *needle) { int n = 0; for (size_t i = 0; i < d->warnings.len; i++) if (strstr(d->warnings.items[i], needle)) n++; return n; } /* ------------------------------------------------------------------ */ /* Layer tests */ /* ------------------------------------------------------------------ */ static void test_loads_global_only(void) { char *ws = tmpdir("ws1"); char *global = tmpdir("g1"); wfile_bin(global, "agents.md", "global guidelines"); wfile_bin(global, "skills/g1/skill.md", "---\nid: g1\nname: G1\ndescription: global skill\n---\ndo g1"); dotagents d; dotagents_load_from(&d, ws, global); CHECK(d.found_count == 1, "one layer found"); CHECK(d.found_layers[0] == LAYER_GLOBAL, "layer is global"); CHECK(d.agents_md && strcmp(d.agents_md, "global guidelines") == 0, "agents_md"); CHECK(d.skill_count == 1, "one skill"); CHECK(strcmp(d.skills[0].id, "g1") == 0, "skill id"); CHECK(strcmp(d.skills[0].body, "do g1") == 0, "skill body"); dotagents_free(&d); free(ws); free(global); } static void test_md_filenames_case_insensitive(void) { char *ws = tmpdir("ws-cs"); wfile_bin(ws, "AGENTS.MD", "workspace guidelines"); wfile_bin(ws, "System-Prompt.md", "the system prompt"); wfile_bin(ws, "skills/UPPER/SKILL.md", "---\nid: upper\nname: Upper\ndescription: caps skill\n---\ndo " "upper"); wfile_bin(ws, "agents/REVIEWER/Agent.md", "---\nid: reviewer\nname: Reviewer\n---\nYou review."); wfile_bin(ws, "tasks/daily/Task.MD", "---\nid: daily\nname: Daily\nenabled: true\n---\nRun daily."); wfile_bin(ws, "memories/note.MD", "---\nid: note\ntitle: Note\n---\nbody"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.found_count == 1, "workspace found"); CHECK(d.agents_md && strcmp(d.agents_md, "workspace guidelines") == 0, "AGENTS.MD"); CHECK(d.system_prompt && strcmp(d.system_prompt, "the system prompt") == 0, "System-Prompt.md"); CHECK(d.skill_count == 1 && strcmp(d.skills[0].id, "upper") == 0, "SKILL.md"); CHECK(d.sub_agent_count == 1 && strcmp(d.sub_agents[0].id, "reviewer") == 0, "Agent.md"); CHECK(d.task_count == 1 && strcmp(d.tasks[0].id, "daily") == 0, "Task.MD"); CHECK(d.memory_count == 1 && strcmp(d.memories[0].id, "note") == 0, "note.MD"); CHECK(d.warnings.len == 0, "no case warnings"); dotagents_free(&d); free(ws); } static void test_workspace_wins(void) { char *ws = tmpdir("ws2"); char *global = tmpdir("g2"); wfile_bin(global, "agents.md", "global guidelines"); wfile_bin(global, "skills/review/skill.md", "---\nid: review\nname: Review G\ndescription: global\n---\n" "global body"); wfile_bin(global, "memories/m.md", "---\nid: m\ntitle: M Global\nimportance: low\ntags: a\n---\ng"); wfile_bin(ws, "agents.md", "workspace guidelines"); wfile_bin(ws, "skills/review/skill.md", "---\nid: review\nname: Review W\ndescription: workspace\n---\n" "workspace body"); wfile_bin(ws, "skills/extra/skill.md", "---\nid: extra\nname: Extra\n---\nextra body"); dotagents d; dotagents_load_from(&d, ws, global); CHECK(d.agents_md && strcmp(d.agents_md, "workspace guidelines") == 0, "workspace agents.md wins"); const skill *review = dotagents_find_skill(&d, "review"); CHECK(review && review->layer == LAYER_WORKSPACE, "review layer"); CHECK(review && strcmp(review->body, "workspace body") == 0, "review body"); CHECK(d.skill_count == 2, "review + extra"); CHECK(d.memory_count == 1, "only global memory"); dotagents_free(&d); free(ws); free(global); } static void test_json_layers_merge_by_key(void) { char *ws = tmpdir("ws3"); char *global = tmpdir("g3"); wfile_bin(global, "models.json", "{\"default\":\"ga\",\"presets\":{\"ga\":{\"model\":\"ga-" "model\"},\"shared\":{\"model\":\"s\"}}}"); wfile_bin(global, "mcp.json", "{\"mcpServers\":{\"fs\":{\"command\":\"npx\"}," "\"git\":{\"url\":\"g\"}}}"); wfile_bin(ws, "models.json", "{\"default\":\"wb\",\"presets\":{\"wb\":{\"provider\":\"" "ollama\"},\"shared\":{\"model\":\"ws\"}}}"); wfile_bin(ws, "mcp.json", "{\"mcpServers\":{\"git\":{\"url\":\"w\"}}}"); dotagents d; dotagents_load_from(&d, ws, global); CHECK(d.has_models, "models present"); CHECK(d.models.default_name && strcmp(d.models.default_name, "wb") == 0, "default=wb"); CHECK(d.models.preset_count == 3, "3 presets"); const model_preset *ga = models_file_get(&d.models, "ga"); CHECK(ga && ga->model && strcmp(ga->model, "ga-model") == 0, "ga.model"); const model_preset *sh = models_file_get(&d.models, "shared"); CHECK(sh && sh->model && strcmp(sh->model, "ws") == 0, "shared.model"); CHECK(d.mcp_count == 2, "2 mcp servers"); const char *git = dotagents_mcp_config(&d, "git"); CHECK(git && strstr(git, "\"url\":\"w\""), "git.url=w"); dotagents_free(&d); free(ws); free(global); } static void test_malformed_files_warn_and_skip(void) { char *ws = tmpdir("ws4"); wfile_bin(ws, "models.json", "{not json"); wfile_bin(ws, "mcp.json", "{\"nope\":true}"); wfile_bin(ws, "skills/broken/skill.md", "no frontmatter"); char cmd[300]; snprintf(cmd, sizeof cmd, "mkdir -p '%s/skills/empty'", ws); if (system(cmd) != 0) exit(1); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(!d.has_models, "no models"); CHECK(d.mcp_count == 0, "no mcp"); CHECK(d.skill_count == 1 && strcmp(d.skills[0].id, "broken") == 0, "broken skill kept"); CHECK(d.skills[0].body && strstr(d.skills[0].body, "no frontmatter"), "broken skill body"); CHECK(count_warns(&d, "models.json") > 0, "models.json warning"); CHECK(count_warns(&d, "mcp.json") > 0, "mcp.json warning"); CHECK(count_warns(&d, "skills/empty") > 0, "skills/empty warning"); dotagents_free(&d); free(ws); } static void test_missing_layers_empty(void) { char *ws = tmpdir("ws5"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.found_count == 0, "no layers"); CHECK(d.skill_count == 0, "no skills"); CHECK(d.warnings.len == 0, "no warnings"); dotagents_free(&d); free(ws); } static void test_tasks_and_sub_agents(void) { char *ws = tmpdir("ws6"); wfile_bin(ws, "tasks/daily/task.md", "---\nkind: task\nid: daily\nname: Daily Review\nintervalMinutes" ": 60\nenabled: true\nrunOnStartup: true\n---\nReview open " "PRs."); wfile_bin(ws, "agents/reviewer/agent.md", "---\nid: reviewer\nname: Reviewer\ndescription: Reviews code\n" "role: delegation-target\nenabled: true\n---\nYou are a " "reviewer."); wfile_bin(ws, "agents/reviewer/config.json", "{\"modelConfig\":{\"mcpToolsOpenaiModel\":\"gpt-4o\"}}"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.task_count == 1, "one task"); CHECK(d.tasks[0].has_interval && d.tasks[0].interval_minutes == 60, "interval"); CHECK(d.tasks[0].run_on_startup, "run_on_startup"); CHECK(d.tasks[0].enabled, "enabled"); CHECK(strcmp(d.tasks[0].body, "Review open PRs.") == 0, "task body"); CHECK(d.sub_agent_count == 1, "one sub agent"); CHECK(strcmp(d.sub_agents[0].role, "delegation-target") == 0, "role"); CHECK(strcmp(d.sub_agents[0].body, "You are a reviewer.") == 0, "sub agent body"); CHECK(d.sub_agents[0].config != NULL, "config present"); dotagents_free(&d); free(ws); } static void test_malformed_sub_agent_config(void) { char *ws = tmpdir("ws9"); wfile_bin(ws, "agents/broken/agent.md", "---\nid: broken\nname: Broken\ndescription: has a bad config\n" "---\nYou are broken."); wfile_bin(ws, "agents/broken/config.json", "{not json"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.sub_agent_count == 1, "one sub agent"); CHECK(d.sub_agents[0].config == NULL, "config dropped"); CHECK(count_warns(&d, "agents/broken/config.json") > 0, "config warning"); dotagents_free(&d); free(ws); } static void test_memories_rank(void) { memory low, high; memset(&low, 0, sizeof low); low.importance = fm_strdup("low"); memset(&high, 0, sizeof high); high.importance = fm_strdup("high"); CHECK(memory_importance_rank(&high) < memory_importance_rank(&low), "high ranks before low"); free(low.importance); free(high.importance); } static void test_project_sections(void) { char *ws = tmpdir("ws7"); wfile_bin(ws, "agents.md", "# Guidelines\n- be nice"); wfile_bin(ws, "skills/cr/skill.md", "---\nid: cr\nname: CR\ndescription: code review\n---\nreview " "steps"); wfile_bin(ws, "memories/p.md", "---\nid: p\ntitle: P\nimportance: high\ntags: x\n---\nbody"); dotagents d; dotagents_load_from(&d, ws, NULL); char *sections = dotagents_project_sections(&d, 50); CHECK(strstr(sections, "- be nice"), "guidelines"); CHECK(strstr(sections, "- cr: code review"), "skill line"); CHECK(strstr(sections, "use_skill"), "use_skill mention"); CHECK(strstr(sections, "search_memories"), "search_memories mention"); char *pre = dotagents_compose_preamble(&d, "BASE", 50); CHECK(strstr(pre, "BASE") && strstr(sections, "be nice") && memcmp(pre, "BASE", 4) == 0, "preamble = base + sections"); free(sections); free(pre); dotagents_free(&d); free(ws); } static void test_skill_index_cap(void) { char *ws = tmpdir("ws10"); const char *ids[5] = {"a", "b", "c", "d", "e"}; for (int i = 0; i < 5; i++) { char rel[64]; char content[128]; snprintf(rel, sizeof rel, "skills/%s/skill.md", ids[i]); snprintf(content, sizeof content, "---\nid: %s\nname: %s\ndescription: skill %d\n---\nbody", ids[i], ids[i], i); wfile_bin(ws, rel, content); } dotagents d; dotagents_load_from(&d, ws, NULL); char *sections = dotagents_project_sections(&d, 2); CHECK(strstr(sections, "- a: skill 0"), "a listed"); CHECK(strstr(sections, "- b: skill 1"), "b listed"); CHECK(!strstr(sections, "- c: skill 2"), "c not listed"); CHECK(strstr(sections, "and 3 more"), "…and 3 more"); free(sections); dotagents_free(&d); free(ws); } static void test_disabled_skills_excluded(void) { char *ws = tmpdir("ws8"); wfile_bin(ws, "skills/off/skill.md", "---\nid: off\ndescription: hidden\nenabled: false\n---\nx"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.skill_count == 1, "skill kept"); CHECK(!d.skills[0].enabled, "disabled"); char *sections = dotagents_project_sections(&d, 50); CHECK(!strstr(sections, "off"), "not in sections"); free(sections); dotagents_free(&d); free(ws); } /* ------------------------------------------------------------------ */ /* Spec-gap conformance */ /* ------------------------------------------------------------------ */ static void test_task_profile_id(void) { char *ws = tmpdir("ws-pid"); wfile_bin(ws, "tasks/nightly/task.md", "---\nkind: task\nid: nightly\nname: Nightly\nprofileId: " "prof-9\nenabled: true\n---\nRun nightly."); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.task_count == 1, "one task"); CHECK(d.tasks[0].profile_id && strcmp(d.tasks[0].profile_id, "prof-9") == 0, "profile_id parsed"); dotagents_free(&d); free(ws); } static void test_task_profile_id_workspace_override(void) { char *ws = tmpdir("ws-pid2"); char *global = tmpdir("g-pid"); wfile_bin(global, "tasks/nightly/task.md", "---\nid: nightly\nprofileId: g-prof\n---\ng"); wfile_bin(ws, "tasks/nightly/task.md", "---\nid: nightly\nprofileId: w-prof\n---\nw"); dotagents d; dotagents_load_from(&d, ws, global); CHECK(d.task_count == 1, "one task"); CHECK(d.tasks[0].profile_id && strcmp(d.tasks[0].profile_id, "w-prof") == 0, "workspace profileId wins"); dotagents_free(&d); free(ws); free(global); } static void test_agent_connection_type(void) { char *ws = tmpdir("ws-ct"); wfile_bin(ws, "agents/remote/agent.md", "---\nid: remote\nname: Remote\nrole: delegation-target\n" "connection-type: internal\nenabled: true\n---\nYou are " "remote."); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.sub_agent_count == 1, "one agent"); CHECK(d.sub_agents[0].connection_type && strcmp(d.sub_agents[0].connection_type, "internal") == 0, "connection-type parsed"); dotagents_free(&d); free(ws); } static void test_settings_layers_merge(void) { char *ws = tmpdir("ws-set"); char *global = tmpdir("g-set"); wfile_bin(global, "speakmcp-settings.json", "{\"theme\":\"dark\",\"telemetry\":true,\"shared\":\"g\"}"); wfile_bin(ws, "speakmcp-settings.json", "{\"shared\":\"w\",\"volume\":2}"); dotagents d; dotagents_load_from(&d, ws, global); CHECK(d.settings_json != NULL, "settings present"); CHECK(d.settings_json && strstr(d.settings_json, "\"shared\":\"w\""), "shared=w"); CHECK(d.settings_json && strstr(d.settings_json, "\"theme\":\"dark\""), "theme kept"); CHECK(d.settings_json && strstr(d.settings_json, "\"volume\":2"), "volume kept"); CHECK(d.settings_json && strstr(d.settings_json, "\"telemetry\":true"), "telemetry kept"); dotagents_free(&d); free(ws); free(global); } static void test_settings_invalid_warns(void) { char *ws = tmpdir("ws-setbad"); wfile_bin(ws, "speakmcp-settings.json", "{nope"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.settings_json == NULL, "no settings"); CHECK(count_warns(&d, "speakmcp-settings.json") > 0, "settings warning"); dotagents_free(&d); free(ws); } static void test_layouts_ui_merge(void) { char *ws = tmpdir("ws-lay"); char *global = tmpdir("g-lay"); wfile_bin(global, "layouts/ui.json", "{\"panes\":2,\"theme\":\"dark\"}"); wfile_bin(ws, "layouts/ui.json", "{\"theme\":\"light\"}"); dotagents d; dotagents_load_from(&d, ws, global); CHECK(d.layouts_json && strstr(d.layouts_json, "\"theme\":\"light\""), "theme=w"); CHECK(d.layouts_json && strstr(d.layouts_json, "\"panes\":2"), "panes kept"); dotagents_free(&d); free(ws); free(global); } static void test_settings_only_layer_discovered(void) { char *ws = tmpdir("ws-setonly"); wfile_bin(ws, "speakmcp-settings.json", "{\"a\":1}"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.found_count == 1, "layer found"); CHECK(d.settings_json != NULL, "settings"); dotagents_free(&d); free(ws); } static void test_layouts_only_layer_discovered(void) { char *ws = tmpdir("ws-layonly"); wfile_bin(ws, "layouts/ui.json", "{\"a\":1}"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.found_count == 1, "layer found"); CHECK(d.layouts_json != NULL, "layouts"); dotagents_free(&d); free(ws); } static void test_backups_ignored(void) { char *ws = tmpdir("ws-bk"); wfile_bin(ws, "agents.md", "guidelines"); wfile_bin(ws, ".backups/skills/ghost/skill.md", "---\nid: ghost\nname: Ghost\n---\nbody"); wfile_bin(ws, ".backups/memories/ghost.md", "---\nid: ghost\ntitle: G\n---\nb"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.agents_md && strcmp(d.agents_md, "guidelines") == 0, "agents_md"); CHECK(d.skill_count == 0, "no ghost skill"); CHECK(d.memory_count == 0, "no ghost memory"); dotagents_free(&d); free(ws); } static void test_unknown_frontmatter_keys_accepted(void) { char *ws = tmpdir("ws-unk"); wfile_bin(ws, "agents/x/agent.md", "---\nkind: agent\nid: x\nname: X\nmetadata: stuff\n---\nb"); dotagents d; dotagents_load_from(&d, ws, NULL); CHECK(d.sub_agent_count == 1, "agent kept"); CHECK(d.warnings.len == 0, "no warnings"); dotagents_free(&d); free(ws); } /* Both ~/.agents and ./.agents must remain reachable in the model context: * a global-only skill and a workspace-only skill both survive the merge, both * appear in the skill index the model sees, and an id conflict resolves to * the workspace copy. */ static void test_both_layers_reachable_in_context(void) { char *ws = tmpdir("ws-ctx"); char *global = tmpdir("g-ctx"); wfile_bin(global, "skills/g-only/skill.md", "---\nid: g-only\nname: G Only\ndescription: global only skill\n" "---\ng global body"); wfile_bin(global, "skills/shared/skill.md", "---\nid: shared\nname: Shared G\ndescription: shared global\n" "---\ng shared body"); wfile_bin(ws, "skills/w-only/skill.md", "---\nid: w-only\nname: W Only\ndescription: workspace only " "skill\n---\nw workspace body"); wfile_bin(ws, "skills/shared/skill.md", "---\nid: shared\nname: Shared W\ndescription: shared " "workspace\n---\nw shared body"); dotagents d; dotagents_load_from(&d, ws, global); /* Neither layer's skills are lost. */ CHECK(d.skill_count == 3, "g-only + w-only + shared"); const skill *g = dotagents_find_skill(&d, "g-only"); CHECK(g && g->layer == LAYER_GLOBAL, "g-only reachable"); CHECK(g && g->body && strstr(g->body, "g global body"), "g-only body"); const skill *w = dotagents_find_skill(&d, "w-only"); CHECK(w && w->layer == LAYER_WORKSPACE, "w-only reachable"); CHECK(w && w->body && strstr(w->body, "w workspace body"), "w-only body"); /* An id conflict resolves to the workspace copy. */ const skill *sh = dotagents_find_skill(&d, "shared"); CHECK(sh && sh->layer == LAYER_WORKSPACE, "shared -> workspace"); CHECK(sh && sh->body && strstr(sh->body, "w shared body"), "shared workspace body"); /* Both layers remain visible in the context skill index. */ char *sections = dotagents_project_sections(&d, 50); CHECK(strstr(sections, "- g-only") != NULL, "g-only in context index"); CHECK(strstr(sections, "- w-only") != NULL, "w-only in context index"); CHECK(strstr(sections, "- shared") != NULL, "shared in context index"); free(sections); /* ...and therefore in the composed preamble the model actually receives. */ char *pre = dotagents_compose_preamble(&d, "BASE", 50); CHECK(strstr(pre, "g-only") != NULL && strstr(pre, "w-only") != NULL, "both layers in composed preamble"); free(pre); dotagents_free(&d); free(ws); free(global); } /* skill_index_cap must apply to the merged two-layer set, not silently drop * global skills: with 3 global + 2 workspace skills and cap 2, the first two * (by id) are listed and the rest are elided. */ static void test_skill_index_cap_across_layers(void) { char *ws = tmpdir("ws-cap"); char *global = tmpdir("g-cap"); wfile_bin(global, "skills/g1/skill.md", "---\nid: g1\nname: G1\ndescription: g1\n---\nb"); wfile_bin(global, "skills/g2/skill.md", "---\nid: g2\nname: G2\ndescription: g2\n---\nb"); wfile_bin(global, "skills/g3/skill.md", "---\nid: g3\nname: G3\ndescription: g3\n---\nb"); wfile_bin(ws, "skills/w1/skill.md", "---\nid: w1\nname: W1\ndescription: w1\n---\nb"); wfile_bin(ws, "skills/w2/skill.md", "---\nid: w2\nname: W2\ndescription: w2\n---\nb"); dotagents d; dotagents_load_from(&d, ws, global); CHECK(d.skill_count == 5, "5 merged skills"); char *sections = dotagents_project_sections(&d, 2); CHECK(strstr(sections, "- g1") != NULL, "g1 listed"); CHECK(strstr(sections, "- g2") != NULL, "g2 listed"); CHECK(!strstr(sections, "- g3"), "g3 capped"); CHECK(!strstr(sections, "- w1"), "w1 capped"); CHECK(strstr(sections, "and 3 more") != NULL, "...and 3 more"); free(sections); dotagents_free(&d); free(ws); free(global); } int main(void) { test_loads_global_only(); test_md_filenames_case_insensitive(); test_workspace_wins(); test_json_layers_merge_by_key(); test_malformed_files_warn_and_skip(); test_missing_layers_empty(); test_tasks_and_sub_agents(); test_malformed_sub_agent_config(); test_memories_rank(); test_project_sections(); test_skill_index_cap(); test_disabled_skills_excluded(); test_task_profile_id(); test_task_profile_id_workspace_override(); test_agent_connection_type(); test_settings_layers_merge(); test_settings_invalid_warns(); test_layouts_ui_merge(); test_settings_only_layer_discovered(); test_layouts_only_layer_discovered(); test_backups_ignored(); test_unknown_frontmatter_keys_accepted(); test_both_layers_reachable_in_context(); test_skill_index_cap_across_layers(); printf("%d checks, %d failures\n", checks, failures); return failures ? 1 : 0; }