skyloom 1.13.5 → 1.13.7
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.
- package/.github/workflows/ci.yml +36 -36
- package/README.md +220 -159
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/commands_md.d.ts +41 -0
- package/dist/cli/commands_md.d.ts.map +1 -0
- package/dist/cli/commands_md.js +140 -0
- package/dist/cli/commands_md.js.map +1 -0
- package/dist/cli/input_macros.d.ts +28 -0
- package/dist/cli/input_macros.d.ts.map +1 -0
- package/dist/cli/input_macros.js +120 -0
- package/dist/cli/input_macros.js.map +1 -0
- package/dist/cli/loom.d.ts +220 -0
- package/dist/cli/loom.d.ts.map +1 -0
- package/dist/cli/loom.js +1094 -0
- package/dist/cli/loom.js.map +1 -0
- package/dist/cli/loom_chat.d.ts +20 -0
- package/dist/cli/loom_chat.d.ts.map +1 -0
- package/dist/cli/loom_chat.js +685 -0
- package/dist/cli/loom_chat.js.map +1 -0
- package/dist/cli/main.js +310 -14
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +7 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/guard.d.ts +45 -0
- package/dist/core/agent/guard.d.ts.map +1 -0
- package/dist/core/agent/guard.js +113 -0
- package/dist/core/agent/guard.js.map +1 -0
- package/dist/core/agent.d.ts +17 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +182 -93
- package/dist/core/agent.js.map +1 -1
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +34 -2
- package/dist/core/factory.js.map +1 -1
- package/dist/core/file_checkpoint.d.ts +57 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -0
- package/dist/core/file_checkpoint.js +162 -0
- package/dist/core/file_checkpoint.js.map +1 -0
- package/dist/core/hooks.d.ts +43 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +110 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +15 -9
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/mcp.d.ts +16 -0
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +55 -0
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/model_config.d.ts +40 -0
- package/dist/core/model_config.d.ts.map +1 -0
- package/dist/core/model_config.js +191 -0
- package/dist/core/model_config.js.map +1 -0
- package/dist/core/skill.d.ts +7 -0
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +47 -0
- package/dist/core/skill.js.map +1 -1
- package/dist/core/skymd.d.ts +39 -0
- package/dist/core/skymd.d.ts.map +1 -0
- package/dist/core/skymd.js +177 -0
- package/dist/core/skymd.js.map +1 -0
- package/dist/core/tool.d.ts +12 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +30 -0
- package/dist/core/tool.js.map +1 -1
- package/dist/core/verify.d.ts +27 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +62 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/skills/loader.d.ts +22 -2
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +45 -15
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +13 -3
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/model_tool.d.ts +11 -0
- package/dist/tools/model_tool.d.ts.map +1 -0
- package/dist/tools/model_tool.js +71 -0
- package/dist/tools/model_tool.js.map +1 -0
- package/dist/tools/todo.d.ts +30 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +78 -0
- package/dist/tools/todo.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -144
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +1 -1
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/commands_md.ts +112 -0
- package/src/cli/input_macros.ts +83 -0
- package/src/cli/loom.ts +982 -0
- package/src/cli/loom_chat.ts +598 -0
- package/src/cli/main.ts +255 -9
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +228 -222
- package/src/core/agent/guard.ts +134 -0
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +177 -95
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -178
- package/src/core/checkpoint.ts +94 -94
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +31 -2
- package/src/core/file_checkpoint.ts +136 -0
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/hooks.ts +126 -0
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +15 -9
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp.ts +48 -0
- package/src/core/mcp_server.ts +176 -176
- package/src/core/model_config.ts +157 -0
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +42 -0
- package/src/core/skymd.ts +143 -0
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +30 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/verify.ts +71 -0
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +45 -16
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +13 -3
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/tools/model_tool.ts +74 -0
- package/src/tools/todo.ts +76 -0
- package/src/web/tts.ts +93 -93
- package/tests/agent.test.ts +159 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -0
- package/tests/claude_compat.test.ts +110 -0
- package/tests/config.test.ts +41 -41
- package/tests/guard.test.ts +75 -0
- package/tests/icons.test.ts +45 -45
- package/tests/loom.test.ts +248 -0
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/skymd.test.ts +146 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -67
- package/vitest.config.ts +17 -17
|
@@ -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
|
+
});
|
package/tests/task.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/tests/tool.test.ts
CHANGED
|
@@ -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
|
+
});
|