skyloom 1.12.0 → 1.13.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 (137) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +137 -46
  3. package/config/default.yaml +43 -47
  4. package/config/models.yaml +155 -155
  5. package/config/providers.yaml +39 -39
  6. package/config/skills/api_integrator/SKILL.md +15 -15
  7. package/config/skills/arch_designer/SKILL.md +13 -13
  8. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  9. package/config/skills/code_analysis/SKILL.md +13 -13
  10. package/config/skills/code_generator/SKILL.md +12 -12
  11. package/config/skills/code_reviewer/SKILL.md +13 -13
  12. package/config/skills/content_writer/SKILL.md +14 -14
  13. package/config/skills/data_transformer/SKILL.md +15 -15
  14. package/config/skills/document_analysis/SKILL.md +13 -13
  15. package/config/skills/emotional_companion/SKILL.md +15 -15
  16. package/config/skills/performance_checker/SKILL.md +14 -14
  17. package/config/skills/security_auditor/SKILL.md +14 -14
  18. package/config/skills/self_evolve/SKILL.md +13 -13
  19. package/config/skills/sys_operator/SKILL.md +15 -15
  20. package/config/skills/task_planner/SKILL.md +14 -14
  21. package/config/skills/web_research/SKILL.md +14 -14
  22. package/config/skills/workflow_designer/SKILL.md +13 -13
  23. package/dist/agents/dew.js +52 -52
  24. package/dist/agents/fair.js +84 -84
  25. package/dist/agents/fog.js +30 -30
  26. package/dist/agents/frost.js +32 -32
  27. package/dist/agents/rain.js +32 -32
  28. package/dist/agents/snow.js +68 -68
  29. package/dist/cli/main.js +127 -74
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts +52 -19
  32. package/dist/cli/tui.d.ts.map +1 -1
  33. package/dist/cli/tui.js +198 -265
  34. package/dist/cli/tui.js.map +1 -1
  35. package/dist/core/agent/task.d.ts +58 -0
  36. package/dist/core/agent/task.d.ts.map +1 -0
  37. package/dist/core/agent/task.js +83 -0
  38. package/dist/core/agent/task.js.map +1 -0
  39. package/dist/core/agent.d.ts +2 -45
  40. package/dist/core/agent.d.ts.map +1 -1
  41. package/dist/core/agent.js +61 -145
  42. package/dist/core/agent.js.map +1 -1
  43. package/dist/core/agent_helpers.d.ts +10 -0
  44. package/dist/core/agent_helpers.d.ts.map +1 -1
  45. package/dist/core/agent_helpers.js +39 -0
  46. package/dist/core/agent_helpers.js.map +1 -1
  47. package/dist/core/catalog.d.ts +71 -0
  48. package/dist/core/catalog.d.ts.map +1 -0
  49. package/dist/core/catalog.js +176 -0
  50. package/dist/core/catalog.js.map +1 -0
  51. package/dist/core/config.d.ts +8 -0
  52. package/dist/core/config.d.ts.map +1 -1
  53. package/dist/core/config.js +12 -4
  54. package/dist/core/config.js.map +1 -1
  55. package/dist/core/factory.js +16 -16
  56. package/dist/core/llm.d.ts +7 -0
  57. package/dist/core/llm.d.ts.map +1 -1
  58. package/dist/core/llm.js +139 -7
  59. package/dist/core/llm.js.map +1 -1
  60. package/dist/core/longdoc.js +5 -5
  61. package/dist/core/memory.d.ts.map +1 -1
  62. package/dist/core/memory.js +69 -62
  63. package/dist/core/memory.js.map +1 -1
  64. package/dist/core/theme.d.ts +46 -0
  65. package/dist/core/theme.d.ts.map +1 -0
  66. package/dist/core/theme.js +42 -0
  67. package/dist/core/theme.js.map +1 -0
  68. package/dist/web/server.js +542 -519
  69. package/dist/web/server.js.map +1 -1
  70. package/docs/AESTHETIC_DESIGN.md +144 -0
  71. package/docs/OPTIMIZATION_PLAN.md +178 -0
  72. package/package.json +60 -60
  73. package/scripts/install.js +48 -48
  74. package/scripts/link.js +10 -10
  75. package/setup.bat +79 -79
  76. package/skill-test-ty2fOA/test.md +10 -10
  77. package/src/agents/dew.ts +70 -70
  78. package/src/agents/fair.ts +102 -102
  79. package/src/agents/fog.ts +48 -48
  80. package/src/agents/frost.ts +50 -50
  81. package/src/agents/rain.ts +50 -50
  82. package/src/agents/snow.ts +239 -239
  83. package/src/cli/main.ts +417 -372
  84. package/src/cli/mode.ts +58 -58
  85. package/src/cli/tui.ts +174 -223
  86. package/src/core/agent/task.ts +100 -0
  87. package/src/core/agent.ts +1446 -1549
  88. package/src/core/agent_helpers.ts +496 -461
  89. package/src/core/arbitrate.ts +162 -162
  90. package/src/core/catalog.ts +178 -0
  91. package/src/core/checkpoint.ts +94 -94
  92. package/src/core/config.ts +20 -4
  93. package/src/core/estimate.ts +104 -104
  94. package/src/core/evolve.ts +191 -191
  95. package/src/core/factory.ts +627 -627
  96. package/src/core/filter.ts +103 -103
  97. package/src/core/graph.ts +156 -156
  98. package/src/core/icons.ts +53 -53
  99. package/src/core/index.ts +37 -37
  100. package/src/core/learn.ts +146 -146
  101. package/src/core/llm.ts +108 -5
  102. package/src/core/longdoc.ts +155 -155
  103. package/src/core/mcp_server.ts +176 -176
  104. package/src/core/memory.ts +1178 -1171
  105. package/src/core/profile.ts +255 -255
  106. package/src/core/router.ts +124 -124
  107. package/src/core/sandbox.ts +142 -142
  108. package/src/core/security.ts +243 -243
  109. package/src/core/skill.ts +342 -342
  110. package/src/core/theme.ts +65 -0
  111. package/src/core/tool_router.ts +193 -193
  112. package/src/core/vector.ts +152 -152
  113. package/src/core/workspace.ts +150 -150
  114. package/src/plugins/loader.ts +66 -66
  115. package/src/skills/loader.ts +46 -46
  116. package/src/sql.js.d.ts +29 -29
  117. package/src/tools/builtin.ts +380 -380
  118. package/src/tools/computer.ts +269 -269
  119. package/src/tools/delegate.ts +49 -49
  120. package/src/web/server.ts +660 -634
  121. package/src/web/tts.ts +93 -93
  122. package/tests/agent_helpers.test.ts +48 -0
  123. package/tests/bus.test.ts +121 -121
  124. package/tests/catalog.test.ts +86 -0
  125. package/tests/config.test.ts +41 -0
  126. package/tests/icons.test.ts +45 -45
  127. package/tests/memory.test.ts +147 -0
  128. package/tests/router.test.ts +86 -86
  129. package/tests/schemas.test.ts +51 -51
  130. package/tests/semantic.test.ts +83 -83
  131. package/tests/setup.ts +10 -10
  132. package/tests/skill.test.ts +172 -172
  133. package/tests/task.test.ts +60 -0
  134. package/tests/tool.test.ts +108 -108
  135. package/tests/tool_router.test.ts +71 -71
  136. package/tests/tui.test.ts +67 -0
  137. package/vitest.config.ts +17 -17
