skyloom 1.13.6 → 1.13.8

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 (193) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +220 -159
  3. package/config/providers.yaml +39 -39
  4. package/config/skills/api_integrator/SKILL.md +15 -15
  5. package/config/skills/arch_designer/SKILL.md +13 -13
  6. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  7. package/config/skills/code_analysis/SKILL.md +13 -13
  8. package/config/skills/code_generator/SKILL.md +12 -12
  9. package/config/skills/code_reviewer/SKILL.md +13 -13
  10. package/config/skills/content_writer/SKILL.md +14 -14
  11. package/config/skills/data_transformer/SKILL.md +15 -15
  12. package/config/skills/document_analysis/SKILL.md +13 -13
  13. package/config/skills/emotional_companion/SKILL.md +15 -15
  14. package/config/skills/performance_checker/SKILL.md +14 -14
  15. package/config/skills/security_auditor/SKILL.md +14 -14
  16. package/config/skills/self_evolve/SKILL.md +13 -13
  17. package/config/skills/sys_operator/SKILL.md +15 -15
  18. package/config/skills/task_planner/SKILL.md +14 -14
  19. package/config/skills/web_research/SKILL.md +14 -14
  20. package/config/skills/workflow_designer/SKILL.md +13 -13
  21. package/dist/agents/dew.js +52 -52
  22. package/dist/agents/fair.js +84 -84
  23. package/dist/agents/fog.js +30 -30
  24. package/dist/agents/frost.js +32 -32
  25. package/dist/agents/rain.js +32 -32
  26. package/dist/agents/snow.js +68 -68
  27. package/dist/cli/commands_md.d.ts +41 -0
  28. package/dist/cli/commands_md.d.ts.map +1 -0
  29. package/dist/cli/commands_md.js +140 -0
  30. package/dist/cli/commands_md.js.map +1 -0
  31. package/dist/cli/input_macros.d.ts +28 -0
  32. package/dist/cli/input_macros.d.ts.map +1 -0
  33. package/dist/cli/input_macros.js +120 -0
  34. package/dist/cli/input_macros.js.map +1 -0
  35. package/dist/cli/loom.d.ts +220 -0
  36. package/dist/cli/loom.d.ts.map +1 -0
  37. package/dist/cli/loom.js +1094 -0
  38. package/dist/cli/loom.js.map +1 -0
  39. package/dist/cli/loom_chat.d.ts +20 -0
  40. package/dist/cli/loom_chat.d.ts.map +1 -0
  41. package/dist/cli/loom_chat.js +685 -0
  42. package/dist/cli/loom_chat.js.map +1 -0
  43. package/dist/cli/main.js +310 -14
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/tui.d.ts.map +1 -1
  46. package/dist/cli/tui.js +7 -1
  47. package/dist/cli/tui.js.map +1 -1
  48. package/dist/core/agent.d.ts +20 -0
  49. package/dist/core/agent.d.ts.map +1 -1
  50. package/dist/core/agent.js +199 -16
  51. package/dist/core/agent.js.map +1 -1
  52. package/dist/core/factory.d.ts.map +1 -1
  53. package/dist/core/factory.js +34 -2
  54. package/dist/core/factory.js.map +1 -1
  55. package/dist/core/file_checkpoint.d.ts +57 -0
  56. package/dist/core/file_checkpoint.d.ts.map +1 -0
  57. package/dist/core/file_checkpoint.js +162 -0
  58. package/dist/core/file_checkpoint.js.map +1 -0
  59. package/dist/core/hooks.d.ts +43 -0
  60. package/dist/core/hooks.d.ts.map +1 -0
  61. package/dist/core/hooks.js +110 -0
  62. package/dist/core/hooks.js.map +1 -0
  63. package/dist/core/llm.d.ts.map +1 -1
  64. package/dist/core/llm.js +15 -9
  65. package/dist/core/llm.js.map +1 -1
  66. package/dist/core/longdoc.js +5 -5
  67. package/dist/core/mcp.d.ts +16 -0
  68. package/dist/core/mcp.d.ts.map +1 -1
  69. package/dist/core/mcp.js +55 -0
  70. package/dist/core/mcp.js.map +1 -1
  71. package/dist/core/model_config.d.ts +40 -0
  72. package/dist/core/model_config.d.ts.map +1 -0
  73. package/dist/core/model_config.js +191 -0
  74. package/dist/core/model_config.js.map +1 -0
  75. package/dist/core/skill.d.ts +7 -0
  76. package/dist/core/skill.d.ts.map +1 -1
  77. package/dist/core/skill.js +47 -0
  78. package/dist/core/skill.js.map +1 -1
  79. package/dist/core/skymd.d.ts +39 -0
  80. package/dist/core/skymd.d.ts.map +1 -0
  81. package/dist/core/skymd.js +177 -0
  82. package/dist/core/skymd.js.map +1 -0
  83. package/dist/core/tool.d.ts +12 -0
  84. package/dist/core/tool.d.ts.map +1 -1
  85. package/dist/core/tool.js +30 -0
  86. package/dist/core/tool.js.map +1 -1
  87. package/dist/core/verify.d.ts +27 -0
  88. package/dist/core/verify.d.ts.map +1 -0
  89. package/dist/core/verify.js +62 -0
  90. package/dist/core/verify.js.map +1 -0
  91. package/dist/skills/loader.d.ts +22 -2
  92. package/dist/skills/loader.d.ts.map +1 -1
  93. package/dist/skills/loader.js +45 -15
  94. package/dist/skills/loader.js.map +1 -1
  95. package/dist/tools/builtin.d.ts.map +1 -1
  96. package/dist/tools/builtin.js +13 -3
  97. package/dist/tools/builtin.js.map +1 -1
  98. package/dist/tools/model_tool.d.ts +11 -0
  99. package/dist/tools/model_tool.d.ts.map +1 -0
  100. package/dist/tools/model_tool.js +71 -0
  101. package/dist/tools/model_tool.js.map +1 -0
  102. package/dist/tools/todo.d.ts +30 -0
  103. package/dist/tools/todo.d.ts.map +1 -0
  104. package/dist/tools/todo.js +78 -0
  105. package/dist/tools/todo.js.map +1 -0
  106. package/docs/AESTHETIC_DESIGN.md +152 -144
  107. package/docs/OPTIMIZATION_PLAN.md +178 -178
  108. package/package.json +68 -68
  109. package/scripts/install.js +48 -48
  110. package/scripts/link.js +10 -10
  111. package/setup.bat +79 -79
  112. package/skill-test-ty2fOA/test.md +10 -10
  113. package/src/agents/dew.ts +70 -70
  114. package/src/agents/fair.ts +102 -102
  115. package/src/agents/fog.ts +48 -48
  116. package/src/agents/frost.ts +50 -50
  117. package/src/agents/rain.ts +50 -50
  118. package/src/agents/snow.ts +239 -239
  119. package/src/cli/commands_md.ts +112 -0
  120. package/src/cli/input_macros.ts +83 -0
  121. package/src/cli/loom.ts +982 -0
  122. package/src/cli/loom_chat.ts +598 -0
  123. package/src/cli/main.ts +255 -9
  124. package/src/cli/mode.ts +58 -58
  125. package/src/cli/tui.ts +228 -222
  126. package/src/core/agent/guard.ts +134 -134
  127. package/src/core/agent/task.ts +100 -100
  128. package/src/core/agent.ts +195 -16
  129. package/src/core/arbitrate.ts +162 -162
  130. package/src/core/catalog.ts +178 -178
  131. package/src/core/checkpoint.ts +94 -94
  132. package/src/core/estimate.ts +104 -104
  133. package/src/core/evolve.ts +191 -191
  134. package/src/core/factory.ts +31 -2
  135. package/src/core/file_checkpoint.ts +136 -0
  136. package/src/core/filter.ts +103 -103
  137. package/src/core/graph.ts +156 -156
  138. package/src/core/hooks.ts +126 -0
  139. package/src/core/icons.ts +53 -53
  140. package/src/core/index.ts +37 -37
  141. package/src/core/learn.ts +146 -146
  142. package/src/core/llm.ts +15 -9
  143. package/src/core/longdoc.ts +155 -155
  144. package/src/core/mcp.ts +48 -0
  145. package/src/core/mcp_server.ts +176 -176
  146. package/src/core/model_config.ts +157 -0
  147. package/src/core/profile.ts +255 -255
  148. package/src/core/router.ts +124 -124
  149. package/src/core/sandbox.ts +142 -142
  150. package/src/core/security.ts +243 -243
  151. package/src/core/skill.ts +42 -0
  152. package/src/core/skymd.ts +143 -0
  153. package/src/core/theme.ts +65 -65
  154. package/src/core/tool.ts +30 -0
  155. package/src/core/tool_router.ts +193 -193
  156. package/src/core/vector.ts +152 -152
  157. package/src/core/verify.ts +71 -0
  158. package/src/core/workspace.ts +150 -150
  159. package/src/plugins/loader.ts +66 -66
  160. package/src/skills/loader.ts +45 -16
  161. package/src/sql.js.d.ts +29 -29
  162. package/src/tools/builtin.ts +13 -3
  163. package/src/tools/computer.ts +269 -269
  164. package/src/tools/delegate.ts +49 -49
  165. package/src/tools/model_tool.ts +74 -0
  166. package/src/tools/todo.ts +76 -0
  167. package/src/web/tts.ts +93 -93
  168. package/tests/agent.test.ts +159 -159
  169. package/tests/agent_helpers.test.ts +48 -48
  170. package/tests/bus.test.ts +121 -121
  171. package/tests/catalog.test.ts +86 -86
  172. package/tests/checkpoint_commands.test.ts +124 -0
  173. package/tests/claude_compat.test.ts +110 -0
  174. package/tests/config.test.ts +41 -41
  175. package/tests/guard.test.ts +75 -75
  176. package/tests/icons.test.ts +45 -45
  177. package/tests/loom.test.ts +248 -0
  178. package/tests/memory.test.ts +170 -170
  179. package/tests/model_config.test.ts +109 -0
  180. package/tests/router.test.ts +86 -86
  181. package/tests/schemas.test.ts +51 -51
  182. package/tests/semantic.test.ts +83 -83
  183. package/tests/setup.ts +10 -10
  184. package/tests/skill.test.ts +172 -172
  185. package/tests/skymd.test.ts +146 -0
  186. package/tests/task.test.ts +60 -60
  187. package/tests/todo_toolstats.test.ts +94 -0
  188. package/tests/tool.test.ts +108 -108
  189. package/tests/tool_router.test.ts +71 -71
  190. package/tests/tui.test.ts +67 -67
  191. package/vitest.config.ts +17 -17
  192. package/=12 +0 -0
  193. package/=8 +0 -0
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import { loadProjectMemory, appendQuickMemory, parseVerifyCommands, projectMemoryFile } from "../src/core/skymd";
6
+ import { resolveVerifyConfig, runVerify } from "../src/core/verify";
7
+ import { loadHooks, matches, runPreToolHooks } from "../src/core/hooks";
8
+ import { expandFileRefs, isBangCommand, bangCommand, runBang, isHashMemory, hashNote } from "../src/cli/input_macros";
9
+
10
+ let tmp: string;
11
+ beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skymd-")); });
12
+ afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
13
+
14
+ describe("项目记忆 SKY.md", () => {
15
+ it("loads the project layer and reports contributing files", () => {
16
+ fs.writeFileSync(path.join(tmp, "SKY.md"), "构建: npm run build");
17
+ const mem = loadProjectMemory(tmp);
18
+ expect(mem.text).toContain("构建: npm run build");
19
+ expect(mem.files.some(f => f.endsWith("SKY.md"))).toBe(true);
20
+ });
21
+
22
+ it("falls back to CLAUDE.md / AGENTS.md for compatibility", () => {
23
+ fs.writeFileSync(path.join(tmp, "AGENTS.md"), "agents 约定");
24
+ expect(projectMemoryFile(tmp)).toContain("AGENTS.md");
25
+ expect(loadProjectMemory(tmp).text).toContain("agents 约定");
26
+ // SKY.md wins over AGENTS.md when both exist
27
+ fs.writeFileSync(path.join(tmp, "SKY.md"), "sky 约定");
28
+ expect(projectMemoryFile(tmp)).toContain("SKY.md");
29
+ });
30
+
31
+ it("layers SKY.local.md after the project file", () => {
32
+ fs.writeFileSync(path.join(tmp, "SKY.md"), "shared");
33
+ fs.writeFileSync(path.join(tmp, "SKY.local.md"), "personal");
34
+ const text = loadProjectMemory(tmp).text;
35
+ expect(text.indexOf("shared")).toBeLessThan(text.indexOf("personal"));
36
+ });
37
+
38
+ it("clamps oversized files", () => {
39
+ fs.writeFileSync(path.join(tmp, "SKY.md"), "x".repeat(50000));
40
+ expect(loadProjectMemory(tmp).text.length).toBeLessThan(15000);
41
+ });
42
+
43
+ it("appendQuickMemory creates SKY.md with a header, then appends", () => {
44
+ const f1 = appendQuickMemory("测试必须用 vitest", tmp);
45
+ expect(fs.readFileSync(f1, "utf-8")).toContain("- 测试必须用 vitest");
46
+ appendQuickMemory("禁止 any", tmp);
47
+ const content = fs.readFileSync(f1, "utf-8");
48
+ expect(content).toContain("- 禁止 any");
49
+ expect(content.indexOf("vitest")).toBeLessThan(content.indexOf("禁止 any"));
50
+ });
51
+
52
+ it("appendQuickMemory targets an existing CLAUDE.md", () => {
53
+ fs.writeFileSync(path.join(tmp, "CLAUDE.md"), "# rules\n");
54
+ const f = appendQuickMemory("note", tmp);
55
+ expect(f).toContain("CLAUDE.md");
56
+ });
57
+
58
+ it("parseVerifyCommands extracts the fenced block under ## Verify", () => {
59
+ const text = "# SKY\n\n## Verify\n```bash\nnpm test\n# comment\nnpm run lint\n```\n\n## Other\n";
60
+ expect(parseVerifyCommands(text)).toEqual(["npm test", "npm run lint"]);
61
+ expect(parseVerifyCommands("no section")).toEqual([]);
62
+ });
63
+ });
64
+
65
+ describe("验证闭环", () => {
66
+ it("passes when every command exits 0", () => {
67
+ const r = runVerify({ commands: ["node -e \"process.exit(0)\""], maxFixRounds: 2, timeoutS: 30 }, tmp);
68
+ expect(r.ok).toBe(true);
69
+ expect(r.report).toContain("✓");
70
+ });
71
+
72
+ it("stops at the first failure and captures output", () => {
73
+ const r = runVerify({
74
+ commands: ["node -e \"console.error('boom'); process.exit(3)\"", "node -e \"process.exit(0)\""],
75
+ maxFixRounds: 2, timeoutS: 30,
76
+ }, tmp);
77
+ expect(r.ok).toBe(false);
78
+ expect(r.report).toContain("boom");
79
+ expect(r.report).toContain("exit 3");
80
+ expect(r.report).not.toContain("✓ node -e \"process.exit(0)\"");
81
+ });
82
+
83
+ it("resolveVerifyConfig prefers config over SKY.md", () => {
84
+ const cfg = resolveVerifyConfig({ verify: { commands: ["a"], max_fix_rounds: 5 } }, tmp);
85
+ expect(cfg.commands).toEqual(["a"]);
86
+ expect(cfg.maxFixRounds).toBe(5);
87
+ });
88
+ });
89
+
90
+ describe("hooks", () => {
91
+ it("loads and normalizes config shapes", () => {
92
+ const h = loadHooks({ hooks: { session_start: ["echo hi"], pre_tool: [{ matcher: "run_bash", command: "true" }], post_tool: ["echo done"] } });
93
+ expect(h.sessionStart).toEqual(["echo hi"]);
94
+ expect(h.preTool[0].matcher).toBe("run_bash");
95
+ expect(h.postTool[0].command).toBe("echo done");
96
+ });
97
+
98
+ it("matcher is a regex on the tool name (missing matcher = all)", () => {
99
+ expect(matches({ matcher: "write_|edit_", command: "x" }, "write_file")).toBe(true);
100
+ expect(matches({ matcher: "write_|edit_", command: "x" }, "read_file")).toBe(false);
101
+ expect(matches({ command: "x" }, "anything")).toBe(true);
102
+ });
103
+
104
+ it("pre_tool hook blocks on non-zero exit", () => {
105
+ const h = loadHooks({ hooks: { pre_tool: [{ matcher: "danger", command: "node -e \"console.log('nope'); process.exit(1)\"" }] } });
106
+ const blocked = runPreToolHooks(h, "danger_tool", {}, "fog");
107
+ expect(blocked.allowed).toBe(false);
108
+ expect(blocked.reason).toContain("nope");
109
+ const allowed = runPreToolHooks(h, "read_file", {}, "fog");
110
+ expect(allowed.allowed).toBe(true);
111
+ });
112
+ });
113
+
114
+ describe("输入宏", () => {
115
+ it("# quick memory detection", () => {
116
+ expect(isHashMemory("# 用 pnpm")).toBe(true);
117
+ expect(isHashMemory("#用 pnpm")).toBe(true);
118
+ expect(isHashMemory("## markdown heading")).toBe(false);
119
+ expect(isHashMemory("正文 # 不算")).toBe(false);
120
+ expect(hashNote("# 用 pnpm ")).toBe("用 pnpm");
121
+ });
122
+
123
+ it("! shell detection and execution", () => {
124
+ expect(isBangCommand("!git status")).toBe(true);
125
+ expect(isBangCommand("hello!")).toBe(false);
126
+ expect(bangCommand("! echo hi")).toBe("echo hi");
127
+ const r = runBang("node -e \"console.log('out')\"", tmp);
128
+ expect(r.ok).toBe(true);
129
+ expect(r.output).toBe("out");
130
+ expect(runBang("node -e \"process.exit(2)\"", tmp).ok).toBe(false);
131
+ });
132
+
133
+ it("@file expands existing files into fenced attachments", () => {
134
+ fs.writeFileSync(path.join(tmp, "a.txt"), "file content here");
135
+ const r = expandFileRefs("看一下 @a.txt 的内容", tmp);
136
+ expect(r.attached).toEqual(["a.txt"]);
137
+ expect(r.text).toContain("file content here");
138
+ expect(r.text).toContain("看一下 @a.txt 的内容"); // original preserved
139
+ });
140
+
141
+ it("@file leaves missing files untouched", () => {
142
+ const r = expandFileRefs("ping @nonexistent.ts", tmp);
143
+ expect(r.attached).toEqual([]);
144
+ expect(r.text).toBe("ping @nonexistent.ts");
145
+ });
146
+ });
@@ -1,60 +1,60 @@
1
- import { describe, it, expect } from "vitest";
2
- import { Task, TaskState, TaskResult, VALID_TRANSITIONS } from "../src/core/agent/task";
3
- // Also assert the re-export path stays stable for external importers.
4
- import { Task as TaskViaAgent, TaskState as TaskStateViaAgent } from "../src/core/agent";
5
-
6
- describe("Task domain model", () => {
7
- it("applies sensible defaults", () => {
8
- const t = new Task({ id: "1", description: "do a thing" });
9
- expect(t.status).toBe(TaskState.PENDING);
10
- expect(t.assignedTo).toBeNull();
11
- expect(t.dependsOn).toEqual([]);
12
- expect(t.priority).toBe(0);
13
- });
14
-
15
- it("allows PENDING -> RUNNING -> COMPLETED", () => {
16
- const t = new Task({ id: "1", description: "x" });
17
- t.transitionTo(TaskState.RUNNING);
18
- expect(t.status).toBe(TaskState.RUNNING);
19
- t.transitionTo(TaskState.COMPLETED);
20
- expect(t.status).toBe(TaskState.COMPLETED);
21
- });
22
-
23
- it("rejects illegal transitions", () => {
24
- const t = new Task({ id: "1", description: "x" });
25
- // PENDING -> COMPLETED is not allowed (must go through RUNNING)
26
- expect(() => t.transitionTo(TaskState.COMPLETED)).toThrow(/Invalid task state transition/);
27
- });
28
-
29
- it("treats COMPLETED as terminal", () => {
30
- expect(VALID_TRANSITIONS[TaskState.COMPLETED].size).toBe(0);
31
- expect(VALID_TRANSITIONS[TaskState.SKIPPED].size).toBe(0);
32
- });
33
-
34
- it("allows FAILED -> RUNNING (retry) and FAILED -> SKIPPED", () => {
35
- const t = new Task({ id: "1", description: "x", status: TaskState.FAILED });
36
- expect(() => t.transitionTo(TaskState.RUNNING)).not.toThrow();
37
- const t2 = new Task({ id: "2", description: "y", status: TaskState.FAILED });
38
- expect(() => t2.transitionTo(TaskState.SKIPPED)).not.toThrow();
39
- });
40
-
41
- it("allDeps merges parentId with dependsOn without duplicates", () => {
42
- const t = new Task({ id: "3", description: "z", parentId: "1", dependsOn: ["1", "2"] });
43
- expect(t.allDeps.sort()).toEqual(["1", "2"]);
44
- const t2 = new Task({ id: "4", description: "z", parentId: "9", dependsOn: ["2"] });
45
- expect(t2.allDeps.sort()).toEqual(["2", "9"]);
46
- });
47
-
48
- it("TaskResult carries success/content/data", () => {
49
- const ok = new TaskResult(true, "done", { x: 1 });
50
- expect(ok.success).toBe(true);
51
- expect(ok.content).toBe("done");
52
- expect(ok.data).toEqual({ x: 1 });
53
- expect(new TaskResult(false, "oops").data).toEqual({});
54
- });
55
-
56
- it("re-export from ../core/agent stays identical", () => {
57
- expect(TaskViaAgent).toBe(Task);
58
- expect(TaskStateViaAgent).toBe(TaskState);
59
- });
60
- });
1
+ import { describe, it, expect } from "vitest";
2
+ import { Task, TaskState, TaskResult, VALID_TRANSITIONS } from "../src/core/agent/task";
3
+ // Also assert the re-export path stays stable for external importers.
4
+ import { Task as TaskViaAgent, TaskState as TaskStateViaAgent } from "../src/core/agent";
5
+
6
+ describe("Task domain model", () => {
7
+ it("applies sensible defaults", () => {
8
+ const t = new Task({ id: "1", description: "do a thing" });
9
+ expect(t.status).toBe(TaskState.PENDING);
10
+ expect(t.assignedTo).toBeNull();
11
+ expect(t.dependsOn).toEqual([]);
12
+ expect(t.priority).toBe(0);
13
+ });
14
+
15
+ it("allows PENDING -> RUNNING -> COMPLETED", () => {
16
+ const t = new Task({ id: "1", description: "x" });
17
+ t.transitionTo(TaskState.RUNNING);
18
+ expect(t.status).toBe(TaskState.RUNNING);
19
+ t.transitionTo(TaskState.COMPLETED);
20
+ expect(t.status).toBe(TaskState.COMPLETED);
21
+ });
22
+
23
+ it("rejects illegal transitions", () => {
24
+ const t = new Task({ id: "1", description: "x" });
25
+ // PENDING -> COMPLETED is not allowed (must go through RUNNING)
26
+ expect(() => t.transitionTo(TaskState.COMPLETED)).toThrow(/Invalid task state transition/);
27
+ });
28
+
29
+ it("treats COMPLETED as terminal", () => {
30
+ expect(VALID_TRANSITIONS[TaskState.COMPLETED].size).toBe(0);
31
+ expect(VALID_TRANSITIONS[TaskState.SKIPPED].size).toBe(0);
32
+ });
33
+
34
+ it("allows FAILED -> RUNNING (retry) and FAILED -> SKIPPED", () => {
35
+ const t = new Task({ id: "1", description: "x", status: TaskState.FAILED });
36
+ expect(() => t.transitionTo(TaskState.RUNNING)).not.toThrow();
37
+ const t2 = new Task({ id: "2", description: "y", status: TaskState.FAILED });
38
+ expect(() => t2.transitionTo(TaskState.SKIPPED)).not.toThrow();
39
+ });
40
+
41
+ it("allDeps merges parentId with dependsOn without duplicates", () => {
42
+ const t = new Task({ id: "3", description: "z", parentId: "1", dependsOn: ["1", "2"] });
43
+ expect(t.allDeps.sort()).toEqual(["1", "2"]);
44
+ const t2 = new Task({ id: "4", description: "z", parentId: "9", dependsOn: ["2"] });
45
+ expect(t2.allDeps.sort()).toEqual(["2", "9"]);
46
+ });
47
+
48
+ it("TaskResult carries success/content/data", () => {
49
+ const ok = new TaskResult(true, "done", { x: 1 });
50
+ expect(ok.success).toBe(true);
51
+ expect(ok.content).toBe("done");
52
+ expect(ok.data).toEqual({ x: 1 });
53
+ expect(new TaskResult(false, "oops").data).toEqual({});
54
+ });
55
+
56
+ it("re-export from ../core/agent stays identical", () => {
57
+ expect(TaskViaAgent).toBe(Task);
58
+ expect(TaskStateViaAgent).toBe(TaskState);
59
+ });
60
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseTodoItems, renderTodoList, createTodoTool, TODO_WORKING_KEY } from "../src/tools/todo";
3
+ import { clampToolResult } from "../src/core/agent";
4
+ import { ToolRegistry } from "../src/core/tool";
5
+
6
+ describe("todo_write 任务清单", () => {
7
+ it("parses a JSON array with status validation and defaults", () => {
8
+ const { items, error } = parseTodoItems('[{"text":"调研","status":"done"},{"text":"实现","status":"active"},{"text":"测试"}]');
9
+ expect(error).toBe("");
10
+ expect(items).toEqual([
11
+ { text: "调研", status: "done" },
12
+ { text: "实现", status: "active" },
13
+ { text: "测试", status: "pending" },
14
+ ]);
15
+ // plain string items are accepted as pending
16
+ expect(parseTodoItems('["a","b"]').items).toEqual([
17
+ { text: "a", status: "pending" },
18
+ { text: "b", status: "pending" },
19
+ ]);
20
+ });
21
+
22
+ it("rejects malformed input", () => {
23
+ expect(parseTodoItems("not json").items).toBeNull();
24
+ expect(parseTodoItems('{"text":"x"}').items).toBeNull();
25
+ expect(parseTodoItems('[{"text":""}]').items).toBeNull();
26
+ expect(parseTodoItems(JSON.stringify(Array(25).fill({ text: "x" }))).items).toBeNull();
27
+ });
28
+
29
+ it("renderTodoList shows progress and per-item marks", () => {
30
+ const out = renderTodoList([
31
+ { text: "调研", status: "done" },
32
+ { text: "实现", status: "active" },
33
+ { text: "测试", status: "pending" },
34
+ ]);
35
+ expect(out).toContain("任务清单 1/3");
36
+ expect(out).toContain("✓ 调研");
37
+ expect(out).toContain("◐ 实现");
38
+ expect(out).toContain("· 测试");
39
+ });
40
+
41
+ it("the tool stores the list in working memory (survives compaction)", async () => {
42
+ const working: Record<string, any> = {};
43
+ const fakeAgent = { memory: { setWorking: (k: string, v: any) => { working[k] = v; } } };
44
+ const tool = createTodoTool(fakeAgent);
45
+ const out = String(await tool.handler!({ items: '[{"text":"步骤一","status":"active"}]' }));
46
+ expect(out).toContain("✓ 任务清单 0/1");
47
+ expect(working[TODO_WORKING_KEY]).toEqual([{ text: "步骤一", status: "active" }]);
48
+ expect(String(await tool.handler!({ items: "bad" }))).toContain("✗");
49
+ });
50
+ });
51
+
52
+ describe("工具结果上下文保护", () => {
53
+ it("passes small results through untouched", () => {
54
+ expect(clampToolResult("short", 100)).toBe("short");
55
+ });
56
+
57
+ it("clamps oversized results keeping head + tail with a hint", () => {
58
+ const big = "H".repeat(9000) + "M".repeat(9000) + "T".repeat(9000);
59
+ const out = clampToolResult(big, 12000);
60
+ expect(out.length).toBeLessThan(13000);
61
+ expect(out.startsWith("HHHH")).toBe(true);
62
+ expect(out.endsWith("TTTT")).toBe(true);
63
+ expect(out).toContain("中间省略");
64
+ expect(out).toContain("offset");
65
+ });
66
+ });
67
+
68
+ describe("ToolRegistry 运行时统计", () => {
69
+ it("tracks calls / failures / cache hits / avg duration", async () => {
70
+ const reg = new ToolRegistry();
71
+ reg.register({
72
+ name: "ok_tool", description: "test tool", parameters: [], cacheable: true,
73
+ handler: async () => "fine",
74
+ });
75
+ reg.register({
76
+ name: "bad_tool", description: "failing test tool", parameters: [], maxRetries: 0,
77
+ handler: async () => { throw new Error("boom"); },
78
+ });
79
+
80
+ await reg.execute("ok_tool", { q: 1 });
81
+ await reg.execute("ok_tool", { q: 1 }); // cache hit
82
+ await reg.execute("bad_tool", {});
83
+
84
+ const stats = reg.getStats();
85
+ const ok = stats.find(s => s.name === "ok_tool")!;
86
+ expect(ok.calls).toBe(1);
87
+ expect(ok.cacheHits).toBe(1);
88
+ expect(ok.failures).toBe(0);
89
+ const bad = stats.find(s => s.name === "bad_tool")!;
90
+ expect(bad.calls).toBe(1);
91
+ expect(bad.failures).toBe(1);
92
+ expect(bad.breaker).toBeDefined();
93
+ });
94
+ });
@@ -1,108 +1,108 @@
1
- /**
2
- * Tests for tool system.
3
- */
4
- import { describe, it, expect, vi } from 'vitest';
5
- import { ToolRegistry, type ToolDefinition } from '../src/core/tool';
6
-
7
- function makeTool(overrides: Partial<ToolDefinition> & { name: string }): ToolDefinition {
8
- return {
9
- name: overrides.name,
10
- description: overrides.description ?? 'Test tool',
11
- parameters: overrides.parameters ?? [],
12
- handler: overrides.handler ?? vi.fn().mockResolvedValue('ok'),
13
- dangerous: overrides.dangerous,
14
- cacheable: overrides.cacheable,
15
- maxRetries: overrides.maxRetries,
16
- retryDelay: overrides.retryDelay,
17
- timeout: overrides.timeout,
18
- };
19
- }
20
-
21
- describe('ToolRegistry', () => {
22
- let registry: ToolRegistry;
23
-
24
- beforeEach(() => {
25
- registry = new ToolRegistry();
26
- });
27
-
28
- it('register and get tool', () => {
29
- const tool = makeTool({ name: 'test_tool' });
30
- registry.register(tool);
31
- expect(registry.get('test_tool')).toBe(tool);
32
- expect(registry.get('nonexistent')).toBeUndefined();
33
- });
34
-
35
- it('list returns all tools', () => {
36
- registry.register(makeTool({ name: 'a', description: 'A' }));
37
- registry.register(makeTool({ name: 'b', description: 'B' }));
38
- expect(registry.list()).toHaveLength(2);
39
- });
40
-
41
- it('listNames returns all names', () => {
42
- registry.register(makeTool({ name: 'alpha', description: 'Alpha' }));
43
- registry.register(makeTool({ name: 'beta', description: 'Beta' }));
44
- const names = registry.listNames();
45
- expect(names).toContain('alpha');
46
- expect(names).toContain('beta');
47
- });
48
-
49
- it('getTools returns all tools', () => {
50
- registry.register(makeTool({ name: 'x', description: 'X' }));
51
- expect(registry.getTools()).toHaveLength(1);
52
- });
53
-
54
- it('reregister overrides existing', () => {
55
- const t1 = makeTool({ name: 't', description: 'v1' });
56
- const t2 = makeTool({ name: 't', description: 'v2' });
57
- registry.register(t1);
58
- registry.register(t2);
59
- expect(registry.get('t')?.description).toBe('v2');
60
- });
61
-
62
- it('unregister removes tool', () => {
63
- registry.register(makeTool({ name: 'temp', description: 'Temp' }));
64
- expect(registry.get('temp')).toBeDefined();
65
- registry.unregister('temp');
66
- expect(registry.get('temp')).toBeUndefined();
67
- });
68
-
69
- it('has checks existence', () => {
70
- registry.register(makeTool({ name: 'exists' }));
71
- expect(registry.has('exists')).toBe(true);
72
- expect(registry.has('missing')).toBe(false);
73
- });
74
-
75
- it('merge copies tools', () => {
76
- const r2 = new ToolRegistry();
77
- r2.register(makeTool({ name: 't2', description: 'T2' }));
78
- registry.merge(r2);
79
- expect(registry.get('t2')).toBeDefined();
80
- });
81
-
82
- it('execute returns result from handler', async () => {
83
- const handler = vi.fn().mockResolvedValue('hello world');
84
- registry.register(makeTool({ name: 'greet', handler }));
85
- const result = await registry.execute('greet', { name: 'world' });
86
- expect(result.success).toBe(true);
87
- expect(result.result).toBe('hello world');
88
- });
89
-
90
- it('execute returns error for unknown tool', async () => {
91
- const result = await registry.execute('unknown', {});
92
- expect(result.success).toBe(false);
93
- expect(result.error).toContain('not found');
94
- });
95
-
96
- it('execute validates required parameters', async () => {
97
- const handler = vi.fn().mockResolvedValue('ok');
98
- registry.register(makeTool({
99
- name: 'needs_path',
100
- parameters: [{ name: 'path', type: 'string', description: 'File path', required: true }],
101
- handler,
102
- }));
103
- const result = await registry.execute('needs_path', {});
104
- expect(result.success).toBe(false);
105
- expect(result.error).toContain('required');
106
- expect(handler).not.toHaveBeenCalled();
107
- });
108
- });
1
+ /**
2
+ * Tests for tool system.
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { ToolRegistry, type ToolDefinition } from '../src/core/tool';
6
+
7
+ function makeTool(overrides: Partial<ToolDefinition> & { name: string }): ToolDefinition {
8
+ return {
9
+ name: overrides.name,
10
+ description: overrides.description ?? 'Test tool',
11
+ parameters: overrides.parameters ?? [],
12
+ handler: overrides.handler ?? vi.fn().mockResolvedValue('ok'),
13
+ dangerous: overrides.dangerous,
14
+ cacheable: overrides.cacheable,
15
+ maxRetries: overrides.maxRetries,
16
+ retryDelay: overrides.retryDelay,
17
+ timeout: overrides.timeout,
18
+ };
19
+ }
20
+
21
+ describe('ToolRegistry', () => {
22
+ let registry: ToolRegistry;
23
+
24
+ beforeEach(() => {
25
+ registry = new ToolRegistry();
26
+ });
27
+
28
+ it('register and get tool', () => {
29
+ const tool = makeTool({ name: 'test_tool' });
30
+ registry.register(tool);
31
+ expect(registry.get('test_tool')).toBe(tool);
32
+ expect(registry.get('nonexistent')).toBeUndefined();
33
+ });
34
+
35
+ it('list returns all tools', () => {
36
+ registry.register(makeTool({ name: 'a', description: 'A' }));
37
+ registry.register(makeTool({ name: 'b', description: 'B' }));
38
+ expect(registry.list()).toHaveLength(2);
39
+ });
40
+
41
+ it('listNames returns all names', () => {
42
+ registry.register(makeTool({ name: 'alpha', description: 'Alpha' }));
43
+ registry.register(makeTool({ name: 'beta', description: 'Beta' }));
44
+ const names = registry.listNames();
45
+ expect(names).toContain('alpha');
46
+ expect(names).toContain('beta');
47
+ });
48
+
49
+ it('getTools returns all tools', () => {
50
+ registry.register(makeTool({ name: 'x', description: 'X' }));
51
+ expect(registry.getTools()).toHaveLength(1);
52
+ });
53
+
54
+ it('reregister overrides existing', () => {
55
+ const t1 = makeTool({ name: 't', description: 'v1' });
56
+ const t2 = makeTool({ name: 't', description: 'v2' });
57
+ registry.register(t1);
58
+ registry.register(t2);
59
+ expect(registry.get('t')?.description).toBe('v2');
60
+ });
61
+
62
+ it('unregister removes tool', () => {
63
+ registry.register(makeTool({ name: 'temp', description: 'Temp' }));
64
+ expect(registry.get('temp')).toBeDefined();
65
+ registry.unregister('temp');
66
+ expect(registry.get('temp')).toBeUndefined();
67
+ });
68
+
69
+ it('has checks existence', () => {
70
+ registry.register(makeTool({ name: 'exists' }));
71
+ expect(registry.has('exists')).toBe(true);
72
+ expect(registry.has('missing')).toBe(false);
73
+ });
74
+
75
+ it('merge copies tools', () => {
76
+ const r2 = new ToolRegistry();
77
+ r2.register(makeTool({ name: 't2', description: 'T2' }));
78
+ registry.merge(r2);
79
+ expect(registry.get('t2')).toBeDefined();
80
+ });
81
+
82
+ it('execute returns result from handler', async () => {
83
+ const handler = vi.fn().mockResolvedValue('hello world');
84
+ registry.register(makeTool({ name: 'greet', handler }));
85
+ const result = await registry.execute('greet', { name: 'world' });
86
+ expect(result.success).toBe(true);
87
+ expect(result.result).toBe('hello world');
88
+ });
89
+
90
+ it('execute returns error for unknown tool', async () => {
91
+ const result = await registry.execute('unknown', {});
92
+ expect(result.success).toBe(false);
93
+ expect(result.error).toContain('not found');
94
+ });
95
+
96
+ it('execute validates required parameters', async () => {
97
+ const handler = vi.fn().mockResolvedValue('ok');
98
+ registry.register(makeTool({
99
+ name: 'needs_path',
100
+ parameters: [{ name: 'path', type: 'string', description: 'File path', required: true }],
101
+ handler,
102
+ }));
103
+ const result = await registry.execute('needs_path', {});
104
+ expect(result.success).toBe(false);
105
+ expect(result.error).toContain('required');
106
+ expect(handler).not.toHaveBeenCalled();
107
+ });
108
+ });