skyloom 1.12.0 → 1.13.0

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 (135) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +142 -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 +103 -51
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts.map +1 -1
  32. package/dist/cli/tui.js +8 -1
  33. package/dist/cli/tui.js.map +1 -1
  34. package/dist/core/agent/task.d.ts +58 -0
  35. package/dist/core/agent/task.d.ts.map +1 -0
  36. package/dist/core/agent/task.js +83 -0
  37. package/dist/core/agent/task.js.map +1 -0
  38. package/dist/core/agent.d.ts +2 -45
  39. package/dist/core/agent.d.ts.map +1 -1
  40. package/dist/core/agent.js +61 -145
  41. package/dist/core/agent.js.map +1 -1
  42. package/dist/core/agent_helpers.d.ts +10 -0
  43. package/dist/core/agent_helpers.d.ts.map +1 -1
  44. package/dist/core/agent_helpers.js +39 -0
  45. package/dist/core/agent_helpers.js.map +1 -1
  46. package/dist/core/catalog.d.ts +71 -0
  47. package/dist/core/catalog.d.ts.map +1 -0
  48. package/dist/core/catalog.js +176 -0
  49. package/dist/core/catalog.js.map +1 -0
  50. package/dist/core/config.d.ts +8 -0
  51. package/dist/core/config.d.ts.map +1 -1
  52. package/dist/core/config.js +12 -4
  53. package/dist/core/config.js.map +1 -1
  54. package/dist/core/factory.js +16 -16
  55. package/dist/core/llm.d.ts +7 -0
  56. package/dist/core/llm.d.ts.map +1 -1
  57. package/dist/core/llm.js +139 -7
  58. package/dist/core/llm.js.map +1 -1
  59. package/dist/core/longdoc.js +5 -5
  60. package/dist/core/memory.d.ts.map +1 -1
  61. package/dist/core/memory.js +69 -62
  62. package/dist/core/memory.js.map +1 -1
  63. package/dist/core/theme.d.ts +46 -0
  64. package/dist/core/theme.d.ts.map +1 -0
  65. package/dist/core/theme.js +42 -0
  66. package/dist/core/theme.js.map +1 -0
  67. package/dist/web/server.js +542 -519
  68. package/dist/web/server.js.map +1 -1
  69. package/docs/AESTHETIC_DESIGN.md +144 -0
  70. package/docs/OPTIMIZATION_PLAN.md +178 -0
  71. package/package.json +60 -60
  72. package/scripts/install.js +48 -48
  73. package/scripts/link.js +10 -10
  74. package/setup.bat +79 -79
  75. package/skill-test-ty2fOA/test.md +10 -10
  76. package/src/agents/dew.ts +70 -70
  77. package/src/agents/fair.ts +102 -102
  78. package/src/agents/fog.ts +48 -48
  79. package/src/agents/frost.ts +50 -50
  80. package/src/agents/rain.ts +50 -50
  81. package/src/agents/snow.ts +239 -239
  82. package/src/cli/main.ts +425 -372
  83. package/src/cli/mode.ts +58 -58
  84. package/src/cli/tui.ts +272 -269
  85. package/src/core/agent/task.ts +100 -0
  86. package/src/core/agent.ts +1446 -1549
  87. package/src/core/agent_helpers.ts +496 -461
  88. package/src/core/arbitrate.ts +162 -162
  89. package/src/core/catalog.ts +178 -0
  90. package/src/core/checkpoint.ts +94 -94
  91. package/src/core/config.ts +20 -4
  92. package/src/core/estimate.ts +104 -104
  93. package/src/core/evolve.ts +191 -191
  94. package/src/core/factory.ts +627 -627
  95. package/src/core/filter.ts +103 -103
  96. package/src/core/graph.ts +156 -156
  97. package/src/core/icons.ts +53 -53
  98. package/src/core/index.ts +37 -37
  99. package/src/core/learn.ts +146 -146
  100. package/src/core/llm.ts +108 -5
  101. package/src/core/longdoc.ts +155 -155
  102. package/src/core/mcp_server.ts +176 -176
  103. package/src/core/memory.ts +1178 -1171
  104. package/src/core/profile.ts +255 -255
  105. package/src/core/router.ts +124 -124
  106. package/src/core/sandbox.ts +142 -142
  107. package/src/core/security.ts +243 -243
  108. package/src/core/skill.ts +342 -342
  109. package/src/core/theme.ts +65 -0
  110. package/src/core/tool_router.ts +193 -193
  111. package/src/core/vector.ts +152 -152
  112. package/src/core/workspace.ts +150 -150
  113. package/src/plugins/loader.ts +66 -66
  114. package/src/skills/loader.ts +46 -46
  115. package/src/sql.js.d.ts +29 -29
  116. package/src/tools/builtin.ts +380 -380
  117. package/src/tools/computer.ts +269 -269
  118. package/src/tools/delegate.ts +49 -49
  119. package/src/web/server.ts +660 -634
  120. package/src/web/tts.ts +93 -93
  121. package/tests/agent_helpers.test.ts +48 -0
  122. package/tests/bus.test.ts +121 -121
  123. package/tests/catalog.test.ts +86 -0
  124. package/tests/config.test.ts +41 -0
  125. package/tests/icons.test.ts +45 -45
  126. package/tests/memory.test.ts +147 -0
  127. package/tests/router.test.ts +86 -86
  128. package/tests/schemas.test.ts +51 -51
  129. package/tests/semantic.test.ts +83 -83
  130. package/tests/setup.ts +10 -10
  131. package/tests/skill.test.ts +172 -172
  132. package/tests/task.test.ts +60 -0
  133. package/tests/tool.test.ts +108 -108
  134. package/tests/tool_router.test.ts +71 -71
  135. 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
+ });