@@ -1,45 +1,45 @@
1
- /**
2
- * Tests for agent icon system.
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { AGENT_COLORS, AGENT_EMOJI, iconText, svgPath } from '../src/core/icons';
6
-
7
- describe('AGENT_COLORS', () => {
8
- it('has all 6 agents', () => {
9
- expect(Object.keys(AGENT_COLORS).sort()).toEqual(['dew', 'fair', 'fog', 'frost', 'rain', 'snow']);
10
- });
11
-
12
- it('each agent has a non-empty color', () => {
13
- for (const color of Object.values(AGENT_COLORS)) {
14
- expect(color).toBeTruthy();
15
- }
16
- });
17
- });
18
-
19
- describe('AGENT_EMOJI', () => {
20
- it('has all 6 agents', () => {
21
- expect(Object.keys(AGENT_EMOJI).sort()).toEqual(['dew', 'fair', 'fog', 'frost', 'rain', 'snow']);
22
- });
23
-
24
- it('unique emoji per agent', () => {
25
- const values = Object.values(AGENT_EMOJI);
26
- expect(new Set(values).size).toBe(values.length);
27
- });
28
- });
29
-
30
- describe('iconText', () => {
31
- it('returns glyph for known agent', () => {
32
- expect(iconText('fog')).toBe('≋');
33
- expect(iconText('fair')).toBe('☼');
34
- });
35
-
36
- it('returns name as fallback for unknown agent', () => {
37
- expect(iconText('unknown')).toBe('unknown');
38
- });
39
- });
40
-
41
- describe('svgPath', () => {
42
- it('returns a path ending with .svg', () => {
43
- expect(svgPath('fog')).toContain('icons');
44
- });
45
- });
1
+ /**
2
+ * Tests for agent icon system.
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { AGENT_COLORS, AGENT_EMOJI, iconText, svgPath } from '../src/core/icons';
6
+
7
+ describe('AGENT_COLORS', () => {
8
+ it('has all 6 agents', () => {
9
+ expect(Object.keys(AGENT_COLORS).sort()).toEqual(['dew', 'fair', 'fog', 'frost', 'rain', 'snow']);
10
+ });
11
+
12
+ it('each agent has a non-empty color', () => {
13
+ for (const color of Object.values(AGENT_COLORS)) {
14
+ expect(color).toBeTruthy();
15
+ }
16
+ });
17
+ });
18
+
19
+ describe('AGENT_EMOJI', () => {
20
+ it('has all 6 agents', () => {
21
+ expect(Object.keys(AGENT_EMOJI).sort()).toEqual(['dew', 'fair', 'fog', 'frost', 'rain', 'snow']);
22
+ });
23
+
24
+ it('unique emoji per agent', () => {
25
+ const values = Object.values(AGENT_EMOJI);
26
+ expect(new Set(values).size).toBe(values.length);
27
+ });
28
+ });
29
+
30
+ describe('iconText', () => {
31
+ it('returns glyph for known agent', () => {
32
+ expect(iconText('fog')).toBe('≋');
33
+ expect(iconText('fair')).toBe('☼');
34
+ });
35
+
36
+ it('returns name as fallback for unknown agent', () => {
37
+ expect(iconText('unknown')).toBe('unknown');
38
+ });
39
+ });
40
+
41
+ describe('svgPath', () => {
42
+ it('returns a path ending with .svg', () => {
43
+ expect(svgPath('fog')).toContain('icons');
44
+ });
45
+ });
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import * as fs from "fs";
5
+ import { Memory } from "../src/core/memory";
6
+
7
+ /** addMessage mutates shortTerm through an async mutex — let microtasks flush. */
8
+ const flush = () => new Promise((r) => setTimeout(r, 15));
9
+
10
+ let tmpDirs: string[] = [];
11
+ function tmpConfig(shortTermLimit = 100) {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sky-mem-"));
13
+ tmpDirs.push(dir);
14
+ return { dbPath: path.join(dir, "memory.db"), shortTermLimit, maxPersistedMessages: 2000 };
15
+ }
16
+
17
+ afterEach(() => {
18
+ for (const d of tmpDirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ } }
19
+ tmpDirs = [];
20
+ });
21
+
22
+ describe("Memory · short-term (in-memory, no DB)", () => {
23
+ it("makes a message visible SYNCHRONOUSLY (regression: first-turn crash)", () => {
24
+ // Previously addMessage pushed inside an async mutex, so getMessages() in the
25
+ // same tick missed the message — crashing chatImpl/chatStreamImpl on a fresh
26
+ // session's first user message. The push must be synchronous.
27
+ const mem = new Memory(tmpConfig(), "fog");
28
+ mem.addMessage("user", "first message");
29
+ const msgs = mem.getMessages(); // no flush!
30
+ expect(msgs).toHaveLength(1);
31
+ expect(msgs[0]).toMatchObject({ role: "user", content: "first message" });
32
+ });
33
+
34
+ it("records and returns messages in order", async () => {
35
+ const mem = new Memory(tmpConfig(), "fog");
36
+ mem.addMessage("user", "hello");
37
+ mem.addMessage("assistant", "hi there");
38
+ await flush();
39
+ const msgs = mem.getMessages();
40
+ expect(msgs.map((m) => m.role)).toEqual(["user", "assistant"]);
41
+ expect(msgs[0].content).toBe("hello");
42
+ });
43
+
44
+ it("preserves tool-call metadata in getMessages", async () => {
45
+ const mem = new Memory(tmpConfig(), "fog");
46
+ mem.addMessage("assistant", "", { toolCalls: [{ id: "t1", function: { name: "x" } }] });
47
+ mem.addMessage("tool", "result", { name: "x", toolCallId: "t1" });
48
+ await flush();
49
+ const msgs = mem.getMessages();
50
+ const toolMsg = msgs.find((m) => m.role === "tool");
51
+ expect(toolMsg?.tool_call_id).toBe("t1");
52
+ expect(toolMsg?.name).toBe("x");
53
+ });
54
+
55
+ it("prunes past the short-term limit but keeps system messages", async () => {
56
+ const mem = new Memory(tmpConfig(3), "fog");
57
+ mem.addMessage("system", "persona");
58
+ for (let i = 0; i < 5; i++) mem.addMessage("user", `m${i}`);
59
+ await flush();
60
+ const msgs = mem.getMessages();
61
+ expect(msgs.length).toBeLessThanOrEqual(3);
62
+ expect(msgs.some((m) => m.role === "system" && m.content === "persona")).toBe(true);
63
+ // most recent user message survives
64
+ expect(msgs[msgs.length - 1].content).toBe("m4");
65
+ });
66
+
67
+ it("clearShortTerm keeps system messages", async () => {
68
+ const mem = new Memory(tmpConfig(), "fog");
69
+ mem.addMessage("system", "persona");
70
+ mem.addMessage("user", "hello");
71
+ await flush();
72
+ await mem.clearShortTerm();
73
+ const msgs = mem.getMessages();
74
+ expect(msgs).toHaveLength(1);
75
+ expect(msgs[0].role).toBe("system");
76
+ });
77
+ });
78
+
79
+ describe("Memory · context window estimation", () => {
80
+ it("counts CJK as heavier than ascii", async () => {
81
+ const mem = new Memory(tmpConfig(), "fog");
82
+ mem.addMessage("user", "你好世界"); // 4 CJK chars
83
+ await flush();
84
+ const usage = mem.getContextWindowUsage();
85
+ expect(usage.messageCount).toBe(1);
86
+ expect(usage.totalChars).toBe(4);
87
+ // CJK weight is 2/char => >= 8
88
+ expect(usage.estimatedTokens).toBeGreaterThanOrEqual(8);
89
+ });
90
+ });
91
+
92
+ describe("Memory · working memory", () => {
93
+ it("set/get/clear round-trips", () => {
94
+ const mem = new Memory(tmpConfig(), "fog");
95
+ mem.setWorking("plan", { step: 1 });
96
+ expect(mem.getWorking("plan")).toEqual({ step: 1 });
97
+ expect(mem.getWorking("missing", "fallback")).toBe("fallback");
98
+ mem.clearWorking();
99
+ expect(mem.getWorking("plan")).toBeNull();
100
+ });
101
+ });
102
+
103
+ describe("Memory · long-term (SQLite)", () => {
104
+ it("remember / recall / forget round-trips", async () => {
105
+ const mem = new Memory(tmpConfig(), "fog");
106
+ await mem.initDb();
107
+ try {
108
+ await mem.remember("favorite_lang", "typescript", "pref");
109
+ const hits = await mem.recall("favorite_lang");
110
+ expect(hits).toHaveLength(1);
111
+ expect(hits[0].value).toBe("typescript");
112
+ expect(hits[0].category).toBe("pref");
113
+
114
+ await mem.forget("favorite_lang");
115
+ expect(await mem.recall("favorite_lang")).toHaveLength(0);
116
+ } finally {
117
+ await mem.close();
118
+ }
119
+ });
120
+
121
+ it("recall filters by category", async () => {
122
+ const mem = new Memory(tmpConfig(), "fog");
123
+ await mem.initDb();
124
+ try {
125
+ await mem.remember("a", 1, "x");
126
+ await mem.remember("b", 2, "y");
127
+ const xs = await mem.recall(null, "x");
128
+ expect(xs).toHaveLength(1);
129
+ expect(xs[0].key).toBe("a");
130
+ } finally {
131
+ await mem.close();
132
+ }
133
+ });
134
+
135
+ it("getMemoryStats returns a populated object", async () => {
136
+ const mem = new Memory(tmpConfig(), "fog");
137
+ await mem.initDb();
138
+ try {
139
+ await mem.remember("k", "v");
140
+ const stats = await mem.getMemoryStats();
141
+ expect(typeof stats).toBe("object");
142
+ expect(stats).not.toBeNull();
143
+ } finally {
144
+ await mem.close();
145
+ }
146
+ });
147
+ });
@@ -1,86 +1,86 @@
1
- /**
2
- * Tests for the complexity router.
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { classify, pickAgentForGoal } from '../src/core/router';
6
-
7
- describe('classify', () => {
8
- it.each([
9
- '你好',
10
- 'hi',
11
- '在吗',
12
- '谢谢',
13
- '什么是 RAG?',
14
- '为什么天空是蓝的?',
15
- '1 + 1 = ?',
16
- '解释一下闭包',
17
- ])('returns "direct" for simple questions: %s', (goal) => {
18
- expect(classify(goal)).toBe('direct');
19
- });
20
-
21
- it.each([
22
- '帮我写一个二分查找函数',
23
- '搜一下今天的天气',
24
- '审查 src/foo.py 的安全问题',
25
- '把这段中文翻译成英文:我喜欢猫',
26
- ])('returns "single" for focused tasks: %s', (goal) => {
27
- expect(classify(goal)).toBe('single');
28
- });
29
-
30
- it.each([
31
- '先帮我分析这段代码,然后重构它,最后写测试',
32
- '首先调研一下市场上有哪些方案,其次对比性能,最后给出推荐',
33
- '1. 创建数据库迁移\n2. 写 API\n3. 加测试\n4. 部署',
34
- ])('returns "orchestrate" for multi-step: %s', (goal) => {
35
- expect(classify(goal)).toBe('orchestrate');
36
- });
37
-
38
- it('empty goal returns direct', () => {
39
- expect(classify('')).toBe('direct');
40
- expect(classify(' ')).toBe('direct');
41
- });
42
-
43
- it('inline enumerated list is orchestrate', () => {
44
- expect(classify('1. 拉数据 2. 分析 3. 出图')).toBe('orchestrate');
45
- expect(classify('先做 1. xxx 2. yyy 3. zzz 4. www')).toBe('orchestrate');
46
- });
47
-
48
- it('two inline items is not orchestrate', () => {
49
- expect(classify('1. 你好 2. 谢谢')).not.toBe('orchestrate');
50
- });
51
- });
52
-
53
- describe('pickAgentForGoal', () => {
54
- const allAgents = new Set(['fog', 'rain', 'frost', 'snow', 'dew', 'fair']);
55
-
56
- it('security keyword picks frost', () => {
57
- expect(pickAgentForGoal('帮我做安全审查', allAgents)).toBe('frost');
58
- });
59
-
60
- it('research keyword picks fog', () => {
61
- expect(pickAgentForGoal('搜一下最新的 React 文档', allAgents)).toBe('fog');
62
- });
63
-
64
- it('greeting picks fair', () => {
65
- expect(pickAgentForGoal('你好啊', allAgents)).toBe('fair');
66
- });
67
-
68
- it('falls back to rain', () => {
69
- expect(pickAgentForGoal('处理这个东西', allAgents)).toBe('rain');
70
- });
71
-
72
- it('binary search picks rain not fog', () => {
73
- expect(pickAgentForGoal('帮我写一个二分查找', allAgents)).toBe('rain');
74
- expect(pickAgentForGoal('实现一个排序函数', allAgents)).toBe('rain');
75
- });
76
-
77
- it('skips missing agents', () => {
78
- const available = new Set(['rain', 'snow']);
79
- const result = pickAgentForGoal('做安全审查', available);
80
- expect(available.has(result)).toBe(true);
81
- });
82
-
83
- it('single agent available', () => {
84
- expect(pickAgentForGoal('anything', new Set(['rain']))).toBe('rain');
85
- });
86
- });
1
+ /**
2
+ * Tests for the complexity router.
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { classify, pickAgentForGoal } from '../src/core/router';
6
+
7
+ describe('classify', () => {
8
+ it.each([
9
+ '你好',
10
+ 'hi',
11
+ '在吗',
12
+ '谢谢',
13
+ '什么是 RAG?',
14
+ '为什么天空是蓝的?',
15
+ '1 + 1 = ?',
16
+ '解释一下闭包',
17
+ ])('returns "direct" for simple questions: %s', (goal) => {
18
+ expect(classify(goal)).toBe('direct');
19
+ });
20
+
21
+ it.each([
22
+ '帮我写一个二分查找函数',
23
+ '搜一下今天的天气',
24
+ '审查 src/foo.py 的安全问题',
25
+ '把这段中文翻译成英文:我喜欢猫',
26
+ ])('returns "single" for focused tasks: %s', (goal) => {
27
+ expect(classify(goal)).toBe('single');
28
+ });
29
+
30
+ it.each([
31
+ '先帮我分析这段代码,然后重构它,最后写测试',
32
+ '首先调研一下市场上有哪些方案,其次对比性能,最后给出推荐',
33
+ '1. 创建数据库迁移\n2. 写 API\n3. 加测试\n4. 部署',
34
+ ])('returns "orchestrate" for multi-step: %s', (goal) => {
35
+ expect(classify(goal)).toBe('orchestrate');
36
+ });
37
+
38
+ it('empty goal returns direct', () => {
39
+ expect(classify('')).toBe('direct');
40
+ expect(classify(' ')).toBe('direct');
41
+ });
42
+
43
+ it('inline enumerated list is orchestrate', () => {
44
+ expect(classify('1. 拉数据 2. 分析 3. 出图')).toBe('orchestrate');
45
+ expect(classify('先做 1. xxx 2. yyy 3. zzz 4. www')).toBe('orchestrate');
46
+ });
47
+
48
+ it('two inline items is not orchestrate', () => {
49
+ expect(classify('1. 你好 2. 谢谢')).not.toBe('orchestrate');
50
+ });
51
+ });
52
+
53
+ describe('pickAgentForGoal', () => {
54
+ const allAgents = new Set(['fog', 'rain', 'frost', 'snow', 'dew', 'fair']);
55
+
56
+ it('security keyword picks frost', () => {
57
+ expect(pickAgentForGoal('帮我做安全审查', allAgents)).toBe('frost');
58
+ });
59
+
60
+ it('research keyword picks fog', () => {
61
+ expect(pickAgentForGoal('搜一下最新的 React 文档', allAgents)).toBe('fog');
62
+ });
63
+
64
+ it('greeting picks fair', () => {
65
+ expect(pickAgentForGoal('你好啊', allAgents)).toBe('fair');
66
+ });
67
+
68
+ it('falls back to rain', () => {
69
+ expect(pickAgentForGoal('处理这个东西', allAgents)).toBe('rain');
70
+ });
71
+
72
+ it('binary search picks rain not fog', () => {
73
+ expect(pickAgentForGoal('帮我写一个二分查找', allAgents)).toBe('rain');
74
+ expect(pickAgentForGoal('实现一个排序函数', allAgents)).toBe('rain');
75
+ });
76
+
77
+ it('skips missing agents', () => {
78
+ const available = new Set(['rain', 'snow']);
79
+ const result = pickAgentForGoal('做安全审查', available);
80
+ expect(available.has(result)).toBe(true);
81
+ });
82
+
83
+ it('single agent available', () => {
84
+ expect(pickAgentForGoal('anything', new Set(['rain']))).toBe('rain');
85
+ });
86
+ });
@@ -1,51 +1,51 @@
1
- /**
2
- * Tests for structured output schema validation.
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { validateTaskPlan, TaskPlanSchema, parseSchema, SchemaValidationError } from '../src/core/schemas';
6
-
7
- describe('validateTaskPlan', () => {
8
- it('parses valid JSON plan', () => {
9
- const data = JSON.parse('{"goal": "build app", "steps": [{"id": "1", "description": "design", "agent": "fog"}]}');
10
- const plan = validateTaskPlan(data);
11
- expect(plan.goal).toBe('build app');
12
- expect(plan.steps).toHaveLength(1);
13
- expect(plan.steps[0].agent).toBe('fog');
14
- });
15
-
16
- it('passes through agent name', () => {
17
- const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a", "agent": "unknown"}]}');
18
- const plan = validateTaskPlan(data);
19
- // Schema-level validation just passes through; agent fallback is in SnowAgent
20
- expect(plan.steps[0].agent).toBe('unknown');
21
- });
22
-
23
- it('empty steps', () => {
24
- const data = JSON.parse('{"goal": "x", "steps": []}');
25
- const plan = validateTaskPlan(data);
26
- expect(plan.steps).toHaveLength(0);
27
- });
28
-
29
- it('parses depends_on', () => {
30
- const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a", "depends_on": ["0"]}]}');
31
- const plan = validateTaskPlan(data);
32
- expect(plan.steps[0].depends_on).toEqual(['0']);
33
- });
34
-
35
- it('default fields', () => {
36
- const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a"}]}');
37
- const plan = validateTaskPlan(data);
38
- expect(plan.steps[0].agent).toBe('rain');
39
- expect(plan.steps[0].depends_on).toEqual([]);
40
- });
41
- });
42
-
43
- describe('SchemaValidationError', () => {
44
- it('raises on empty', () => {
45
- expect(() => parseSchema('', TaskPlanSchema)).toThrow(SchemaValidationError);
46
- });
47
-
48
- it('raises on garbage', () => {
49
- expect(() => parseSchema('<html>garbage</html>', TaskPlanSchema)).toThrow(SchemaValidationError);
50
- });
51
- });
1
+ /**
2
+ * Tests for structured output schema validation.
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { validateTaskPlan, TaskPlanSchema, parseSchema, SchemaValidationError } from '../src/core/schemas';
6
+
7
+ describe('validateTaskPlan', () => {
8
+ it('parses valid JSON plan', () => {
9
+ const data = JSON.parse('{"goal": "build app", "steps": [{"id": "1", "description": "design", "agent": "fog"}]}');
10
+ const plan = validateTaskPlan(data);
11
+ expect(plan.goal).toBe('build app');
12
+ expect(plan.steps).toHaveLength(1);
13
+ expect(plan.steps[0].agent).toBe('fog');
14
+ });
15
+
16
+ it('passes through agent name', () => {
17
+ const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a", "agent": "unknown"}]}');
18
+ const plan = validateTaskPlan(data);
19
+ // Schema-level validation just passes through; agent fallback is in SnowAgent
20
+ expect(plan.steps[0].agent).toBe('unknown');
21
+ });
22
+
23
+ it('empty steps', () => {
24
+ const data = JSON.parse('{"goal": "x", "steps": []}');
25
+ const plan = validateTaskPlan(data);
26
+ expect(plan.steps).toHaveLength(0);
27
+ });
28
+
29
+ it('parses depends_on', () => {
30
+ const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a", "depends_on": ["0"]}]}');
31
+ const plan = validateTaskPlan(data);
32
+ expect(plan.steps[0].depends_on).toEqual(['0']);
33
+ });
34
+
35
+ it('default fields', () => {
36
+ const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a"}]}');
37
+ const plan = validateTaskPlan(data);
38
+ expect(plan.steps[0].agent).toBe('rain');
39
+ expect(plan.steps[0].depends_on).toEqual([]);
40
+ });
41
+ });
42
+
43
+ describe('SchemaValidationError', () => {
44
+ it('raises on empty', () => {
45
+ expect(() => parseSchema('', TaskPlanSchema)).toThrow(SchemaValidationError);
46
+ });
47
+
48
+ it('raises on garbage', () => {
49
+ expect(() => parseSchema('<html>garbage</html>', TaskPlanSchema)).toThrow(SchemaValidationError);
50
+ });
51
+ });