agentopology 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +8 -6
  2. package/dist/analyzer/index.d.ts +1 -0
  3. package/dist/analyzer/index.d.ts.map +1 -1
  4. package/dist/analyzer/index.js +35 -0
  5. package/dist/analyzer/index.js.map +1 -1
  6. package/dist/bindings/__tests__/bindings.test.js +11 -11
  7. package/dist/bindings/__tests__/bindings.test.js.map +1 -1
  8. package/dist/bindings/claude-code.d.ts.map +1 -1
  9. package/dist/bindings/claude-code.js +339 -85
  10. package/dist/bindings/claude-code.js.map +1 -1
  11. package/dist/bindings/openclaw.d.ts.map +1 -1
  12. package/dist/bindings/openclaw.js +367 -5
  13. package/dist/bindings/openclaw.js.map +1 -1
  14. package/dist/cli/index.js +2 -2
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/docs/__tests__/docs.test.js +2 -1
  17. package/dist/docs/__tests__/docs.test.js.map +1 -1
  18. package/dist/docs/content.d.ts.map +1 -1
  19. package/dist/docs/content.js +730 -4
  20. package/dist/docs/content.js.map +1 -1
  21. package/dist/exporters/markdown.d.ts.map +1 -1
  22. package/dist/exporters/markdown.js +165 -0
  23. package/dist/exporters/markdown.js.map +1 -1
  24. package/dist/exporters/mermaid.d.ts.map +1 -1
  25. package/dist/exporters/mermaid.js +30 -0
  26. package/dist/exporters/mermaid.js.map +1 -1
  27. package/dist/import/__tests__/import.test.js +556 -2
  28. package/dist/import/__tests__/import.test.js.map +1 -1
  29. package/dist/import/claude-code.d.ts.map +1 -1
  30. package/dist/import/claude-code.js +2 -0
  31. package/dist/import/claude-code.js.map +1 -1
  32. package/dist/import/index.d.ts +1 -0
  33. package/dist/import/index.d.ts.map +1 -1
  34. package/dist/import/index.js +7 -1
  35. package/dist/import/index.js.map +1 -1
  36. package/dist/import/openclaw.d.ts +70 -0
  37. package/dist/import/openclaw.d.ts.map +1 -0
  38. package/dist/import/openclaw.js +1060 -0
  39. package/dist/import/openclaw.js.map +1 -0
  40. package/dist/import/serializer.d.ts.map +1 -1
  41. package/dist/import/serializer.js +171 -4
  42. package/dist/import/serializer.js.map +1 -1
  43. package/dist/parser/__tests__/parser.test.js +463 -0
  44. package/dist/parser/__tests__/parser.test.js.map +1 -1
  45. package/dist/parser/ast.d.ts +122 -0
  46. package/dist/parser/ast.d.ts.map +1 -1
  47. package/dist/parser/index.d.ts +14 -2
  48. package/dist/parser/index.d.ts.map +1 -1
  49. package/dist/parser/index.js +226 -5
  50. package/dist/parser/index.js.map +1 -1
  51. package/dist/parser/validator.d.ts +1 -1
  52. package/dist/parser/validator.d.ts.map +1 -1
  53. package/dist/parser/validator.js +214 -1
  54. package/dist/parser/validator.js.map +1 -1
  55. package/dist/visualizer/index.d.ts.map +1 -1
  56. package/dist/visualizer/index.js +104 -16
  57. package/dist/visualizer/index.js.map +1 -1
  58. package/examples/memory-system.at +162 -0
  59. package/examples/script-agent-pipeline.at +65 -0
  60. package/package.json +2 -1
  61. package/skill/SKILL.md +7 -7
  62. package/spec/grammar.md +345 -10
  63. package/spec/reserved-keywords.md +25 -1
  64. package/spec/validation.md +171 -2
