patchwarden 0.6.4 → 1.5.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.
- package/README.en.md +54 -20
- package/README.md +48 -21
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.js +212 -3
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.js +32 -1
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +130 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -0
- package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +176 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/evidencePack.d.ts +39 -0
- package/dist/tools/evidencePack.js +168 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +926 -2
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +262 -0
- package/dist/tools/safeViews.js +252 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +39 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +834 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/package.json +3 -2
- package/scripts/checks/control-center-smoke.js +87 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/doctor-smoke.js +1 -0
- package/scripts/checks/lifecycle-smoke.js +13 -9
- package/scripts/checks/mcp-manifest-check.js +65 -6
- package/scripts/checks/mcp-smoke.js +60 -9
- package/scripts/checks/watcher-supervisor-smoke.js +2 -2
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +215 -4
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +36 -1
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/policy/projectPolicy.ts +344 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/evidence-pack.test.ts +142 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +260 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/evidencePack.ts +205 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +1093 -73
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +273 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +40 -11
- package/src/tools/toolRegistry.ts +896 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
- package/ui/pages/dashboard.html +143 -2
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, existsSync, readdirSync, readFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { generateGoalId, getGoalsDir, getGoalDir, createGoal, listGoals, readGoal, writeGoalStatus, readGoalStatus, } from "../../goal/goalStore.js";
|
|
7
|
+
import { addSubgoal, updateSubgoalStatus } from "../../goal/goalStatus.js";
|
|
8
|
+
import { PatchWardenError } from "../../errors.js";
|
|
9
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
10
|
+
let tempDir;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-goalstore-"));
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
function todayDatePart() {
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const y = now.getFullYear();
|
|
20
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
21
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
22
|
+
return `${y}${m}${d}`;
|
|
23
|
+
}
|
|
24
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
25
|
+
describe("goalStore", () => {
|
|
26
|
+
describe("generateGoalId", () => {
|
|
27
|
+
it("正常 title 生成 goal_{date}_{slug}", () => {
|
|
28
|
+
const id = generateGoalId("Implement Feature X", []);
|
|
29
|
+
assert.equal(id, `goal_${todayDatePart()}_implement_feature_x`);
|
|
30
|
+
});
|
|
31
|
+
it("特殊字符 title 转换为 slug", () => {
|
|
32
|
+
const id = generateGoalId("Fix: Bug #123 & Refactor!", []);
|
|
33
|
+
assert.equal(id, `goal_${todayDatePart()}_fix_bug_123_refactor`);
|
|
34
|
+
});
|
|
35
|
+
it("空 title 用 untitled 代替", () => {
|
|
36
|
+
const id = generateGoalId("", []);
|
|
37
|
+
assert.equal(id, `goal_${todayDatePart()}_untitled`);
|
|
38
|
+
});
|
|
39
|
+
it("纯符号 title 用 untitled 代替", () => {
|
|
40
|
+
const id = generateGoalId("!!!@#$%^&*()", []);
|
|
41
|
+
assert.equal(id, `goal_${todayDatePart()}_untitled`);
|
|
42
|
+
});
|
|
43
|
+
it("title 截断到 30 字符 slug", () => {
|
|
44
|
+
const longTitle = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
45
|
+
const id = generateGoalId(longTitle, []);
|
|
46
|
+
const slug = id.split("_").slice(2).join("_");
|
|
47
|
+
assert.ok(slug.length <= 30, `slug length should be <= 30, got ${slug.length}`);
|
|
48
|
+
});
|
|
49
|
+
it("冲突时追加 _2", () => {
|
|
50
|
+
const base = `goal_${todayDatePart()}_implement_feature_x`;
|
|
51
|
+
const id = generateGoalId("Implement Feature X", [base]);
|
|
52
|
+
assert.equal(id, `${base}_2`);
|
|
53
|
+
});
|
|
54
|
+
it("多次冲突递增 _3", () => {
|
|
55
|
+
const base = `goal_${todayDatePart()}_implement_feature_x`;
|
|
56
|
+
const id = generateGoalId("Implement Feature X", [base, `${base}_2`]);
|
|
57
|
+
assert.equal(id, `${base}_3`);
|
|
58
|
+
});
|
|
59
|
+
it("不冲突时不追加后缀", () => {
|
|
60
|
+
const base = `goal_${todayDatePart()}_implement_feature_x`;
|
|
61
|
+
const other = `goal_${todayDatePart()}_other_goal`;
|
|
62
|
+
const id = generateGoalId("Implement Feature X", [other]);
|
|
63
|
+
assert.equal(id, base);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("getGoalsDir / getGoalDir", () => {
|
|
67
|
+
it("getGoalsDir 返回 .patchwarden/goals 路径", () => {
|
|
68
|
+
const dir = getGoalsDir(tempDir);
|
|
69
|
+
assert.equal(dir, join(tempDir, ".patchwarden", "goals"));
|
|
70
|
+
});
|
|
71
|
+
it("getGoalDir 返回 {goalsDir}/{goalId}", () => {
|
|
72
|
+
const dir = getGoalDir("goal_001", tempDir);
|
|
73
|
+
assert.equal(dir, join(tempDir, ".patchwarden", "goals", "goal_001"));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("createGoal", () => {
|
|
77
|
+
it("成功创建目录结构和文件", () => {
|
|
78
|
+
const repoRel = "my-repo";
|
|
79
|
+
const { goal_id, goal_dir } = createGoal(repoRel, "Test Goal", "A test description", tempDir);
|
|
80
|
+
assert.ok(goal_id.startsWith(`goal_${todayDatePart()}_test_goal`));
|
|
81
|
+
assert.equal(goal_dir, join(tempDir, ".patchwarden", "goals", goal_id));
|
|
82
|
+
// 验证目录结构
|
|
83
|
+
assert.ok(existsSync(goal_dir), "goal_dir exists");
|
|
84
|
+
assert.ok(existsSync(join(goal_dir, "tasks")), "tasks/ exists");
|
|
85
|
+
assert.ok(existsSync(join(goal_dir, "artifacts")), "artifacts/ exists");
|
|
86
|
+
// 验证 GOAL.md
|
|
87
|
+
const goalMd = readFileSync(join(goal_dir, "GOAL.md"), "utf-8");
|
|
88
|
+
assert.ok(goalMd.includes("# Test Goal"));
|
|
89
|
+
assert.ok(goalMd.includes("A test description"));
|
|
90
|
+
assert.ok(goalMd.includes("- Status: active"));
|
|
91
|
+
assert.ok(goalMd.includes("- Repo:"));
|
|
92
|
+
// 验证 GOALS.md
|
|
93
|
+
const goalsMd = readFileSync(join(goal_dir, "GOALS.md"), "utf-8");
|
|
94
|
+
assert.ok(goalsMd.includes("# Subgoals: Test Goal"));
|
|
95
|
+
assert.ok(goalsMd.includes("_No subgoals yet._"));
|
|
96
|
+
// 验证 goal_status.json
|
|
97
|
+
const statusPath = join(goal_dir, "goal_status.json");
|
|
98
|
+
assert.ok(existsSync(statusPath));
|
|
99
|
+
const status = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
100
|
+
assert.equal(status.goal_id, goal_id);
|
|
101
|
+
assert.equal(status.title, "Test Goal");
|
|
102
|
+
assert.equal(status.status, "active");
|
|
103
|
+
assert.deepEqual(status.subgoals, []);
|
|
104
|
+
});
|
|
105
|
+
it("repo_path 越界抛出 workspace_path_escape", () => {
|
|
106
|
+
const outside = process.platform === "win32" ? "C:\\Windows\\System32" : "/etc";
|
|
107
|
+
assert.throws(() => createGoal(outside, "Bad", "desc", tempDir), (err) => {
|
|
108
|
+
assert.ok(err instanceof PatchWardenError);
|
|
109
|
+
assert.equal(err.reason, "workspace_path_escape");
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
it("相同 title 创建多个 goal 时 id 不冲突", () => {
|
|
114
|
+
const r1 = createGoal("repo", "Same Title", "d1", tempDir);
|
|
115
|
+
const r2 = createGoal("repo", "Same Title", "d2", tempDir);
|
|
116
|
+
assert.notEqual(r1.goal_id, r2.goal_id);
|
|
117
|
+
assert.ok(r2.goal_id.endsWith("_2"));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe("listGoals", () => {
|
|
121
|
+
it("空目录返回空数组", () => {
|
|
122
|
+
const list = listGoals(tempDir);
|
|
123
|
+
assert.deepEqual(list, []);
|
|
124
|
+
});
|
|
125
|
+
it("goals 目录不存在返回空数组", () => {
|
|
126
|
+
// tempDir 中没有 .patchwarden/goals
|
|
127
|
+
const list = listGoals(tempDir);
|
|
128
|
+
assert.deepEqual(list, []);
|
|
129
|
+
});
|
|
130
|
+
it("返回多个 goal 的摘要", () => {
|
|
131
|
+
createGoal("repo", "Goal A", "desc A", tempDir);
|
|
132
|
+
createGoal("repo", "Goal B", "desc B", tempDir);
|
|
133
|
+
const list = listGoals(tempDir);
|
|
134
|
+
assert.equal(list.length, 2);
|
|
135
|
+
const titles = list.map((g) => g.title).sort();
|
|
136
|
+
assert.deepEqual(titles, ["Goal A", "Goal B"]);
|
|
137
|
+
});
|
|
138
|
+
it("按 updated_at 降序排列", async () => {
|
|
139
|
+
const r1 = createGoal("repo", "Older", "desc", tempDir);
|
|
140
|
+
// 等待一小段时间确保 updated_at 不同
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
142
|
+
const r2 = createGoal("repo", "Newer", "desc", tempDir);
|
|
143
|
+
// 修改 older 的 status,使其 updated_at 更新
|
|
144
|
+
const olderStatus = readGoalStatus(r1.goal_id, tempDir);
|
|
145
|
+
const newerStatus = readGoalStatus(r2.goal_id, tempDir);
|
|
146
|
+
// 让 older 的 updated_at 比 newer 更晚
|
|
147
|
+
const future = new Date(Date.now() + 10000).toISOString();
|
|
148
|
+
writeGoalStatus(r1.goal_id, { ...olderStatus, updated_at: future }, tempDir);
|
|
149
|
+
const list = listGoals(tempDir);
|
|
150
|
+
assert.equal(list.length, 2);
|
|
151
|
+
assert.equal(list[0].goal_id, r1.goal_id);
|
|
152
|
+
assert.equal(list[1].goal_id, r2.goal_id);
|
|
153
|
+
assert.ok(list[0].updated_at >= list[1].updated_at);
|
|
154
|
+
});
|
|
155
|
+
it("subgoal 摘要正确计算", () => {
|
|
156
|
+
const { goal_id } = createGoal("repo", "With Subs", "desc", tempDir);
|
|
157
|
+
let status = readGoalStatus(goal_id, tempDir);
|
|
158
|
+
status = addSubgoal(status, "S1").goalStatus;
|
|
159
|
+
status = updateSubgoalStatus(status, "subgoal-001", "running");
|
|
160
|
+
status = addSubgoal(status, "S2").goalStatus;
|
|
161
|
+
status = updateSubgoalStatus(status, "subgoal-002", "running");
|
|
162
|
+
status = updateSubgoalStatus(status, "subgoal-002", "done_by_agent");
|
|
163
|
+
status = updateSubgoalStatus(status, "subgoal-002", "accepted");
|
|
164
|
+
writeGoalStatus(goal_id, status, tempDir);
|
|
165
|
+
const list = listGoals(tempDir);
|
|
166
|
+
assert.equal(list.length, 1);
|
|
167
|
+
const item = list[0];
|
|
168
|
+
assert.equal(item.subgoal_total, 2);
|
|
169
|
+
assert.equal(item.subgoal_accepted, 1);
|
|
170
|
+
assert.equal(item.subgoal_running, 1);
|
|
171
|
+
});
|
|
172
|
+
it("跳过无法解析的目录", () => {
|
|
173
|
+
// 创建一个有效的 goal
|
|
174
|
+
createGoal("repo", "Valid", "desc", tempDir);
|
|
175
|
+
// 创建一个无效的目录(没有 goal_status.json)
|
|
176
|
+
const goalsDir = getGoalsDir(tempDir);
|
|
177
|
+
const badDir = join(goalsDir, "goal_bad_9999_invalid");
|
|
178
|
+
mkdirSync(badDir, { recursive: true });
|
|
179
|
+
const list = listGoals(tempDir);
|
|
180
|
+
assert.equal(list.length, 1);
|
|
181
|
+
assert.equal(list[0].title, "Valid");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe("readGoal", () => {
|
|
185
|
+
it("返回完整详情", () => {
|
|
186
|
+
const { goal_id } = createGoal("repo", "Read Me", "description here", tempDir);
|
|
187
|
+
const detail = readGoal(goal_id, tempDir);
|
|
188
|
+
assert.equal(detail.goal_id, goal_id);
|
|
189
|
+
assert.equal(detail.title, "Read Me");
|
|
190
|
+
assert.equal(detail.status, "active");
|
|
191
|
+
assert.equal(detail.created_at, detail.updated_at);
|
|
192
|
+
assert.ok(detail.goal_description.includes("# Read Me"));
|
|
193
|
+
assert.ok(detail.goal_description.includes("description here"));
|
|
194
|
+
assert.deepEqual(detail.subgoals, []);
|
|
195
|
+
});
|
|
196
|
+
it("goal 不存在抛出 goal_not_found", () => {
|
|
197
|
+
assert.throws(() => readGoal("goal_nonexistent", tempDir), (err) => {
|
|
198
|
+
assert.ok(err instanceof PatchWardenError);
|
|
199
|
+
assert.equal(err.reason, "goal_not_found");
|
|
200
|
+
return true;
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
it("goal_status.json 不存在抛出 goal_not_found", () => {
|
|
204
|
+
// 手动创建一个 goal 目录但不写 status
|
|
205
|
+
const goalsDir = getGoalsDir(tempDir);
|
|
206
|
+
const emptyGoalDir = join(goalsDir, "goal_empty");
|
|
207
|
+
mkdirSync(emptyGoalDir, { recursive: true });
|
|
208
|
+
assert.throws(() => readGoal("goal_empty", tempDir), (err) => {
|
|
209
|
+
assert.ok(err instanceof PatchWardenError);
|
|
210
|
+
assert.equal(err.reason, "goal_not_found");
|
|
211
|
+
return true;
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe("writeGoalStatus + readGoalStatus", () => {
|
|
216
|
+
it("写入后读取一致", () => {
|
|
217
|
+
const { goal_id } = createGoal("repo", "Round Trip", "desc", tempDir);
|
|
218
|
+
const original = readGoalStatus(goal_id, tempDir);
|
|
219
|
+
const withSub = addSubgoal(original, "New Sub").goalStatus;
|
|
220
|
+
writeGoalStatus(goal_id, withSub, tempDir);
|
|
221
|
+
const read = readGoalStatus(goal_id, tempDir);
|
|
222
|
+
assert.equal(read.subgoals.length, 1);
|
|
223
|
+
assert.equal(read.subgoals[0].id, "subgoal-001");
|
|
224
|
+
assert.equal(read.subgoals[0].title, "New Sub");
|
|
225
|
+
assert.equal(read.subgoals[0].status, "ready");
|
|
226
|
+
});
|
|
227
|
+
it("原子写:tmp 文件不留", () => {
|
|
228
|
+
const { goal_id, goal_dir } = createGoal("repo", "Atomic", "desc", tempDir);
|
|
229
|
+
const status = readGoalStatus(goal_id, tempDir);
|
|
230
|
+
writeGoalStatus(goal_id, status, tempDir);
|
|
231
|
+
const entries = readdirSync(goal_dir);
|
|
232
|
+
assert.ok(!entries.includes("goal_status.json.tmp"), "tmp file should not remain");
|
|
233
|
+
assert.ok(entries.includes("goal_status.json"), "final file should exist");
|
|
234
|
+
});
|
|
235
|
+
it("readGoalStatus 文件不存在抛出 goal_not_found", () => {
|
|
236
|
+
assert.throws(() => readGoalStatus("goal_nonexistent", tempDir), (err) => {
|
|
237
|
+
assert.ok(err instanceof PatchWardenError);
|
|
238
|
+
assert.equal(err.reason, "goal_not_found");
|
|
239
|
+
return true;
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
it("JSON 格式化(2 空格缩进 + 末尾换行)", () => {
|
|
243
|
+
const { goal_id, goal_dir } = createGoal("repo", "Format", "desc", tempDir);
|
|
244
|
+
const status = readGoalStatus(goal_id, tempDir);
|
|
245
|
+
writeGoalStatus(goal_id, status, tempDir);
|
|
246
|
+
const raw = readFileSync(join(goal_dir, "goal_status.json"), "utf-8");
|
|
247
|
+
// 末尾换行
|
|
248
|
+
assert.ok(raw.endsWith("\n"), "should end with newline");
|
|
249
|
+
// 2 空格缩进
|
|
250
|
+
assert.ok(raw.includes('\n "goal_id"'), "should use 2-space indent");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { createSubgoalTask } from "../../tools/goalSubgoalTask.js";
|
|
4
|
+
import { PatchWardenError } from "../../errors.js";
|
|
5
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// 说明:createSubgoalTask 内部调用 createTask,后者使用 getConfig()(无 workspaceRoot
|
|
8
|
+
// 覆盖),因此 happy-path 依赖真实配置的工作区,留待集成测试覆盖。
|
|
9
|
+
// 此处仅测试不触及 createTask 完整流程的错误路径:
|
|
10
|
+
// - invalid_execution_mode:在读取 goal 之前即抛错
|
|
11
|
+
// - goal_not_found:readGoalStatus 在默认工作区找不到 goal 即抛错
|
|
12
|
+
describe("createSubgoalTask", () => {
|
|
13
|
+
describe("错误路径", () => {
|
|
14
|
+
it("execution_mode=assess_only 抛 invalid_execution_mode", () => {
|
|
15
|
+
assert.throws(() => createSubgoalTask({
|
|
16
|
+
goal_id: "goal_nonexistent_test",
|
|
17
|
+
subgoal_title: "Sub A",
|
|
18
|
+
repo_path: "repo",
|
|
19
|
+
execution_mode: "assess_only",
|
|
20
|
+
}), (err) => {
|
|
21
|
+
assert.ok(err instanceof PatchWardenError);
|
|
22
|
+
assert.equal(err.reason, "invalid_execution_mode");
|
|
23
|
+
assert.equal(err.blocked, true);
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it("goal_id 不存在抛 goal_not_found", () => {
|
|
28
|
+
assert.throws(() => createSubgoalTask({
|
|
29
|
+
goal_id: "goal_definitely_does_not_exist_99999",
|
|
30
|
+
subgoal_title: "Sub A",
|
|
31
|
+
repo_path: "repo",
|
|
32
|
+
}), (err) => {
|
|
33
|
+
assert.ok(err instanceof PatchWardenError);
|
|
34
|
+
assert.equal(err.reason, "goal_not_found");
|
|
35
|
+
assert.equal(err.blocked, true);
|
|
36
|
+
assert.ok(err.details.goal_id === "goal_definitely_does_not_exist_99999");
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it("assess_only 优先于 goal_not_found(在读取 goal 前校验)", () => {
|
|
41
|
+
// execution_mode=assess_only 应先抛 invalid_execution_mode,
|
|
42
|
+
// 而不是去读不存在的 goal
|
|
43
|
+
assert.throws(() => createSubgoalTask({
|
|
44
|
+
goal_id: "goal_definitely_does_not_exist_99999",
|
|
45
|
+
subgoal_title: "Sub A",
|
|
46
|
+
repo_path: "repo",
|
|
47
|
+
execution_mode: "assess_only",
|
|
48
|
+
}), (err) => {
|
|
49
|
+
assert.ok(err instanceof PatchWardenError);
|
|
50
|
+
assert.equal(err.reason, "invalid_execution_mode");
|
|
51
|
+
return true;
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { getToolDefs, handleToolCall } from "../../tools/registry.js";
|
|
7
|
+
import { reloadConfig } from "../../config.js";
|
|
8
|
+
import { PatchWardenError } from "../../errors.js";
|
|
9
|
+
// ── 9 个 Goal Session MCP 工具名 ──────────────────────────────────
|
|
10
|
+
const GOAL_TOOL_NAMES = [
|
|
11
|
+
"create_goal",
|
|
12
|
+
"list_goals",
|
|
13
|
+
"read_goal",
|
|
14
|
+
"create_subgoal_task",
|
|
15
|
+
"accept_subgoal",
|
|
16
|
+
"reject_subgoal",
|
|
17
|
+
"suggest_next_subgoal",
|
|
18
|
+
"summarize_goal_progress",
|
|
19
|
+
"export_handoff",
|
|
20
|
+
];
|
|
21
|
+
// ── 测试环境隔离:使用临时 workspaceRoot + full profile ───────────
|
|
22
|
+
let tempDir;
|
|
23
|
+
let prevConfigEnv;
|
|
24
|
+
let prevProfileEnv;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-goal-registry-"));
|
|
27
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
28
|
+
writeFileSync(configPath, JSON.stringify({
|
|
29
|
+
workspaceRoot: tempDir,
|
|
30
|
+
toolProfile: "full",
|
|
31
|
+
agents: {},
|
|
32
|
+
allowedTestCommands: [],
|
|
33
|
+
}), "utf-8");
|
|
34
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
35
|
+
prevProfileEnv = process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
36
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
37
|
+
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
38
|
+
reloadConfig();
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
if (prevConfigEnv === undefined) {
|
|
42
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
46
|
+
}
|
|
47
|
+
if (prevProfileEnv === undefined) {
|
|
48
|
+
delete process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
process.env.PATCHWARDEN_TOOL_PROFILE = prevProfileEnv;
|
|
52
|
+
}
|
|
53
|
+
reloadConfig();
|
|
54
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
57
|
+
describe("goal tools registry", () => {
|
|
58
|
+
describe("getToolDefs — 工具定义", () => {
|
|
59
|
+
it("包含全部 9 个 goal 工具", () => {
|
|
60
|
+
const tools = getToolDefs();
|
|
61
|
+
const names = new Set(tools.map((t) => t.name));
|
|
62
|
+
for (const name of GOAL_TOOL_NAMES) {
|
|
63
|
+
assert.ok(names.has(name), `Missing tool definition: ${name}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
it("每个 goal 工具描述以 v0.8.0 开头且 inputSchema.type 为 object", () => {
|
|
67
|
+
const tools = getToolDefs();
|
|
68
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
69
|
+
for (const name of GOAL_TOOL_NAMES) {
|
|
70
|
+
const tool = byName.get(name);
|
|
71
|
+
assert.ok(tool, `Tool not found: ${name}`);
|
|
72
|
+
assert.equal(tool.inputSchema.type, "object", `${name} schema type should be object`);
|
|
73
|
+
assert.ok(tool.description.startsWith("v0.8.0:"), `${name} description should start with v0.8.0:`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
it("create_goal required 字段为 title/goal_description/repo_path", () => {
|
|
77
|
+
const tools = getToolDefs();
|
|
78
|
+
const tool = tools.find((t) => t.name === "create_goal");
|
|
79
|
+
assert.ok(tool);
|
|
80
|
+
assert.deepEqual(tool.inputSchema.required, ["title", "goal_description", "repo_path"]);
|
|
81
|
+
});
|
|
82
|
+
it("create_subgoal_task required 字段为 goal_id/subgoal_title/repo_path", () => {
|
|
83
|
+
const tools = getToolDefs();
|
|
84
|
+
const tool = tools.find((t) => t.name === "create_subgoal_task");
|
|
85
|
+
assert.ok(tool);
|
|
86
|
+
assert.deepEqual(tool.inputSchema.required, ["goal_id", "subgoal_title", "repo_path"]);
|
|
87
|
+
});
|
|
88
|
+
it("create_subgoal_task template enum 复用 TASK_TEMPLATE_NAMES", () => {
|
|
89
|
+
const tools = getToolDefs();
|
|
90
|
+
const tool = tools.find((t) => t.name === "create_subgoal_task");
|
|
91
|
+
assert.ok(tool);
|
|
92
|
+
const props = tool.inputSchema.properties;
|
|
93
|
+
assert.ok(Array.isArray(props.template.enum));
|
|
94
|
+
assert.ok(props.template.enum.length > 0);
|
|
95
|
+
});
|
|
96
|
+
it("accept_subgoal required 字段为 goal_id/subgoal_id", () => {
|
|
97
|
+
const tools = getToolDefs();
|
|
98
|
+
const tool = tools.find((t) => t.name === "accept_subgoal");
|
|
99
|
+
assert.ok(tool);
|
|
100
|
+
assert.deepEqual(tool.inputSchema.required, ["goal_id", "subgoal_id"]);
|
|
101
|
+
});
|
|
102
|
+
it("reject_subgoal required 字段为 goal_id/subgoal_id/reason", () => {
|
|
103
|
+
const tools = getToolDefs();
|
|
104
|
+
const tool = tools.find((t) => t.name === "reject_subgoal");
|
|
105
|
+
assert.ok(tool);
|
|
106
|
+
assert.deepEqual(tool.inputSchema.required, ["goal_id", "subgoal_id", "reason"]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe("handleToolCall — handler 分发", () => {
|
|
110
|
+
it("list_goals 返回 { goals: [] } 格式(空工作区)", async () => {
|
|
111
|
+
const result = await handleToolCall("list_goals", {});
|
|
112
|
+
assert.ok(Array.isArray(result.content));
|
|
113
|
+
assert.equal(result.content[0].type, "text");
|
|
114
|
+
const data = JSON.parse(result.content[0].text);
|
|
115
|
+
assert.ok(Array.isArray(data.goals));
|
|
116
|
+
assert.equal(data.goals.length, 0);
|
|
117
|
+
});
|
|
118
|
+
it("read_goal 不存在时抛出 goal_not_found", async () => {
|
|
119
|
+
await assert.rejects(() => handleToolCall("read_goal", { goal_id: "goal_nonexistent_xyz_99999" }), (err) => {
|
|
120
|
+
assert.ok(err instanceof PatchWardenError, "should be PatchWardenError");
|
|
121
|
+
assert.equal(err.reason, "goal_not_found");
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
it("create_goal 路径越界抛出 workspace_path_escape", async () => {
|
|
126
|
+
await assert.rejects(() => handleToolCall("create_goal", {
|
|
127
|
+
title: "Test Goal",
|
|
128
|
+
goal_description: "desc",
|
|
129
|
+
repo_path: "../../../../..",
|
|
130
|
+
}), (err) => {
|
|
131
|
+
assert.ok(err instanceof PatchWardenError, "should be PatchWardenError");
|
|
132
|
+
assert.equal(err.reason, "workspace_path_escape");
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
it("suggest_next_subgoal 不存在时抛出 goal_not_found", async () => {
|
|
137
|
+
await assert.rejects(() => handleToolCall("suggest_next_subgoal", { goal_id: "goal_nonexistent_xyz_99999" }), (err) => {
|
|
138
|
+
assert.ok(err instanceof PatchWardenError, "should be PatchWardenError");
|
|
139
|
+
assert.equal(err.reason, "goal_not_found");
|
|
140
|
+
return true;
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it("summarize_goal_progress 不存在时抛出 goal_not_found", async () => {
|
|
144
|
+
await assert.rejects(() => handleToolCall("summarize_goal_progress", { goal_id: "goal_nonexistent_xyz_99999" }), (err) => {
|
|
145
|
+
assert.ok(err instanceof PatchWardenError, "should be PatchWardenError");
|
|
146
|
+
assert.equal(err.reason, "goal_not_found");
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
it("export_handoff 不存在时抛出 goal_not_found", async () => {
|
|
151
|
+
await assert.rejects(() => handleToolCall("export_handoff", { goal_id: "goal_nonexistent_xyz_99999" }), (err) => {
|
|
152
|
+
assert.ok(err instanceof PatchWardenError, "should be PatchWardenError");
|
|
153
|
+
assert.equal(err.reason, "goal_not_found");
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
it("accept_subgoal 不存在时抛出 goal_not_found", async () => {
|
|
158
|
+
await assert.rejects(() => handleToolCall("accept_subgoal", {
|
|
159
|
+
goal_id: "goal_nonexistent_xyz_99999",
|
|
160
|
+
subgoal_id: "sg_1",
|
|
161
|
+
}), (err) => {
|
|
162
|
+
assert.ok(err instanceof PatchWardenError, "should be PatchWardenError");
|
|
163
|
+
assert.equal(err.reason, "goal_not_found");
|
|
164
|
+
return true;
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
it("reject_subgoal 不存在时抛出 goal_not_found", async () => {
|
|
168
|
+
await assert.rejects(() => handleToolCall("reject_subgoal", {
|
|
169
|
+
goal_id: "goal_nonexistent_xyz_99999",
|
|
170
|
+
subgoal_id: "sg_1",
|
|
171
|
+
reason: "test reason",
|
|
172
|
+
}), (err) => {
|
|
173
|
+
assert.ok(err instanceof PatchWardenError, "should be PatchWardenError");
|
|
174
|
+
assert.equal(err.reason, "goal_not_found");
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
it("create_subgoal_task 不存在时抛出 goal_not_found", async () => {
|
|
179
|
+
await assert.rejects(() => handleToolCall("create_subgoal_task", {
|
|
180
|
+
goal_id: "goal_nonexistent_xyz_99999",
|
|
181
|
+
subgoal_title: "Sub A",
|
|
182
|
+
repo_path: "repo",
|
|
183
|
+
}), (err) => {
|
|
184
|
+
assert.ok(err instanceof PatchWardenError, "should be PatchWardenError");
|
|
185
|
+
assert.equal(err.reason, "goal_not_found");
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|