patchwarden 0.6.1 → 1.1.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/PatchWarden-Control-Tray.cmd +11 -0
- package/PatchWarden-Control.cmd +6 -0
- package/PatchWarden-Desktop.cmd +5 -0
- package/PatchWarden.cmd +1 -1
- package/README.en.md +112 -24
- package/README.md +36 -25
- package/Restart-PatchWarden-Control.cmd +6 -0
- package/Stop-PatchWarden.cmd +11 -0
- 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.d.ts +14 -0
- package/dist/controlCenter.js +2015 -0
- package/dist/doctor.js +35 -4
- 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/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/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/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/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 +175 -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/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/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +550 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +256 -0
- package/dist/tools/safeViews.js +250 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +33 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +724 -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/control-center/README.md +33 -0
- package/docs/control-center/control-center-daily-driver.md +211 -0
- package/docs/control-center/control-center-mvp.md +205 -0
- package/docs/control-center/control-center-phase2.md +159 -0
- package/docs/demo.md +3 -0
- package/docs/release-v0.6.4.md +45 -0
- package/examples/openai-tunnel/README.md +5 -5
- package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
- package/package.json +25 -16
- package/scripts/README.md +47 -0
- package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
- package/scripts/checks/control-center-smoke.js +1098 -0
- package/scripts/{control-smoke.js → checks/control-smoke.js} +19 -5
- package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +2 -1
- package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
- package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +25 -21
- package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +54 -7
- package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +40 -11
- package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
- package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
- package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
- package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +13 -7
- package/scripts/control/control-center-tray.ps1 +281 -0
- package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
- package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
- package/scripts/control/restart-control-center.ps1 +173 -0
- package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
- package/scripts/control/start-control-center.ps1 +263 -0
- package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
- package/scripts/control/stop-patchwarden.ps1 +114 -0
- package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
- package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
- package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
- package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
- package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +2166 -0
- package/src/doctor.ts +40 -5
- 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/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/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/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +258 -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/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/mergeWorktree.ts +42 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +618 -1
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +271 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/toolCatalog.ts +34 -11
- package/src/tools/toolRegistry.ts +786 -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/colors_and_type.css +141 -0
- package/ui/pages/audit.html +743 -0
- package/ui/pages/dashboard.html +1154 -0
- package/ui/pages/direct-sessions.html +652 -0
- package/ui/pages/logs.html +502 -0
- package/ui/pages/task-detail.html +1229 -0
- package/ui/pages/tasks.html +702 -0
- package/ui/pages/workspace.html +947 -0
- package/ui/partials/project-shell.html +362 -0
- package/ui/vendor/lucide.js +12 -0
- package/ui/vendor/tailwindcss-browser.js +947 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { computeSchemaDigest } from "../../tools/toolRegistry.js";
|
|
4
|
+
import type {
|
|
5
|
+
PatchWardenToolMeta,
|
|
6
|
+
ToolMode,
|
|
7
|
+
ToolRisk,
|
|
8
|
+
} from "../../tools/toolRegistry.js";
|
|
9
|
+
import type { ToolProfile } from "../../tools/toolCatalog.js";
|
|
10
|
+
import { CHATGPT_CORE_TOOL_NAMES } from "../../tools/toolCatalog.js";
|
|
11
|
+
import {
|
|
12
|
+
checkRegistrySchemaDigest,
|
|
13
|
+
checkChatgptCoreManifestStable,
|
|
14
|
+
checkNewToolsProfileAppend,
|
|
15
|
+
runAllSchemaDriftChecks,
|
|
16
|
+
type ToolDefLike,
|
|
17
|
+
} from "../../tools/schemaDriftCheck.js";
|
|
18
|
+
|
|
19
|
+
// ── 测试夹具构造工具 ─────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const SAMPLE_SCHEMA = {
|
|
22
|
+
type: "object" as const,
|
|
23
|
+
properties: {
|
|
24
|
+
task_id: { type: "string", description: "Task ID" },
|
|
25
|
+
},
|
|
26
|
+
required: ["task_id"],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const OTHER_SCHEMA = {
|
|
30
|
+
type: "object" as const,
|
|
31
|
+
properties: {
|
|
32
|
+
plan_id: { type: "string", description: "Plan ID" },
|
|
33
|
+
},
|
|
34
|
+
required: ["plan_id"],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
interface MakeMetaOpts {
|
|
38
|
+
name: string;
|
|
39
|
+
inputSchema?: unknown;
|
|
40
|
+
inputSchemaDigest?: string;
|
|
41
|
+
profiles?: ToolProfile[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 构造一个满足 PatchWardenToolMeta 接口的元数据对象。 */
|
|
45
|
+
function makeMeta(opts: MakeMetaOpts): PatchWardenToolMeta {
|
|
46
|
+
const schema = opts.inputSchema ?? SAMPLE_SCHEMA;
|
|
47
|
+
return {
|
|
48
|
+
name: opts.name,
|
|
49
|
+
title: `Tool ${opts.name}`,
|
|
50
|
+
summary: `summary for ${opts.name}`,
|
|
51
|
+
description: `description for ${opts.name}`,
|
|
52
|
+
profiles: opts.profiles ?? ["full"],
|
|
53
|
+
modes: ["diagnostic"] as ToolMode[],
|
|
54
|
+
tags: [],
|
|
55
|
+
aliases: [],
|
|
56
|
+
risk: "readonly" as ToolRisk,
|
|
57
|
+
requiresConfirmation: false,
|
|
58
|
+
inputSchemaDigest: opts.inputSchemaDigest ?? computeSchemaDigest(schema),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 构造 toolDefs Map。 */
|
|
63
|
+
function makeToolDefs(
|
|
64
|
+
entries: Array<{ name: string; inputSchema?: unknown }>,
|
|
65
|
+
): Map<string, ToolDefLike> {
|
|
66
|
+
const map = new Map<string, ToolDefLike>();
|
|
67
|
+
for (const e of entries) {
|
|
68
|
+
map.set(e.name, {
|
|
69
|
+
inputSchema: e.inputSchema ?? SAMPLE_SCHEMA,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return map;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 构造包含全部 21 个 chatgpt_core 工具(顺序一致)的 toolDefs Map。 */
|
|
76
|
+
function makeCoreToolDefs(): Map<string, ToolDefLike> {
|
|
77
|
+
return makeToolDefs(
|
|
78
|
+
[...CHATGPT_CORE_TOOL_NAMES].map((name) => ({ name })),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 构造一个 registry:所有工具都属于 chatgpt_core profile。 */
|
|
83
|
+
function makeCoreRegistry(count: number): PatchWardenToolMeta[] {
|
|
84
|
+
const names = [...CHATGPT_CORE_TOOL_NAMES].slice(0, count);
|
|
85
|
+
return names.map((name) => makeMeta({ name, profiles: ["chatgpt_core"] }));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── 1. checkRegistrySchemaDigest ─────────────────────────────────
|
|
89
|
+
|
|
90
|
+
describe("checkRegistrySchemaDigest", () => {
|
|
91
|
+
it("digest 一致时 ok=true,warnings 为空", () => {
|
|
92
|
+
const schema = { type: "object", properties: { a: { type: "string" } } };
|
|
93
|
+
const registry = [makeMeta({ name: "tool_a", inputSchema: schema })];
|
|
94
|
+
const toolDefs = makeToolDefs([{ name: "tool_a", inputSchema: schema }]);
|
|
95
|
+
|
|
96
|
+
const result = checkRegistrySchemaDigest(registry, toolDefs);
|
|
97
|
+
|
|
98
|
+
assert.equal(result.ok, true);
|
|
99
|
+
assert.deepEqual(result.warnings, []);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("digest 不一致时 ok=false,warnings 包含工具名和两个 digest", () => {
|
|
103
|
+
const registrySchema = SAMPLE_SCHEMA;
|
|
104
|
+
const actualSchema = OTHER_SCHEMA;
|
|
105
|
+
// registry 记录的是 registrySchema 的 digest,但实际 toolDef 用的是 actualSchema
|
|
106
|
+
const registry = [
|
|
107
|
+
makeMeta({ name: "tool_drift", inputSchema: registrySchema }),
|
|
108
|
+
];
|
|
109
|
+
const toolDefs = makeToolDefs([{ name: "tool_drift", inputSchema: actualSchema }]);
|
|
110
|
+
|
|
111
|
+
const result = checkRegistrySchemaDigest(registry, toolDefs);
|
|
112
|
+
|
|
113
|
+
assert.equal(result.ok, false);
|
|
114
|
+
assert.equal(result.warnings.length, 1);
|
|
115
|
+
const warning = result.warnings[0];
|
|
116
|
+
const expectedRegistryDigest = computeSchemaDigest(registrySchema);
|
|
117
|
+
const expectedActualDigest = computeSchemaDigest(actualSchema);
|
|
118
|
+
assert.ok(warning.includes("tool_drift"), "warning 应包含工具名");
|
|
119
|
+
assert.ok(
|
|
120
|
+
warning.includes(expectedRegistryDigest),
|
|
121
|
+
"warning 应包含 registry digest",
|
|
122
|
+
);
|
|
123
|
+
assert.ok(
|
|
124
|
+
warning.includes(expectedActualDigest),
|
|
125
|
+
"warning 应包含 actual digest",
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("toolDefs 中找不到工具时跳过(不报 drift)", () => {
|
|
130
|
+
const registry = [makeMeta({ name: "missing_tool" })];
|
|
131
|
+
const toolDefs = makeToolDefs([]); // 空映射
|
|
132
|
+
|
|
133
|
+
const result = checkRegistrySchemaDigest(registry, toolDefs);
|
|
134
|
+
|
|
135
|
+
assert.equal(result.ok, true);
|
|
136
|
+
assert.deepEqual(result.warnings, []);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── 2. checkChatgptCoreManifestStable ────────────────────────────
|
|
141
|
+
|
|
142
|
+
describe("checkChatgptCoreManifestStable", () => {
|
|
143
|
+
it("21 工具全部存在时 ok=true", () => {
|
|
144
|
+
const toolDefs = makeCoreToolDefs();
|
|
145
|
+
|
|
146
|
+
const result = checkChatgptCoreManifestStable(toolDefs);
|
|
147
|
+
|
|
148
|
+
assert.equal(result.ok, true);
|
|
149
|
+
assert.deepEqual(result.warnings, []);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("缺少工具时 ok=false", () => {
|
|
153
|
+
// 移除最后一个工具,使 chatgpt_core 工具数为 20
|
|
154
|
+
const names = [...CHATGPT_CORE_TOOL_NAMES].slice(0, 20);
|
|
155
|
+
const toolDefs = makeToolDefs(names.map((name) => ({ name })));
|
|
156
|
+
|
|
157
|
+
const result = checkChatgptCoreManifestStable(toolDefs);
|
|
158
|
+
|
|
159
|
+
assert.equal(result.ok, false);
|
|
160
|
+
assert.equal(result.warnings.length, 1);
|
|
161
|
+
assert.ok(
|
|
162
|
+
result.warnings[0].includes("missing tools"),
|
|
163
|
+
"warning 应包含 missing tools",
|
|
164
|
+
);
|
|
165
|
+
assert.ok(
|
|
166
|
+
result.warnings[0].includes(CHATGPT_CORE_TOOL_NAMES[20]),
|
|
167
|
+
"warning 应包含缺失的工具名",
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── 3. checkNewToolsProfileAppend ────────────────────────────────
|
|
173
|
+
|
|
174
|
+
describe("checkNewToolsProfileAppend", () => {
|
|
175
|
+
it("chatgpt_core 21 工具时 ok=true", () => {
|
|
176
|
+
const registry = makeCoreRegistry(21);
|
|
177
|
+
|
|
178
|
+
const result = checkNewToolsProfileAppend(registry);
|
|
179
|
+
|
|
180
|
+
assert.equal(result.ok, true);
|
|
181
|
+
assert.deepEqual(result.warnings, []);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("chatgpt_core 工具数不为 21 时 ok=false", () => {
|
|
185
|
+
// 22 个 chatgpt_core 工具:21 个标准工具 + 1 个重复名(模拟新工具追加)
|
|
186
|
+
const registry = makeCoreRegistry(21);
|
|
187
|
+
registry.push(makeMeta({ name: "extra_tool", profiles: ["chatgpt_core"] }));
|
|
188
|
+
|
|
189
|
+
const result = checkNewToolsProfileAppend(registry);
|
|
190
|
+
|
|
191
|
+
assert.equal(result.ok, false);
|
|
192
|
+
assert.equal(result.warnings.length, 1);
|
|
193
|
+
assert.ok(
|
|
194
|
+
result.warnings[0].includes("expected 21"),
|
|
195
|
+
"warning 应包含预期数量 21",
|
|
196
|
+
);
|
|
197
|
+
assert.ok(
|
|
198
|
+
result.warnings[0].includes("got 22"),
|
|
199
|
+
"warning 应包含实际数量 22",
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ── 4. runAllSchemaDriftChecks ───────────────────────────────────
|
|
205
|
+
|
|
206
|
+
describe("runAllSchemaDriftChecks", () => {
|
|
207
|
+
it("无 drift 时 ok=true", () => {
|
|
208
|
+
// registry 与 toolDefs 完全一致,且 chatgpt_core 工具数为 21
|
|
209
|
+
const registry = makeCoreRegistry(21);
|
|
210
|
+
const toolDefs = makeCoreToolDefs();
|
|
211
|
+
|
|
212
|
+
const result = runAllSchemaDriftChecks(registry, toolDefs);
|
|
213
|
+
|
|
214
|
+
assert.equal(result.ok, true);
|
|
215
|
+
assert.deepEqual(result.warnings, []);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("有 drift 时 ok=false,warnings 合并正确", () => {
|
|
219
|
+
// 构造同时触发三个检查的 drift:
|
|
220
|
+
// - check 1: 一个工具 digest 不一致
|
|
221
|
+
// - check 2: toolDefs 缺少一个 chatgpt_core 工具(数量 16)
|
|
222
|
+
// - check 3: registry 有 22 个 chatgpt_core 工具
|
|
223
|
+
const driftedSchema = OTHER_SCHEMA;
|
|
224
|
+
const registry = makeCoreRegistry(21);
|
|
225
|
+
// 第一个工具的 digest 指向 SAMPLE_SCHEMA,但 toolDefs 中该工具用 driftedSchema
|
|
226
|
+
registry[0] = makeMeta({
|
|
227
|
+
name: CHATGPT_CORE_TOOL_NAMES[0],
|
|
228
|
+
inputSchema: SAMPLE_SCHEMA,
|
|
229
|
+
profiles: ["chatgpt_core"],
|
|
230
|
+
});
|
|
231
|
+
// 追加一个额外的 chatgpt_core 工具,使 profile 工具数变为 22
|
|
232
|
+
registry.push(makeMeta({ name: "extra_tool", profiles: ["chatgpt_core"] }));
|
|
233
|
+
|
|
234
|
+
// toolDefs:第一个工具用 driftedSchema(触发 check 1),且移除最后一个核心工具(触发 check 2)
|
|
235
|
+
const coreNames = [...CHATGPT_CORE_TOOL_NAMES];
|
|
236
|
+
const toolDefEntries = coreNames.slice(0, 20).map((name) => ({
|
|
237
|
+
name,
|
|
238
|
+
inputSchema: name === CHATGPT_CORE_TOOL_NAMES[0] ? driftedSchema : SAMPLE_SCHEMA,
|
|
239
|
+
}));
|
|
240
|
+
const toolDefs = makeToolDefs(toolDefEntries);
|
|
241
|
+
|
|
242
|
+
const result = runAllSchemaDriftChecks(registry, toolDefs);
|
|
243
|
+
|
|
244
|
+
assert.equal(result.ok, false);
|
|
245
|
+
// 至少包含来自三个检查的警告
|
|
246
|
+
assert.ok(
|
|
247
|
+
result.warnings.length >= 3,
|
|
248
|
+
`应有至少 3 条警告,实际 ${result.warnings.length}`,
|
|
249
|
+
);
|
|
250
|
+
// 验证三类警告都存在
|
|
251
|
+
const hasDigestDrift = result.warnings.some((w) => w.startsWith("Schema drift for tool"));
|
|
252
|
+
const hasManifestMissing = result.warnings.some((w) => w.includes("missing tools"));
|
|
253
|
+
const hasProfileCount = result.warnings.some((w) => w.includes("profile tool count changed"));
|
|
254
|
+
assert.ok(hasDigestDrift, "应包含 registry digest drift 警告");
|
|
255
|
+
assert.ok(hasManifestMissing, "应包含 manifest 缺失工具警告");
|
|
256
|
+
assert.ok(hasProfileCount, "应包含 profile 数量变化警告");
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { createGoal, readGoalStatus, writeGoalStatus } from "../../goal/goalStore.js";
|
|
7
|
+
import { addSubgoal, updateSubgoalStatus } from "../../goal/goalStatus.js";
|
|
8
|
+
import { syncSubgoalOnTaskDone, readTaskGoalMeta } from "../../goal/subgoalSync.js";
|
|
9
|
+
|
|
10
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
let tempDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-subgoalsync-"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 在 tempDir 下创建 goal,添加一个 subgoal 并置为 running,写回 goal_status.json。
|
|
24
|
+
* 返回 goal_id 和 subgoalId。
|
|
25
|
+
*/
|
|
26
|
+
function setupGoalWithRunningSubgoal(
|
|
27
|
+
title = "Test Goal"
|
|
28
|
+
): { goalId: string; subgoalId: string } {
|
|
29
|
+
const { goal_id } = createGoal("repo", title, "desc", tempDir);
|
|
30
|
+
let status = readGoalStatus(goal_id, tempDir);
|
|
31
|
+
const { goalStatus: withSub, subgoalId } = addSubgoal(status, "First subgoal");
|
|
32
|
+
const withRunning = updateSubgoalStatus(withSub, subgoalId, "running");
|
|
33
|
+
writeGoalStatus(goal_id, withRunning, tempDir);
|
|
34
|
+
return { goalId: goal_id, subgoalId };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 创建一个假的 task 目录,写入 status.json(可包含 goal_id/subgoal_id)。 */
|
|
38
|
+
function writeTaskStatus(
|
|
39
|
+
taskId: string,
|
|
40
|
+
fields: Record<string, unknown>
|
|
41
|
+
): string {
|
|
42
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", taskId);
|
|
43
|
+
mkdirSync(taskDir, { recursive: true });
|
|
44
|
+
writeFileSync(
|
|
45
|
+
join(taskDir, "status.json"),
|
|
46
|
+
JSON.stringify({ task_id: taskId, ...fields }, null, 2),
|
|
47
|
+
"utf-8"
|
|
48
|
+
);
|
|
49
|
+
return taskDir;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
describe("subgoalSync", () => {
|
|
55
|
+
|
|
56
|
+
describe("syncSubgoalOnTaskDone", () => {
|
|
57
|
+
it("无 goal_id/subgoal_id 直接返回(向后兼容)", () => {
|
|
58
|
+
const { goalId, subgoalId } = setupGoalWithRunningSubgoal();
|
|
59
|
+
// 不传 goal_id / subgoal_id
|
|
60
|
+
syncSubgoalOnTaskDone("task_001", { goal_id: null, subgoal_id: null }, tempDir);
|
|
61
|
+
|
|
62
|
+
// subgoal 状态不变
|
|
63
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
64
|
+
assert.equal(status.subgoals[0].status, "running");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("仅 goal_id 缺失直接返回", () => {
|
|
68
|
+
const { subgoalId } = setupGoalWithRunningSubgoal();
|
|
69
|
+
syncSubgoalOnTaskDone("task_001", { goal_id: null, subgoal_id: subgoalId }, tempDir);
|
|
70
|
+
// 不应抛错
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("仅 subgoal_id 缺失直接返回", () => {
|
|
74
|
+
const { goalId } = setupGoalWithRunningSubgoal();
|
|
75
|
+
syncSubgoalOnTaskDone("task_001", { goal_id: goalId, subgoal_id: null }, tempDir);
|
|
76
|
+
// 不应抛错
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("subgoal 状态为 running → 更新为 done_by_agent", () => {
|
|
80
|
+
const { goalId, subgoalId } = setupGoalWithRunningSubgoal();
|
|
81
|
+
syncSubgoalOnTaskDone("task_001", { goal_id: goalId, subgoal_id: subgoalId }, tempDir);
|
|
82
|
+
|
|
83
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
84
|
+
assert.equal(status.subgoals[0].status, "done_by_agent");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("subgoal 状态非 running(如 ready)→ 不更新", () => {
|
|
88
|
+
const { goal_id } = createGoal("repo", "Ready Goal", "desc", tempDir);
|
|
89
|
+
let status = readGoalStatus(goal_id, tempDir);
|
|
90
|
+
const { goalStatus: withSub, subgoalId } = addSubgoal(status, "Ready subgoal");
|
|
91
|
+
// 不做 running 转换,保持 ready
|
|
92
|
+
writeGoalStatus(goal_id, withSub, tempDir);
|
|
93
|
+
|
|
94
|
+
syncSubgoalOnTaskDone("task_001", { goal_id: goal_id, subgoal_id: subgoalId }, tempDir);
|
|
95
|
+
|
|
96
|
+
const after = readGoalStatus(goal_id, tempDir);
|
|
97
|
+
assert.equal(after.subgoals[0].status, "ready");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("subgoal 状态为 done_by_agent → 不重复更新", () => {
|
|
101
|
+
const { goalId, subgoalId } = setupGoalWithRunningSubgoal();
|
|
102
|
+
// 第一次同步:running → done_by_agent
|
|
103
|
+
syncSubgoalOnTaskDone("task_001", { goal_id: goalId, subgoal_id: subgoalId }, tempDir);
|
|
104
|
+
// 第二次同步:done_by_agent 不应再变(running → done_by_agent 已完成,且 done_by_agent → done_by_agent 非法)
|
|
105
|
+
syncSubgoalOnTaskDone("task_001", { goal_id: goalId, subgoal_id: subgoalId }, tempDir);
|
|
106
|
+
|
|
107
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
108
|
+
assert.equal(status.subgoals[0].status, "done_by_agent");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("goal 不存在 → 不抛错(try/catch 隔离)", () => {
|
|
112
|
+
// 不抛错即通过
|
|
113
|
+
assert.doesNotThrow(() => {
|
|
114
|
+
syncSubgoalOnTaskDone(
|
|
115
|
+
"task_001",
|
|
116
|
+
{ goal_id: "goal_nonexistent", subgoal_id: "subgoal-001" },
|
|
117
|
+
tempDir
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("subgoal 不存在 → 不抛错", () => {
|
|
123
|
+
const { goalId } = setupGoalWithRunningSubgoal();
|
|
124
|
+
assert.doesNotThrow(() => {
|
|
125
|
+
syncSubgoalOnTaskDone(
|
|
126
|
+
"task_001",
|
|
127
|
+
{ goal_id: goalId, subgoal_id: "subgoal-999" },
|
|
128
|
+
tempDir
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
// 原 subgoal 不受影响
|
|
132
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
133
|
+
assert.equal(status.subgoals[0].status, "running");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("不传 workspaceRoot 时不抛错(使用默认 getConfig)", () => {
|
|
137
|
+
// 此测试验证向后兼容签名;默认 getConfig().workspaceRoot 指向 cwd,
|
|
138
|
+
// goal 不存在于那里,应被 try/catch 吞掉,不抛错。
|
|
139
|
+
const { goalId, subgoalId } = setupGoalWithRunningSubgoal();
|
|
140
|
+
assert.doesNotThrow(() => {
|
|
141
|
+
syncSubgoalOnTaskDone("task_001", { goal_id: goalId, subgoal_id: subgoalId });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("readTaskGoalMeta", () => {
|
|
147
|
+
it("正常读取 status.json 中的 goal_id/subgoal_id", () => {
|
|
148
|
+
const taskDir = writeTaskStatus("task_001", {
|
|
149
|
+
goal_id: "goal_20260101_test",
|
|
150
|
+
subgoal_id: "subgoal-001",
|
|
151
|
+
});
|
|
152
|
+
const meta = readTaskGoalMeta(taskDir);
|
|
153
|
+
assert.equal(meta.goal_id, "goal_20260101_test");
|
|
154
|
+
assert.equal(meta.subgoal_id, "subgoal-001");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("status.json 不存在 → 返回 { null, null }", () => {
|
|
158
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", "nonexistent");
|
|
159
|
+
const meta = readTaskGoalMeta(taskDir);
|
|
160
|
+
assert.equal(meta.goal_id, null);
|
|
161
|
+
assert.equal(meta.subgoal_id, null);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("status.json 无 goal_id/subgoal_id 字段 → 返回 { null, null }", () => {
|
|
165
|
+
const taskDir = writeTaskStatus("task_002", { status: "pending" });
|
|
166
|
+
const meta = readTaskGoalMeta(taskDir);
|
|
167
|
+
assert.equal(meta.goal_id, null);
|
|
168
|
+
assert.equal(meta.subgoal_id, null);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("status.json 字段为非字符串 → 返回 { null, null }", () => {
|
|
172
|
+
const taskDir = writeTaskStatus("task_003", {
|
|
173
|
+
goal_id: 123,
|
|
174
|
+
subgoal_id: null,
|
|
175
|
+
});
|
|
176
|
+
const meta = readTaskGoalMeta(taskDir);
|
|
177
|
+
assert.equal(meta.goal_id, null);
|
|
178
|
+
assert.equal(meta.subgoal_id, null);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("status.json 为损坏 JSON → 返回 { null, null }", () => {
|
|
182
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", "task_004");
|
|
183
|
+
mkdirSync(taskDir, { recursive: true });
|
|
184
|
+
writeFileSync(join(taskDir, "status.json"), "{ not valid json", "utf-8");
|
|
185
|
+
const meta = readTaskGoalMeta(taskDir);
|
|
186
|
+
assert.equal(meta.goal_id, null);
|
|
187
|
+
assert.equal(meta.subgoal_id, null);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("仅 goal_id 字段存在 → subgoal_id 为 null", () => {
|
|
191
|
+
const taskDir = writeTaskStatus("task_005", { goal_id: "goal_x" });
|
|
192
|
+
const meta = readTaskGoalMeta(taskDir);
|
|
193
|
+
assert.equal(meta.goal_id, "goal_x");
|
|
194
|
+
assert.equal(meta.subgoal_id, null);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("end-to-end: task 完成 → subgoal 同步", () => {
|
|
199
|
+
it("模拟 reconcileTasks done_by_agent 流程:task status.json 写入后同步 subgoal", () => {
|
|
200
|
+
const { goalId, subgoalId } = setupGoalWithRunningSubgoal();
|
|
201
|
+
const taskId = "task_20260101_abc123";
|
|
202
|
+
|
|
203
|
+
// 模拟 createTask 写入 task status.json(含 goal_id/subgoal_id)
|
|
204
|
+
const taskDir = writeTaskStatus(taskId, {
|
|
205
|
+
status: "pending",
|
|
206
|
+
goal_id: goalId,
|
|
207
|
+
subgoal_id: subgoalId,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// 模拟 reconcileTasks 将状态置为 done_by_agent 后调用同步
|
|
211
|
+
const meta = readTaskGoalMeta(taskDir);
|
|
212
|
+
assert.equal(meta.subgoal_id, subgoalId);
|
|
213
|
+
syncSubgoalOnTaskDone(taskId, meta, tempDir);
|
|
214
|
+
|
|
215
|
+
// 验证 subgoal 状态已同步为 done_by_agent
|
|
216
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
217
|
+
assert.equal(status.subgoals[0].status, "done_by_agent");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("无关联的 task 完成 → subgoal 状态不变", () => {
|
|
221
|
+
const { goalId, subgoalId } = setupGoalWithRunningSubgoal();
|
|
222
|
+
const taskId = "task_20260101_nogoal";
|
|
223
|
+
|
|
224
|
+
// task 没有 goal_id/subgoal_id 关联
|
|
225
|
+
const taskDir = writeTaskStatus(taskId, { status: "pending" });
|
|
226
|
+
const meta = readTaskGoalMeta(taskDir);
|
|
227
|
+
assert.equal(meta.subgoal_id, null);
|
|
228
|
+
|
|
229
|
+
// 即便误调用同步,也不会影响 subgoal
|
|
230
|
+
syncSubgoalOnTaskDone(taskId, meta, tempDir);
|
|
231
|
+
|
|
232
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
233
|
+
assert.equal(status.subgoals[0].status, "running");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|