@@ -446,9 +446,9 @@ describe("parseSettingsJson", () => {
446
446
  it("skips gate hooks", () => {
447
447
  const { hooks } = parseSettingsJson(JSON.stringify({
448
448
  hooks: {
449
- PreToolUse: [{
449
+ SubagentStop: [{
450
450
  hooks: [{ command: "bash scripts/gate-quality.sh", type: "command" }],
451
- matcher: "Task",
451
+ matcher: "builder",
452
452
  }],
453
453
  },
454
454
  }));
@@ -778,4 +778,558 @@ topology complex : [pipeline, fan-out] {
778
778
  expect(lead.outputs.confidence).toContain("high");
779
779
  });
780
780
  });
781
+ // ===========================================================================
782
+ // OpenClaw importer tests
783
+ // ===========================================================================
784
+ import { parseOpenClawJson, parseSoulMd, parseAgentsMd, parseSkillFile, parseMemoryMd, parseCronJobsJson, importOpenClaw, } from "../openclaw.js";
785
+ // ---------------------------------------------------------------------------
786
+ // OpenClaw test data
787
+ // ---------------------------------------------------------------------------
788
+ const OPENCLAW_JSON = `{
789
+ "agents": {
790
+ "defaults": {
791
+ "workspace": "~/.openclaw/workspace",
792
+ "model": { "primary": "openrouter/google/gemini-2.5-flash" }
793
+ },
794
+ "list": [
795
+ {
796
+ "id": "commander",
797
+ "name": "Commander",
798
+ "model": { "primary": "openrouter/google/gemini-2.5-flash" },
799
+ "workspace": "~/.openclaw/workspace-commander",
800
+ "agentDir": "~/.openclaw/config/agents/commander/agent",
801
+ "subagents": { "allowAgents": ["researcher", "worker"] }
802
+ },
803
+ {
804
+ "id": "researcher",
805
+ "name": "Researcher",
806
+ "model": {
807
+ "primary": "openrouter/google/gemini-2.5-flash",
808
+ "fallbacks": ["openrouter/google/gemini-2.5-flash-lite"]
809
+ },
810
+ "workspace": "~/.openclaw/workspace-researcher",
811
+ "agentDir": "~/.openclaw/config/agents/researcher/agent"
812
+ }
813
+ ]
814
+ },
815
+ "gateway": { "port": 18789, "auth": { "mode": "token", "token": "\${OPENCLAW_AUTH_TOKEN}" } },
816
+ "models": {
817
+ "providers": {
818
+ "openrouter": {
819
+ "apiKey": "\${OPENROUTER_API_KEY}",
820
+ "baseUrl": "https://openrouter.ai/api/v1",
821
+ "models": [
822
+ { "id": "openrouter/google/gemini-2.5-flash", "name": "Gemini 2.5 Flash" }
823
+ ]
824
+ }
825
+ }
826
+ },
827
+ "tools": { "allow": [], "deny": ["dangerous-tool"], "agentToAgent": { "enabled": true } },
828
+ "env": { "OPENROUTER_API_KEY": "\${OPENROUTER_API_KEY}", "MY_VAR": "hello" }
829
+ }`;
830
+ const SOUL_MD = `> A test topology for unit testing.
831
+
832
+ ## Identity
833
+ - Name: Test Topology
834
+ - Version: 2.0.0
835
+ - Patterns: supervisor, fan-out
836
+
837
+ ## Mission
838
+ A test topology for unit testing.
839
+
840
+ ## Roles
841
+ ### Strategy
842
+ Plan and coordinate
843
+
844
+ ### Research
845
+ Find information
846
+
847
+ ## Ethical Guardrails
848
+ - Tool restrictions: none
849
+
850
+ ## Parameters
851
+
852
+ - **campaign**: string (required)
853
+ - **mode**: string = intelligence
854
+
855
+ ## Interface
856
+
857
+ - Entry: commander
858
+ - Exit: commander
859
+
860
+ ## Triggers
861
+
862
+ ### launch
863
+ Pattern: \`/launch <CAMPAIGN>\`
864
+ Argument: CAMPAIGN
865
+
866
+ ### status
867
+ Pattern: \`/status <CAMPAIGN>\`
868
+ Argument: CAMPAIGN
869
+
870
+ ## Environment
871
+
872
+ - \`OPENROUTER_API_KEY\`: \${OPENROUTER_API_KEY}
873
+ - \`MY_VAR\`: hello
874
+ `;
875
+ const AGENTS_MD = `# Sub-Agent Definitions
876
+
877
+ ## Pipeline Overview
878
+ Test Topology uses a supervisor + fan-out topology.
879
+
880
+ ## Agents
881
+
882
+ ### Commander
883
+ - **Model:** openrouter/google/gemini-2.5-flash
884
+ - **Phase:** 1
885
+ - **Role:** Plan and coordinate
886
+ - **Tools allowed:** Read, Write, Bash
887
+ - **Tools denied:** none
888
+ - **Reads:** workspace
889
+ - **Writes:** workspace
890
+ - **Skills:** campaign-management
891
+ - **Max turns:** 100
892
+
893
+ #### Instructions
894
+
895
+ You are the commander.
896
+
897
+ Do important things.
898
+ - **Maximum execution time:** 30m
899
+ - **Retry:** max 2 attempts
900
+
901
+ ### Researcher
902
+ - **Model:** openrouter/google/gemini-2.5-flash
903
+ - **Phase:** 2
904
+ - **Role:** Find information
905
+ - **Tools allowed:** Read, Write
906
+ - **Tools denied:** none
907
+ - **Reads:** workspace, domains
908
+ - **Writes:** workspace
909
+ - **Rate limit:** 30/hour
910
+ - **Circuit breaker:** threshold=5, window=30m, cooldown=1h
911
+ - **On failure:** continue
912
+
913
+ #### Instructions
914
+
915
+ You are the researcher.
916
+ - **Maximum execution time:** 45m
917
+ - **Retry:** max 3 attempts
918
+
919
+ ## Group Chat Coordination
920
+
921
+ ### Strategy Session
922
+ - **Members:** commander, researcher
923
+ - **Speaker selection:** round-robin
924
+ - **Max rounds:** 5
925
+ - **Termination:** Commander confirms the plan
926
+ - **Description:** Strategy planning session
927
+ - **Timeout:** 30m
928
+
929
+ ## Flow
930
+
931
+ ### Execution Order
932
+ 1. commander -> strategy-session
933
+ 2. strategy-session -> researcher
934
+ 3. researcher -x-> commander (max 3 iterations)
935
+
936
+ ### Bounded Loops
937
+ - researcher <-> commander: max 3 iterations (prevents infinite loops)
938
+
939
+ ### Error Handling
940
+ - On error in researcher: route to commander
941
+
942
+ ## Gates
943
+
944
+ ### Quality Check
945
+ - **After:** researcher
946
+ - **Before:** commander
947
+ - **On failure:** bounce-back
948
+ - **Behavior:** blocking
949
+ - **Checks:** output-valid, data-complete
950
+
951
+ ## Schedule
952
+
953
+ ### Morning Run
954
+ - **Agent:** commander
955
+ - **Cron:** \`0 9 * * 0-4\`
956
+ - **Enabled:** true
957
+
958
+ ## Hooks
959
+
960
+ ### Block Destructive
961
+ - **Event:** PreToolUse
962
+ - **Run:** exit 2
963
+ - **Type:** command
964
+
965
+ ### Log Completion
966
+ - **Event:** AgentStop
967
+ - **Run:** scripts/log.sh
968
+ - **Type:** command
969
+
970
+ ---
971
+
972
+ ## Test Topology -- Topology Overview
973
+
974
+ ### Architecture
975
+ - Patterns: supervisor, fan-out
976
+ - Agents: 2
977
+ - Gates: 1
978
+ - Version: 2.0.0
979
+ `;
980
+ const SKILL_MD = `---
981
+ name: campaign-management
982
+ description: Manage campaigns end to end
983
+ user-invocable: true
984
+ ---
985
+
986
+ This skill manages campaign lifecycle.
987
+ `;
988
+ const MEMORY_MD = `# Memory Structure
989
+
990
+ ## Workspace
991
+ - Path: \`campaigns/{CAMPAIGN}/\`
992
+ - Structure: messages, outbox, logs
993
+
994
+ ## Domains
995
+ - Path: \`campaigns/{CAMPAIGN}/intel/\`
996
+ `;
997
+ // ---------------------------------------------------------------------------
998
+ // parseOpenClawJson
999
+ // ---------------------------------------------------------------------------
1000
+ describe("parseOpenClawJson", () => {
1001
+ it("extracts agent list with models", () => {
1002
+ const result = parseOpenClawJson(OPENCLAW_JSON);
1003
+ expect(result.agents).toHaveLength(2);
1004
+ expect(result.agents[0].id).toBe("commander");
1005
+ expect(result.agents[0].model).toBe("openrouter/google/gemini-2.5-flash");
1006
+ expect(result.agents[0].subagents).toEqual(["researcher", "worker"]);
1007
+ });
1008
+ it("extracts fallback chains", () => {
1009
+ const result = parseOpenClawJson(OPENCLAW_JSON);
1010
+ expect(result.agents[1].fallbacks).toEqual(["openrouter/google/gemini-2.5-flash-lite"]);
1011
+ });
1012
+ it("extracts providers", () => {
1013
+ const result = parseOpenClawJson(OPENCLAW_JSON);
1014
+ expect(result.providers).toHaveLength(1);
1015
+ expect(result.providers[0].name).toBe("openrouter");
1016
+ expect(result.providers[0].apiKey).toBe("${OPENROUTER_API_KEY}");
1017
+ expect(result.providers[0].baseUrl).toBe("https://openrouter.ai/api/v1");
1018
+ expect(result.providerNames).toEqual(["openrouter"]);
1019
+ });
1020
+ it("extracts tools settings and env", () => {
1021
+ const result = parseOpenClawJson(OPENCLAW_JSON);
1022
+ expect(result.settings.deny).toEqual(["dangerous-tool"]);
1023
+ expect(result.env.MY_VAR).toBe("hello");
1024
+ });
1025
+ it("extracts gateway extensions", () => {
1026
+ const result = parseOpenClawJson(OPENCLAW_JSON);
1027
+ expect(result.extensions["gateway-port"]).toBe(18789);
1028
+ expect(result.extensions["auth-mode"]).toBe("token");
1029
+ });
1030
+ });
1031
+ // ---------------------------------------------------------------------------
1032
+ // parseSoulMd
1033
+ // ---------------------------------------------------------------------------
1034
+ describe("parseSoulMd", () => {
1035
+ it("extracts identity", () => {
1036
+ const result = parseSoulMd(SOUL_MD);
1037
+ expect(result.name).toBe("test-topology");
1038
+ expect(result.version).toBe("2.0.0");
1039
+ expect(result.patterns).toEqual(["supervisor", "fan-out"]);
1040
+ });
1041
+ it("extracts description from quote block", () => {
1042
+ const result = parseSoulMd(SOUL_MD);
1043
+ expect(result.description).toBe("A test topology for unit testing.");
1044
+ });
1045
+ it("extracts roles", () => {
1046
+ const result = parseSoulMd(SOUL_MD);
1047
+ expect(result.roles.strategy).toBe("Plan and coordinate");
1048
+ expect(result.roles.research).toBe("Find information");
1049
+ });
1050
+ it("extracts parameters", () => {
1051
+ const result = parseSoulMd(SOUL_MD);
1052
+ expect(result.params).toHaveLength(2);
1053
+ expect(result.params[0]).toEqual({ name: "campaign", type: "string", default: undefined, required: true });
1054
+ expect(result.params[1]).toEqual({ name: "mode", type: "string", default: "intelligence", required: false });
1055
+ });
1056
+ it("extracts interface endpoints", () => {
1057
+ const result = parseSoulMd(SOUL_MD);
1058
+ expect(result.interfaceEndpoints).toEqual({ entry: "commander", exit: "commander" });
1059
+ });
1060
+ it("extracts triggers", () => {
1061
+ const result = parseSoulMd(SOUL_MD);
1062
+ expect(result.triggers).toHaveLength(2);
1063
+ expect(result.triggers[0]).toEqual({ name: "launch", pattern: "/launch <CAMPAIGN>", argument: "CAMPAIGN" });
1064
+ expect(result.triggers[1]).toEqual({ name: "status", pattern: "/status <CAMPAIGN>", argument: "CAMPAIGN" });
1065
+ });
1066
+ it("extracts environment variables", () => {
1067
+ const result = parseSoulMd(SOUL_MD);
1068
+ expect(result.env["OPENROUTER_API_KEY"]).toBe("${OPENROUTER_API_KEY}");
1069
+ expect(result.env["MY_VAR"]).toBe("hello");
1070
+ });
1071
+ });
1072
+ // ---------------------------------------------------------------------------
1073
+ // parseAgentsMd
1074
+ // ---------------------------------------------------------------------------
1075
+ describe("parseAgentsMd", () => {
1076
+ it("extracts agents with basic properties", () => {
1077
+ const result = parseAgentsMd(AGENTS_MD);
1078
+ expect(result.agents).toHaveLength(2);
1079
+ const cmd = result.agents[0];
1080
+ expect(cmd.id).toBe("commander");
1081
+ expect(cmd.phase).toBe(1);
1082
+ expect(cmd.tools).toEqual(["Read", "Write", "Bash"]);
1083
+ expect(cmd.reads).toEqual(["workspace"]);
1084
+ expect(cmd.writes).toEqual(["workspace"]);
1085
+ expect(cmd.skills).toEqual(["campaign-management"]);
1086
+ expect(cmd.maxTurns).toBe(100);
1087
+ });
1088
+ it("extracts agent prompt (instructions)", () => {
1089
+ const result = parseAgentsMd(AGENTS_MD);
1090
+ const cmd = result.agents[0];
1091
+ expect(cmd.prompt).toContain("You are the commander.");
1092
+ expect(cmd.prompt).toContain("Do important things.");
1093
+ });
1094
+ it("extracts trailing properties after instructions", () => {
1095
+ const result = parseAgentsMd(AGENTS_MD);
1096
+ const cmd = result.agents[0];
1097
+ expect(cmd.timeout).toBe("30m");
1098
+ expect(cmd.retry).toBe(2);
1099
+ });
1100
+ it("extracts rate limit and circuit breaker", () => {
1101
+ const result = parseAgentsMd(AGENTS_MD);
1102
+ const researcher = result.agents[1];
1103
+ expect(researcher.rateLimit).toBe("30/hour");
1104
+ expect(researcher.circuitBreaker).toEqual({
1105
+ threshold: 5,
1106
+ window: "30m",
1107
+ cooldown: "1h",
1108
+ });
1109
+ expect(researcher.onFail).toBe("continue");
1110
+ });
1111
+ it("extracts group chat", () => {
1112
+ const result = parseAgentsMd(AGENTS_MD);
1113
+ expect(result.groups).toHaveLength(1);
1114
+ const group = result.groups[0];
1115
+ expect(group.id).toBe("strategy-session");
1116
+ expect(group.members).toEqual(["commander", "researcher"]);
1117
+ expect(group.speakerSelection).toBe("round-robin");
1118
+ expect(group.maxRounds).toBe(5);
1119
+ expect(group.termination).toBe("Commander confirms the plan");
1120
+ expect(group.timeout).toBe("30m");
1121
+ });
1122
+ it("extracts flow edges including error edges", () => {
1123
+ const result = parseAgentsMd(AGENTS_MD);
1124
+ expect(result.edges).toHaveLength(3);
1125
+ expect(result.edges[0]).toMatchObject({ from: "commander", to: "strategy-session" });
1126
+ expect(result.edges[0].isError).toBeUndefined();
1127
+ expect(result.edges[1]).toMatchObject({ from: "strategy-session", to: "researcher" });
1128
+ expect(result.edges[1].isError).toBeUndefined();
1129
+ expect(result.edges[2]).toMatchObject({ from: "researcher", to: "commander", isError: true, maxIterations: 3 });
1130
+ });
1131
+ it("extracts gates", () => {
1132
+ const result = parseAgentsMd(AGENTS_MD);
1133
+ expect(result.gates).toHaveLength(1);
1134
+ const gate = result.gates[0];
1135
+ expect(gate.id).toBe("quality-check");
1136
+ expect(gate.after).toBe("researcher");
1137
+ expect(gate.before).toBe("commander");
1138
+ expect(gate.onFail).toBe("bounce-back");
1139
+ expect(gate.behavior).toBe("blocking");
1140
+ expect(gate.checks).toEqual(["output-valid", "data-complete"]);
1141
+ });
1142
+ it("extracts schedule", () => {
1143
+ const result = parseAgentsMd(AGENTS_MD);
1144
+ expect(result.schedules).toHaveLength(1);
1145
+ expect(result.schedules[0]).toEqual({
1146
+ id: "morning-run",
1147
+ agent: "commander",
1148
+ cron: "0 9 * * 0-4",
1149
+ enabled: true,
1150
+ });
1151
+ });
1152
+ it("extracts hooks", () => {
1153
+ const result = parseAgentsMd(AGENTS_MD);
1154
+ expect(result.hooks).toHaveLength(2);
1155
+ expect(result.hooks[0].name).toBe("block-destructive");
1156
+ expect(result.hooks[0].on).toBe("PreToolUse");
1157
+ expect(result.hooks[0].run).toBe("exit 2");
1158
+ expect(result.hooks[1].name).toBe("log-completion");
1159
+ expect(result.hooks[1].on).toBe("AgentStop");
1160
+ });
1161
+ });
1162
+ // ---------------------------------------------------------------------------
1163
+ // parseSkillFile
1164
+ // ---------------------------------------------------------------------------
1165
+ describe("parseSkillFile (openclaw)", () => {
1166
+ it("extracts skill from frontmatter", () => {
1167
+ const skill = parseSkillFile("campaign-management", SKILL_MD);
1168
+ expect(skill.id).toBe("campaign-management");
1169
+ expect(skill.description).toBe("Manage campaigns end to end");
1170
+ expect(skill.userInvocable).toBe(true);
1171
+ });
1172
+ it("handles skill without frontmatter", () => {
1173
+ const skill = parseSkillFile("my-skill", "This skill does things.");
1174
+ expect(skill.id).toBe("my-skill");
1175
+ expect(skill.description).toBe("This skill does things.");
1176
+ expect(skill.userInvocable).toBeUndefined();
1177
+ });
1178
+ });
1179
+ // ---------------------------------------------------------------------------
1180
+ // parseMemoryMd
1181
+ // ---------------------------------------------------------------------------
1182
+ describe("parseMemoryMd", () => {
1183
+ it("extracts workspace and domains", () => {
1184
+ const result = parseMemoryMd(MEMORY_MD);
1185
+ const ws = result.memory.workspace;
1186
+ expect(ws.path).toBe("campaigns/{CAMPAIGN}/");
1187
+ expect(ws.structure).toEqual(["messages", "outbox", "logs"]);
1188
+ const domains = result.memory.domains;
1189
+ expect(domains.path).toBe("campaigns/{CAMPAIGN}/intel/");
1190
+ });
1191
+ });
1192
+ // ---------------------------------------------------------------------------
1193
+ // parseCronJobsJson
1194
+ // ---------------------------------------------------------------------------
1195
+ describe("parseCronJobsJson", () => {
1196
+ it("parses cron jobs array", () => {
1197
+ const json = JSON.stringify({
1198
+ jobs: [
1199
+ { id: "morning", agentId: "commander", schedule: { expr: "0 9 * * *" }, enabled: true },
1200
+ { id: "evening", agentId: "reporter", cron: "0 18 * * *", enabled: false },
1201
+ ],
1202
+ });
1203
+ const jobs = parseCronJobsJson(json);
1204
+ expect(jobs).toHaveLength(2);
1205
+ expect(jobs[0]).toEqual({ id: "morning", agent: "commander", cron: "0 9 * * *", enabled: true });
1206
+ expect(jobs[1]).toEqual({ id: "evening", agent: "reporter", cron: "0 18 * * *", enabled: false });
1207
+ });
1208
+ });
1209
+ // ---------------------------------------------------------------------------
1210
+ // importOpenClaw (integration)
1211
+ // ---------------------------------------------------------------------------
1212
+ describe("importOpenClaw", () => {
1213
+ const minimalFiles = [
1214
+ { path: "openclaw.json", content: OPENCLAW_JSON },
1215
+ { path: "SOUL.md", content: SOUL_MD },
1216
+ { path: "AGENTS.md", content: AGENTS_MD },
1217
+ { path: "MEMORY.md", content: MEMORY_MD },
1218
+ { path: "skills/campaign-management/SKILL.md", content: SKILL_MD },
1219
+ ];
1220
+ it("produces a complete TopologyAST", () => {
1221
+ const ast = importOpenClaw(minimalFiles, "test-topology");
1222
+ expect(ast.topology.name).toBe("test-topology");
1223
+ expect(ast.topology.version).toBe("2.0.0");
1224
+ expect(ast.topology.patterns).toEqual(["supervisor", "fan-out"]);
1225
+ expect(ast.topology.description).toBe("A test topology for unit testing.");
1226
+ });
1227
+ it("imports agents with reverse-mapped models", () => {
1228
+ const ast = importOpenClaw(minimalFiles, "test-topology");
1229
+ const agents = ast.nodes.filter((n) => n.type === "agent");
1230
+ expect(agents).toHaveLength(2);
1231
+ const cmd = agents.find((a) => a.id === "commander");
1232
+ expect(cmd.model).toBe("google/gemini-2.5-flash");
1233
+ expect(cmd.phase).toBe(1);
1234
+ expect(cmd.timeout).toBe("30m");
1235
+ const researcher = agents.find((a) => a.id === "researcher");
1236
+ expect(researcher.model).toBe("google/gemini-2.5-flash");
1237
+ expect(researcher.fallbackChain).toEqual(["google/gemini-2.5-flash-lite"]);
1238
+ });
1239
+ it("imports roles, matching agents to role keys", () => {
1240
+ const ast = importOpenClaw(minimalFiles, "test-topology");
1241
+ expect(ast.roles.strategy).toBe("Plan and coordinate");
1242
+ expect(ast.roles.research).toBe("Find information");
1243
+ const cmd = ast.nodes.find((n) => n.id === "commander");
1244
+ expect(cmd.role).toBe("strategy");
1245
+ });
1246
+ it("imports groups, gates, edges, hooks, schedules", () => {
1247
+ const ast = importOpenClaw(minimalFiles, "test-topology");
1248
+ const groups = ast.nodes.filter((n) => n.type === "group");
1249
+ expect(groups).toHaveLength(1);
1250
+ const gates = ast.nodes.filter((n) => n.type === "gate");
1251
+ expect(gates).toHaveLength(1);
1252
+ expect(ast.edges).toHaveLength(3);
1253
+ expect(ast.hooks).toHaveLength(2);
1254
+ expect(ast.schedules).toHaveLength(1);
1255
+ });
1256
+ it("imports skills", () => {
1257
+ const ast = importOpenClaw(minimalFiles, "test-topology");
1258
+ expect(ast.skills).toHaveLength(1);
1259
+ expect(ast.skills[0].id).toBe("campaign-management");
1260
+ expect(ast.skills[0].userInvocable).toBe(true);
1261
+ });
1262
+ it("imports params, triggers, interface endpoints", () => {
1263
+ const ast = importOpenClaw(minimalFiles, "test-topology");
1264
+ expect(ast.params).toHaveLength(2);
1265
+ expect(ast.triggers).toHaveLength(2);
1266
+ expect(ast.interfaceEndpoints).toEqual({ entry: "commander", exit: "commander" });
1267
+ });
1268
+ it("imports providers with reverse-mapped models", () => {
1269
+ const ast = importOpenClaw(minimalFiles, "test-topology");
1270
+ expect(ast.providers).toHaveLength(1);
1271
+ expect(ast.providers[0].name).toBe("openrouter");
1272
+ expect(ast.providers[0].default).toBe(true);
1273
+ });
1274
+ it("imports env and settings", () => {
1275
+ const ast = importOpenClaw(minimalFiles, "test-topology");
1276
+ expect(ast.env["MY_VAR"]).toBe("hello");
1277
+ expect(ast.settings.deny).toEqual(["dangerous-tool"]);
1278
+ });
1279
+ it("imports memory", () => {
1280
+ const ast = importOpenClaw(minimalFiles, "test-topology");
1281
+ expect(ast.memory.workspace?.path).toBe("campaigns/{CAMPAIGN}/");
1282
+ });
1283
+ });
1284
+ // ---------------------------------------------------------------------------
1285
+ // importFromPlatform — OpenClaw dispatch
1286
+ // ---------------------------------------------------------------------------
1287
+ describe("importFromPlatform (openclaw)", () => {
1288
+ it("dispatches to openclaw", () => {
1289
+ const files = [
1290
+ { path: "openclaw.json", content: OPENCLAW_JSON },
1291
+ { path: "SOUL.md", content: SOUL_MD },
1292
+ { path: "AGENTS.md", content: AGENTS_MD },
1293
+ ];
1294
+ const atSource = importFromPlatform(files, "openclaw", "test");
1295
+ // SOUL.md Identity overrides the topologyName param
1296
+ expect(atSource).toContain("topology test-topology");
1297
+ expect(atSource).toContain("agent commander");
1298
+ expect(atSource).toContain("agent researcher");
1299
+ });
1300
+ it("falls back to topologyName when SOUL.md is missing", () => {
1301
+ const files = [
1302
+ { path: "openclaw.json", content: OPENCLAW_JSON },
1303
+ { path: "AGENTS.md", content: AGENTS_MD },
1304
+ ];
1305
+ const atSource = importFromPlatform(files, "openclaw", "my-project");
1306
+ expect(atSource).toContain("topology my-project");
1307
+ });
1308
+ it("generates parseable .at output", () => {
1309
+ const files = [
1310
+ { path: "openclaw.json", content: OPENCLAW_JSON },
1311
+ { path: "SOUL.md", content: SOUL_MD },
1312
+ { path: "AGENTS.md", content: AGENTS_MD },
1313
+ { path: "MEMORY.md", content: MEMORY_MD },
1314
+ { path: "skills/campaign-management/SKILL.md", content: SKILL_MD },
1315
+ ];
1316
+ const atSource = importFromPlatform(files, "openclaw", "test");
1317
+ const ast = parse(atSource);
1318
+ expect(ast.topology.name).toBe("test-topology");
1319
+ expect(ast.nodes.filter((n) => n.type === "agent").length).toBe(2);
1320
+ });
1321
+ it("validates with zero errors (warnings allowed)", () => {
1322
+ const files = [
1323
+ { path: "openclaw.json", content: OPENCLAW_JSON },
1324
+ { path: "SOUL.md", content: SOUL_MD },
1325
+ { path: "AGENTS.md", content: AGENTS_MD },
1326
+ { path: "MEMORY.md", content: MEMORY_MD },
1327
+ { path: "skills/campaign-management/SKILL.md", content: SKILL_MD },
1328
+ ];
1329
+ const atSource = importFromPlatform(files, "openclaw", "test");
1330
+ const ast = parse(atSource);
1331
+ const errors = validate(ast).filter((e) => e.level === "error");
1332
+ expect(errors).toEqual([]);
1333
+ });
1334
+ });
781
1335
  //# sourceMappingURL=import.test.js.map