patchwarden 0.6.4 → 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/README.en.md +18 -18
- package/README.md +20 -20
- 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 +15 -2
- 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/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/package.json +3 -2
- 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 +53 -6
- package/scripts/checks/mcp-smoke.js +36 -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 +17 -3
- 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/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
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { runReleaseGateCheck, checkPublishedVerified, checkGitHubReleaseVerified, checkCiVerified, computeTokenDigest, } from "../../release/releaseGate.js";
|
|
4
|
+
// ── Mock helpers ───────────────────────────────────────────────────
|
|
5
|
+
/**
|
|
6
|
+
* Build a mock httpGet that responds based on the URL.
|
|
7
|
+
* Each responder returns an HttpResponse synchronously (wrapped in a Promise).
|
|
8
|
+
*/
|
|
9
|
+
function mockHttpGet(responder) {
|
|
10
|
+
return async (url) => responder(url);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build a mock httpGet that captures headers (for token-leak assertions).
|
|
14
|
+
*/
|
|
15
|
+
function mockHttpGetCapture(responder, capture) {
|
|
16
|
+
return async (url, headers) => {
|
|
17
|
+
capture.headers = { ...(headers || {}) };
|
|
18
|
+
return responder(url);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build a mock httpGet that always throws (simulates network error).
|
|
23
|
+
*/
|
|
24
|
+
function mockHttpGetError(error) {
|
|
25
|
+
return async () => {
|
|
26
|
+
throw error;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// ── 1. checkPublishedVerified ──────────────────────────────────────
|
|
30
|
+
describe("checkPublishedVerified", () => {
|
|
31
|
+
it("version exists → passed", async () => {
|
|
32
|
+
const httpGet = mockHttpGet(() => ({
|
|
33
|
+
statusCode: 200,
|
|
34
|
+
data: { versions: { "1.0.0": {}, "0.9.0": {} } },
|
|
35
|
+
}));
|
|
36
|
+
const result = await checkPublishedVerified("patchwarden", "1.0.0", httpGet);
|
|
37
|
+
assert.equal(result.status, "passed");
|
|
38
|
+
});
|
|
39
|
+
it("version does not exist → failed", async () => {
|
|
40
|
+
const httpGet = mockHttpGet(() => ({
|
|
41
|
+
statusCode: 200,
|
|
42
|
+
data: { versions: { "0.9.0": {} } },
|
|
43
|
+
}));
|
|
44
|
+
const result = await checkPublishedVerified("patchwarden", "9.9.9", httpGet);
|
|
45
|
+
assert.equal(result.status, "failed");
|
|
46
|
+
assert.ok(result.reason?.includes("9.9.9"));
|
|
47
|
+
});
|
|
48
|
+
it("network error → not_checked", async () => {
|
|
49
|
+
const httpGet = mockHttpGetError(new Error("connect ECONNREFUSED"));
|
|
50
|
+
const result = await checkPublishedVerified("patchwarden", "1.0.0", httpGet);
|
|
51
|
+
assert.equal(result.status, "not_checked");
|
|
52
|
+
assert.ok(result.reason?.includes("Network error"));
|
|
53
|
+
});
|
|
54
|
+
it("package 404 → failed", async () => {
|
|
55
|
+
const httpGet = mockHttpGet(() => ({
|
|
56
|
+
statusCode: 404,
|
|
57
|
+
data: { error: "Not found" },
|
|
58
|
+
}));
|
|
59
|
+
const result = await checkPublishedVerified("nonexistent-pkg", "1.0.0", httpGet);
|
|
60
|
+
assert.equal(result.status, "failed");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
// ── 2. checkGitHubReleaseVerified ──────────────────────────────────
|
|
64
|
+
describe("checkGitHubReleaseVerified", () => {
|
|
65
|
+
it("200 → passed", async () => {
|
|
66
|
+
const httpGet = mockHttpGet(() => ({
|
|
67
|
+
statusCode: 200,
|
|
68
|
+
data: { tag_name: "v1.0.0" },
|
|
69
|
+
}));
|
|
70
|
+
const result = await checkGitHubReleaseVerified("user/repo", "v1.0.0", httpGet);
|
|
71
|
+
assert.equal(result.status, "passed");
|
|
72
|
+
});
|
|
73
|
+
it("404 → failed", async () => {
|
|
74
|
+
const httpGet = mockHttpGet(() => ({
|
|
75
|
+
statusCode: 404,
|
|
76
|
+
data: { message: "Not Found" },
|
|
77
|
+
}));
|
|
78
|
+
const result = await checkGitHubReleaseVerified("user/repo", "v1.0.0", httpGet);
|
|
79
|
+
assert.equal(result.status, "failed");
|
|
80
|
+
assert.ok(result.reason?.includes("v1.0.0"));
|
|
81
|
+
});
|
|
82
|
+
it("network error → not_checked", async () => {
|
|
83
|
+
const httpGet = mockHttpGetError(new Error("connect ETIMEDOUT"));
|
|
84
|
+
const result = await checkGitHubReleaseVerified("user/repo", "v1.0.0", httpGet);
|
|
85
|
+
assert.equal(result.status, "not_checked");
|
|
86
|
+
});
|
|
87
|
+
it("token is used as Bearer but never appears in result", async () => {
|
|
88
|
+
const TOKEN = "test-token";
|
|
89
|
+
process.env.GITHUB_TOKEN = TOKEN;
|
|
90
|
+
const capture = {};
|
|
91
|
+
const httpGet = mockHttpGetCapture(() => ({ statusCode: 200, data: { tag_name: "v1.0.0" } }), capture);
|
|
92
|
+
try {
|
|
93
|
+
const result = await checkGitHubReleaseVerified("user/repo", "v1.0.0", httpGet);
|
|
94
|
+
assert.equal(result.status, "passed");
|
|
95
|
+
// Authorization header was set correctly (token used internally)
|
|
96
|
+
assert.equal(capture.headers?.["Authorization"], `Bearer ${TOKEN}`);
|
|
97
|
+
// Raw token must NOT appear anywhere in the serialized result
|
|
98
|
+
const resultStr = JSON.stringify(result);
|
|
99
|
+
assert.ok(!resultStr.includes(TOKEN), "raw GITHUB_TOKEN must not leak into the result object");
|
|
100
|
+
// The token digest is different from the raw token
|
|
101
|
+
const digest = computeTokenDigest(TOKEN);
|
|
102
|
+
assert.ok(digest !== TOKEN, "digest must differ from raw token");
|
|
103
|
+
assert.ok(digest.startsWith("sha256:"));
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
delete process.env.GITHUB_TOKEN;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// ── 3. checkCiVerified ─────────────────────────────────────────────
|
|
111
|
+
describe("checkCiVerified", () => {
|
|
112
|
+
it("conclusion=success → passed", async () => {
|
|
113
|
+
const httpGet = mockHttpGet(() => ({
|
|
114
|
+
statusCode: 200,
|
|
115
|
+
data: { workflow_runs: [{ conclusion: "success" }] },
|
|
116
|
+
}));
|
|
117
|
+
const result = await checkCiVerified("user/repo", "main", httpGet);
|
|
118
|
+
assert.equal(result.status, "passed");
|
|
119
|
+
});
|
|
120
|
+
it("conclusion=failure → failed", async () => {
|
|
121
|
+
const httpGet = mockHttpGet(() => ({
|
|
122
|
+
statusCode: 200,
|
|
123
|
+
data: { workflow_runs: [{ conclusion: "failure" }] },
|
|
124
|
+
}));
|
|
125
|
+
const result = await checkCiVerified("user/repo", "main", httpGet);
|
|
126
|
+
assert.equal(result.status, "failed");
|
|
127
|
+
});
|
|
128
|
+
it("conclusion=null → not_checked", async () => {
|
|
129
|
+
const httpGet = mockHttpGet(() => ({
|
|
130
|
+
statusCode: 200,
|
|
131
|
+
data: { workflow_runs: [{ conclusion: null }] },
|
|
132
|
+
}));
|
|
133
|
+
const result = await checkCiVerified("user/repo", "main", httpGet);
|
|
134
|
+
assert.equal(result.status, "not_checked");
|
|
135
|
+
});
|
|
136
|
+
it("network error → not_checked", async () => {
|
|
137
|
+
const httpGet = mockHttpGetError(new Error("connect ECONNREFUSED"));
|
|
138
|
+
const result = await checkCiVerified("user/repo", "main", httpGet);
|
|
139
|
+
assert.equal(result.status, "not_checked");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
// ── 4. runReleaseGateCheck orchestration ───────────────────────────
|
|
143
|
+
describe("runReleaseGateCheck", () => {
|
|
144
|
+
it("target=local_ready passed → local_ready=passed, rest not_checked, no blocked_reason", async () => {
|
|
145
|
+
const deps = {
|
|
146
|
+
checkLocalReady: () => ({ status: "passed" }),
|
|
147
|
+
};
|
|
148
|
+
const result = await runReleaseGateCheck("/fake/repo", "local_ready", {}, deps);
|
|
149
|
+
assert.equal(result.target_stage, "local_ready");
|
|
150
|
+
assert.equal(result.stages.local_ready, "passed");
|
|
151
|
+
assert.equal(result.stages.packed_ready, "not_checked");
|
|
152
|
+
assert.equal(result.stages.published_verified, "not_checked");
|
|
153
|
+
assert.equal(result.stages.github_release_verified, "not_checked");
|
|
154
|
+
assert.equal(result.stages.ci_verified, "not_checked");
|
|
155
|
+
assert.equal(result.blocked_reason, undefined);
|
|
156
|
+
});
|
|
157
|
+
it("local_ready failed → subsequent stages all not_checked, blocked_reason non-empty", async () => {
|
|
158
|
+
const deps = {
|
|
159
|
+
checkLocalReady: () => ({ status: "failed", reason: "build exited with code 1" }),
|
|
160
|
+
};
|
|
161
|
+
const result = await runReleaseGateCheck("/fake/repo", "ci_verified", {
|
|
162
|
+
packageName: "patchwarden",
|
|
163
|
+
version: "1.0.0",
|
|
164
|
+
githubRepo: "user/repo",
|
|
165
|
+
branch: "main",
|
|
166
|
+
}, deps);
|
|
167
|
+
assert.equal(result.stages.local_ready, "failed");
|
|
168
|
+
assert.equal(result.stages.packed_ready, "not_checked");
|
|
169
|
+
assert.equal(result.stages.published_verified, "not_checked");
|
|
170
|
+
assert.equal(result.stages.github_release_verified, "not_checked");
|
|
171
|
+
assert.equal(result.stages.ci_verified, "not_checked");
|
|
172
|
+
assert.ok(result.blocked_reason, "blocked_reason should be non-empty");
|
|
173
|
+
assert.ok(result.blocked_reason.includes("local_ready"), "blocked_reason should mention the failed stage");
|
|
174
|
+
});
|
|
175
|
+
it("full pass-through: all stages passed up to ci_verified", async () => {
|
|
176
|
+
const deps = {
|
|
177
|
+
checkLocalReady: () => ({ status: "passed" }),
|
|
178
|
+
checkPackedReady: () => ({ status: "passed", manifestPath: "/fake/release-artifact-manifest.json" }),
|
|
179
|
+
checkPublishedVerified: async () => ({ status: "passed" }),
|
|
180
|
+
checkGitHubReleaseVerified: async () => ({ status: "passed" }),
|
|
181
|
+
checkCiVerified: async () => ({ status: "passed" }),
|
|
182
|
+
};
|
|
183
|
+
const result = await runReleaseGateCheck("/fake/repo", "ci_verified", {
|
|
184
|
+
packageName: "patchwarden",
|
|
185
|
+
version: "1.0.0",
|
|
186
|
+
githubRepo: "user/repo",
|
|
187
|
+
branch: "main",
|
|
188
|
+
}, deps);
|
|
189
|
+
assert.equal(result.stages.local_ready, "passed");
|
|
190
|
+
assert.equal(result.stages.packed_ready, "passed");
|
|
191
|
+
assert.equal(result.stages.published_verified, "passed");
|
|
192
|
+
assert.equal(result.stages.github_release_verified, "passed");
|
|
193
|
+
assert.equal(result.stages.ci_verified, "passed");
|
|
194
|
+
assert.equal(result.blocked_reason, undefined);
|
|
195
|
+
});
|
|
196
|
+
it("missing options for a required stage → failed with clear reason", async () => {
|
|
197
|
+
const deps = {
|
|
198
|
+
checkLocalReady: () => ({ status: "passed" }),
|
|
199
|
+
checkPackedReady: () => ({ status: "passed" }),
|
|
200
|
+
};
|
|
201
|
+
// published_verified needs packageName + version, neither provided
|
|
202
|
+
const result = await runReleaseGateCheck("/fake/repo", "published_verified", {}, deps);
|
|
203
|
+
assert.equal(result.stages.local_ready, "passed");
|
|
204
|
+
assert.equal(result.stages.packed_ready, "passed");
|
|
205
|
+
assert.equal(result.stages.published_verified, "failed");
|
|
206
|
+
assert.ok(result.blocked_reason?.includes("published_verified"));
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
// ── 5. Function name spelling (Verified, not Verfied) ─────────────
|
|
210
|
+
describe("function name spelling", () => {
|
|
211
|
+
it("checkPublishedVerified is a function (not Verfied)", () => {
|
|
212
|
+
assert.equal(typeof checkPublishedVerified, "function");
|
|
213
|
+
});
|
|
214
|
+
it("checkGitHubReleaseVerified is a function", () => {
|
|
215
|
+
assert.equal(typeof checkGitHubReleaseVerified, "function");
|
|
216
|
+
});
|
|
217
|
+
it("checkCiVerified is a function", () => {
|
|
218
|
+
assert.equal(typeof checkCiVerified, "function");
|
|
219
|
+
});
|
|
220
|
+
it("runReleaseGateCheck is a function", () => {
|
|
221
|
+
assert.equal(typeof runReleaseGateCheck, "function");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
// ── 6. computeTokenDigest ──────────────────────────────────────────
|
|
225
|
+
describe("computeTokenDigest", () => {
|
|
226
|
+
it("returns sha256: prefix with hex digest", () => {
|
|
227
|
+
const digest = computeTokenDigest("test-token");
|
|
228
|
+
assert.ok(digest.startsWith("sha256:"));
|
|
229
|
+
// sha256 hex is 64 chars + "sha256:" prefix = 71 chars
|
|
230
|
+
assert.equal(digest.length, 71);
|
|
231
|
+
});
|
|
232
|
+
it("does not contain the raw token", () => {
|
|
233
|
+
const TOKEN = "test-token";
|
|
234
|
+
const digest = computeTokenDigest(TOKEN);
|
|
235
|
+
assert.ok(!digest.includes(TOKEN), "digest must not contain the raw token");
|
|
236
|
+
});
|
|
237
|
+
it("is deterministic for the same input", () => {
|
|
238
|
+
const d1 = computeTokenDigest("abc");
|
|
239
|
+
const d2 = computeTokenDigest("abc");
|
|
240
|
+
assert.equal(d1, d2);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
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 { reloadConfig } from "../../config.js";
|
|
7
|
+
import { safeDirectSummary, safeAuditDirectSession, safeDiffSummary, safeResult, safeTestSummary } from "../../tools/safeViews.js";
|
|
8
|
+
let tempDir;
|
|
9
|
+
let prevConfigEnv;
|
|
10
|
+
function writeConfig() {
|
|
11
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
12
|
+
writeFileSync(configPath, JSON.stringify({
|
|
13
|
+
workspaceRoot: tempDir,
|
|
14
|
+
tasksDir: ".patchwarden/tasks",
|
|
15
|
+
plansDir: ".patchwarden/plans",
|
|
16
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
17
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
18
|
+
agents: {},
|
|
19
|
+
allowedTestCommands: ["npm test"],
|
|
20
|
+
directAllowedCommands: ["npm test"],
|
|
21
|
+
enableDirectProfile: true,
|
|
22
|
+
}), "utf-8");
|
|
23
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
24
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
25
|
+
reloadConfig();
|
|
26
|
+
}
|
|
27
|
+
describe("safeViews", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-safeviews-"));
|
|
30
|
+
writeConfig();
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
if (prevConfigEnv === undefined)
|
|
34
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
35
|
+
else
|
|
36
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
37
|
+
reloadConfig();
|
|
38
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
it("returns bounded task summaries without log or diff content", () => {
|
|
41
|
+
const taskId = "task-safe-001";
|
|
42
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", taskId);
|
|
43
|
+
mkdirSync(taskDir, { recursive: true });
|
|
44
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify({
|
|
45
|
+
task_id: taskId,
|
|
46
|
+
status: "done_by_agent",
|
|
47
|
+
phase: "done_by_agent",
|
|
48
|
+
repo_path: "repo",
|
|
49
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
50
|
+
verify_status: "passed",
|
|
51
|
+
}), "utf-8");
|
|
52
|
+
writeFileSync(join(taskDir, "result.json"), JSON.stringify({
|
|
53
|
+
task_id: taskId,
|
|
54
|
+
status: "done_by_agent",
|
|
55
|
+
summary: "Finished safely",
|
|
56
|
+
changed_files: [{ path: "src/index.ts", change: "modified" }],
|
|
57
|
+
verify_status: "passed",
|
|
58
|
+
warnings: [],
|
|
59
|
+
}), "utf-8");
|
|
60
|
+
writeFileSync(join(taskDir, "verify.json"), JSON.stringify({
|
|
61
|
+
status: "passed",
|
|
62
|
+
commands: [{ command: "npm test", status: "passed", exit_code: 0, stdout_tail: "SECRET_STDOUT", stderr_tail: "SECRET_STDERR" }],
|
|
63
|
+
}), "utf-8");
|
|
64
|
+
writeFileSync(join(taskDir, "changed-files.json"), JSON.stringify({
|
|
65
|
+
changed_files: [{ path: "src/index.ts", change: "modified", tracked: true, ignored: false, kind: "source" }],
|
|
66
|
+
additions: 1,
|
|
67
|
+
deletions: 0,
|
|
68
|
+
diff_available: true,
|
|
69
|
+
diff_truncated: false,
|
|
70
|
+
patch_mode: "textual",
|
|
71
|
+
artifact_hygiene: { counts: { source_changes: 1 }, source_changes: [], tracked_build_artifacts: [], ignored_untracked_artifacts: [], runtime_generated_files: [], suspicious_changes: [] },
|
|
72
|
+
}), "utf-8");
|
|
73
|
+
writeFileSync(join(taskDir, "git.diff"), "SECRET_DIFF", "utf-8");
|
|
74
|
+
writeFileSync(join(taskDir, "test.log"), "SECRET_TEST_LOG", "utf-8");
|
|
75
|
+
const payload = JSON.stringify({
|
|
76
|
+
result: safeResult(taskId),
|
|
77
|
+
tests: safeTestSummary(taskId),
|
|
78
|
+
diff: safeDiffSummary(taskId),
|
|
79
|
+
});
|
|
80
|
+
assert.ok(!payload.includes("SECRET_STDOUT"));
|
|
81
|
+
assert.ok(!payload.includes("SECRET_STDERR"));
|
|
82
|
+
assert.ok(!payload.includes("SECRET_DIFF"));
|
|
83
|
+
assert.ok(!payload.includes("SECRET_TEST_LOG"));
|
|
84
|
+
assert.ok(payload.includes("src/index.ts"));
|
|
85
|
+
});
|
|
86
|
+
it("treats corrupted safe-view JSON artifacts as empty summaries", () => {
|
|
87
|
+
const taskId = "task-safe-corrupt";
|
|
88
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", taskId);
|
|
89
|
+
mkdirSync(taskDir, { recursive: true });
|
|
90
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify({
|
|
91
|
+
task_id: taskId,
|
|
92
|
+
status: "done_by_agent",
|
|
93
|
+
phase: "done_by_agent",
|
|
94
|
+
repo_path: "repo",
|
|
95
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
96
|
+
verify_status: "passed",
|
|
97
|
+
}), "utf-8");
|
|
98
|
+
writeFileSync(join(taskDir, "result.json"), JSON.stringify({
|
|
99
|
+
task_id: taskId,
|
|
100
|
+
status: "done_by_agent",
|
|
101
|
+
summary: "Finished safely",
|
|
102
|
+
changed_files: [],
|
|
103
|
+
verify_status: "passed",
|
|
104
|
+
warnings: [],
|
|
105
|
+
}), "utf-8");
|
|
106
|
+
writeFileSync(join(taskDir, "verify.json"), "\uFEFF{not json", "utf-8");
|
|
107
|
+
writeFileSync(join(taskDir, "changed-files.json"), "{not json", "utf-8");
|
|
108
|
+
const tests = safeTestSummary(taskId);
|
|
109
|
+
const diff = safeDiffSummary(taskId);
|
|
110
|
+
assert.equal(tests.command_count, 0);
|
|
111
|
+
assert.equal(diff.changed_files_total, 0);
|
|
112
|
+
});
|
|
113
|
+
it("returns Direct safe summaries without verification tails", () => {
|
|
114
|
+
const sessionId = "direct-safe-001";
|
|
115
|
+
const sessionDir = join(tempDir, ".patchwarden", "direct-sessions", sessionId);
|
|
116
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
117
|
+
const session = {
|
|
118
|
+
session_id: sessionId,
|
|
119
|
+
title: "safe direct",
|
|
120
|
+
repo_path: "repo",
|
|
121
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
122
|
+
created_at: new Date().toISOString(),
|
|
123
|
+
expires_at: new Date(Date.now() + 60_000).toISOString(),
|
|
124
|
+
server_version: "1.0.0",
|
|
125
|
+
schema_epoch: "test",
|
|
126
|
+
tool_manifest_sha256: "x".repeat(64),
|
|
127
|
+
workspace_snapshot_before: { head: null, status: "", files: {}, workspace_dirty: false, warnings: [], is_git: false },
|
|
128
|
+
workspace_fingerprint_before: "fingerprint",
|
|
129
|
+
allowed_commands: ["npm test"],
|
|
130
|
+
operations: [],
|
|
131
|
+
verification_runs: [{
|
|
132
|
+
command: "npm test",
|
|
133
|
+
exit_code: 0,
|
|
134
|
+
passed: true,
|
|
135
|
+
timed_out: false,
|
|
136
|
+
stdout_tail: "SECRET_DIRECT_STDOUT",
|
|
137
|
+
stderr_tail: "SECRET_DIRECT_STDERR",
|
|
138
|
+
started_at: new Date().toISOString(),
|
|
139
|
+
finished_at: new Date().toISOString(),
|
|
140
|
+
log_path: join(sessionDir, "verification.log"),
|
|
141
|
+
}],
|
|
142
|
+
finalized: true,
|
|
143
|
+
finalized_at: new Date().toISOString(),
|
|
144
|
+
audited: false,
|
|
145
|
+
change_artifacts: {
|
|
146
|
+
changed_files: [],
|
|
147
|
+
diff: "SECRET_DIRECT_DIFF",
|
|
148
|
+
diff_available: true,
|
|
149
|
+
diff_truncated: false,
|
|
150
|
+
diff_size_bytes: 18,
|
|
151
|
+
additions: 0,
|
|
152
|
+
deletions: 0,
|
|
153
|
+
file_stats: [],
|
|
154
|
+
workspace_dirty_before: false,
|
|
155
|
+
workspace_dirty_after: false,
|
|
156
|
+
patch_mode: "no_changes",
|
|
157
|
+
unavailable_reason: null,
|
|
158
|
+
artifact_hygiene: { counts: { source_changes: 0, tracked_build_artifacts: 0, ignored_untracked_artifacts: 0, runtime_generated_files: 0, suspicious_changes: 0 }, source_changes: [], tracked_build_artifacts: [], ignored_untracked_artifacts: [], runtime_generated_files: [], suspicious_changes: [] },
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
writeFileSync(join(sessionDir, "session.json"), JSON.stringify(session, null, 2), "utf-8");
|
|
162
|
+
const payload = JSON.stringify({
|
|
163
|
+
summary: safeDirectSummary(sessionId),
|
|
164
|
+
audit: safeAuditDirectSession(sessionId),
|
|
165
|
+
});
|
|
166
|
+
assert.ok(!payload.includes("SECRET_DIRECT_STDOUT"));
|
|
167
|
+
assert.ok(!payload.includes("SECRET_DIRECT_STDERR"));
|
|
168
|
+
assert.ok(!payload.includes("SECRET_DIRECT_DIFF"));
|
|
169
|
+
assert.ok(payload.includes("npm test"));
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { computeSchemaDigest } from "../../tools/toolRegistry.js";
|
|
4
|
+
import { CHATGPT_CORE_TOOL_NAMES } from "../../tools/toolCatalog.js";
|
|
5
|
+
import { checkRegistrySchemaDigest, checkChatgptCoreManifestStable, checkNewToolsProfileAppend, runAllSchemaDriftChecks, } from "../../tools/schemaDriftCheck.js";
|
|
6
|
+
// ── 测试夹具构造工具 ─────────────────────────────────────────────
|
|
7
|
+
const SAMPLE_SCHEMA = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
task_id: { type: "string", description: "Task ID" },
|
|
11
|
+
},
|
|
12
|
+
required: ["task_id"],
|
|
13
|
+
};
|
|
14
|
+
const OTHER_SCHEMA = {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
plan_id: { type: "string", description: "Plan ID" },
|
|
18
|
+
},
|
|
19
|
+
required: ["plan_id"],
|
|
20
|
+
};
|
|
21
|
+
/** 构造一个满足 PatchWardenToolMeta 接口的元数据对象。 */
|
|
22
|
+
function makeMeta(opts) {
|
|
23
|
+
const schema = opts.inputSchema ?? SAMPLE_SCHEMA;
|
|
24
|
+
return {
|
|
25
|
+
name: opts.name,
|
|
26
|
+
title: `Tool ${opts.name}`,
|
|
27
|
+
summary: `summary for ${opts.name}`,
|
|
28
|
+
description: `description for ${opts.name}`,
|
|
29
|
+
profiles: opts.profiles ?? ["full"],
|
|
30
|
+
modes: ["diagnostic"],
|
|
31
|
+
tags: [],
|
|
32
|
+
aliases: [],
|
|
33
|
+
risk: "readonly",
|
|
34
|
+
requiresConfirmation: false,
|
|
35
|
+
inputSchemaDigest: opts.inputSchemaDigest ?? computeSchemaDigest(schema),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** 构造 toolDefs Map。 */
|
|
39
|
+
function makeToolDefs(entries) {
|
|
40
|
+
const map = new Map();
|
|
41
|
+
for (const e of entries) {
|
|
42
|
+
map.set(e.name, {
|
|
43
|
+
inputSchema: e.inputSchema ?? SAMPLE_SCHEMA,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return map;
|
|
47
|
+
}
|
|
48
|
+
/** 构造包含全部 21 个 chatgpt_core 工具(顺序一致)的 toolDefs Map。 */
|
|
49
|
+
function makeCoreToolDefs() {
|
|
50
|
+
return makeToolDefs([...CHATGPT_CORE_TOOL_NAMES].map((name) => ({ name })));
|
|
51
|
+
}
|
|
52
|
+
/** 构造一个 registry:所有工具都属于 chatgpt_core profile。 */
|
|
53
|
+
function makeCoreRegistry(count) {
|
|
54
|
+
const names = [...CHATGPT_CORE_TOOL_NAMES].slice(0, count);
|
|
55
|
+
return names.map((name) => makeMeta({ name, profiles: ["chatgpt_core"] }));
|
|
56
|
+
}
|
|
57
|
+
// ── 1. checkRegistrySchemaDigest ─────────────────────────────────
|
|
58
|
+
describe("checkRegistrySchemaDigest", () => {
|
|
59
|
+
it("digest 一致时 ok=true,warnings 为空", () => {
|
|
60
|
+
const schema = { type: "object", properties: { a: { type: "string" } } };
|
|
61
|
+
const registry = [makeMeta({ name: "tool_a", inputSchema: schema })];
|
|
62
|
+
const toolDefs = makeToolDefs([{ name: "tool_a", inputSchema: schema }]);
|
|
63
|
+
const result = checkRegistrySchemaDigest(registry, toolDefs);
|
|
64
|
+
assert.equal(result.ok, true);
|
|
65
|
+
assert.deepEqual(result.warnings, []);
|
|
66
|
+
});
|
|
67
|
+
it("digest 不一致时 ok=false,warnings 包含工具名和两个 digest", () => {
|
|
68
|
+
const registrySchema = SAMPLE_SCHEMA;
|
|
69
|
+
const actualSchema = OTHER_SCHEMA;
|
|
70
|
+
// registry 记录的是 registrySchema 的 digest,但实际 toolDef 用的是 actualSchema
|
|
71
|
+
const registry = [
|
|
72
|
+
makeMeta({ name: "tool_drift", inputSchema: registrySchema }),
|
|
73
|
+
];
|
|
74
|
+
const toolDefs = makeToolDefs([{ name: "tool_drift", inputSchema: actualSchema }]);
|
|
75
|
+
const result = checkRegistrySchemaDigest(registry, toolDefs);
|
|
76
|
+
assert.equal(result.ok, false);
|
|
77
|
+
assert.equal(result.warnings.length, 1);
|
|
78
|
+
const warning = result.warnings[0];
|
|
79
|
+
const expectedRegistryDigest = computeSchemaDigest(registrySchema);
|
|
80
|
+
const expectedActualDigest = computeSchemaDigest(actualSchema);
|
|
81
|
+
assert.ok(warning.includes("tool_drift"), "warning 应包含工具名");
|
|
82
|
+
assert.ok(warning.includes(expectedRegistryDigest), "warning 应包含 registry digest");
|
|
83
|
+
assert.ok(warning.includes(expectedActualDigest), "warning 应包含 actual digest");
|
|
84
|
+
});
|
|
85
|
+
it("toolDefs 中找不到工具时跳过(不报 drift)", () => {
|
|
86
|
+
const registry = [makeMeta({ name: "missing_tool" })];
|
|
87
|
+
const toolDefs = makeToolDefs([]); // 空映射
|
|
88
|
+
const result = checkRegistrySchemaDigest(registry, toolDefs);
|
|
89
|
+
assert.equal(result.ok, true);
|
|
90
|
+
assert.deepEqual(result.warnings, []);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
// ── 2. checkChatgptCoreManifestStable ────────────────────────────
|
|
94
|
+
describe("checkChatgptCoreManifestStable", () => {
|
|
95
|
+
it("21 工具全部存在时 ok=true", () => {
|
|
96
|
+
const toolDefs = makeCoreToolDefs();
|
|
97
|
+
const result = checkChatgptCoreManifestStable(toolDefs);
|
|
98
|
+
assert.equal(result.ok, true);
|
|
99
|
+
assert.deepEqual(result.warnings, []);
|
|
100
|
+
});
|
|
101
|
+
it("缺少工具时 ok=false", () => {
|
|
102
|
+
// 移除最后一个工具,使 chatgpt_core 工具数为 20
|
|
103
|
+
const names = [...CHATGPT_CORE_TOOL_NAMES].slice(0, 20);
|
|
104
|
+
const toolDefs = makeToolDefs(names.map((name) => ({ name })));
|
|
105
|
+
const result = checkChatgptCoreManifestStable(toolDefs);
|
|
106
|
+
assert.equal(result.ok, false);
|
|
107
|
+
assert.equal(result.warnings.length, 1);
|
|
108
|
+
assert.ok(result.warnings[0].includes("missing tools"), "warning 应包含 missing tools");
|
|
109
|
+
assert.ok(result.warnings[0].includes(CHATGPT_CORE_TOOL_NAMES[20]), "warning 应包含缺失的工具名");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
// ── 3. checkNewToolsProfileAppend ────────────────────────────────
|
|
113
|
+
describe("checkNewToolsProfileAppend", () => {
|
|
114
|
+
it("chatgpt_core 21 工具时 ok=true", () => {
|
|
115
|
+
const registry = makeCoreRegistry(21);
|
|
116
|
+
const result = checkNewToolsProfileAppend(registry);
|
|
117
|
+
assert.equal(result.ok, true);
|
|
118
|
+
assert.deepEqual(result.warnings, []);
|
|
119
|
+
});
|
|
120
|
+
it("chatgpt_core 工具数不为 21 时 ok=false", () => {
|
|
121
|
+
// 22 个 chatgpt_core 工具:21 个标准工具 + 1 个重复名(模拟新工具追加)
|
|
122
|
+
const registry = makeCoreRegistry(21);
|
|
123
|
+
registry.push(makeMeta({ name: "extra_tool", profiles: ["chatgpt_core"] }));
|
|
124
|
+
const result = checkNewToolsProfileAppend(registry);
|
|
125
|
+
assert.equal(result.ok, false);
|
|
126
|
+
assert.equal(result.warnings.length, 1);
|
|
127
|
+
assert.ok(result.warnings[0].includes("expected 21"), "warning 应包含预期数量 21");
|
|
128
|
+
assert.ok(result.warnings[0].includes("got 22"), "warning 应包含实际数量 22");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
// ── 4. runAllSchemaDriftChecks ───────────────────────────────────
|
|
132
|
+
describe("runAllSchemaDriftChecks", () => {
|
|
133
|
+
it("无 drift 时 ok=true", () => {
|
|
134
|
+
// registry 与 toolDefs 完全一致,且 chatgpt_core 工具数为 21
|
|
135
|
+
const registry = makeCoreRegistry(21);
|
|
136
|
+
const toolDefs = makeCoreToolDefs();
|
|
137
|
+
const result = runAllSchemaDriftChecks(registry, toolDefs);
|
|
138
|
+
assert.equal(result.ok, true);
|
|
139
|
+
assert.deepEqual(result.warnings, []);
|
|
140
|
+
});
|
|
141
|
+
it("有 drift 时 ok=false,warnings 合并正确", () => {
|
|
142
|
+
// 构造同时触发三个检查的 drift:
|
|
143
|
+
// - check 1: 一个工具 digest 不一致
|
|
144
|
+
// - check 2: toolDefs 缺少一个 chatgpt_core 工具(数量 16)
|
|
145
|
+
// - check 3: registry 有 22 个 chatgpt_core 工具
|
|
146
|
+
const driftedSchema = OTHER_SCHEMA;
|
|
147
|
+
const registry = makeCoreRegistry(21);
|
|
148
|
+
// 第一个工具的 digest 指向 SAMPLE_SCHEMA,但 toolDefs 中该工具用 driftedSchema
|
|
149
|
+
registry[0] = makeMeta({
|
|
150
|
+
name: CHATGPT_CORE_TOOL_NAMES[0],
|
|
151
|
+
inputSchema: SAMPLE_SCHEMA,
|
|
152
|
+
profiles: ["chatgpt_core"],
|
|
153
|
+
});
|
|
154
|
+
// 追加一个额外的 chatgpt_core 工具,使 profile 工具数变为 22
|
|
155
|
+
registry.push(makeMeta({ name: "extra_tool", profiles: ["chatgpt_core"] }));
|
|
156
|
+
// toolDefs:第一个工具用 driftedSchema(触发 check 1),且移除最后一个核心工具(触发 check 2)
|
|
157
|
+
const coreNames = [...CHATGPT_CORE_TOOL_NAMES];
|
|
158
|
+
const toolDefEntries = coreNames.slice(0, 20).map((name) => ({
|
|
159
|
+
name,
|
|
160
|
+
inputSchema: name === CHATGPT_CORE_TOOL_NAMES[0] ? driftedSchema : SAMPLE_SCHEMA,
|
|
161
|
+
}));
|
|
162
|
+
const toolDefs = makeToolDefs(toolDefEntries);
|
|
163
|
+
const result = runAllSchemaDriftChecks(registry, toolDefs);
|
|
164
|
+
assert.equal(result.ok, false);
|
|
165
|
+
// 至少包含来自三个检查的警告
|
|
166
|
+
assert.ok(result.warnings.length >= 3, `应有至少 3 条警告,实际 ${result.warnings.length}`);
|
|
167
|
+
// 验证三类警告都存在
|
|
168
|
+
const hasDigestDrift = result.warnings.some((w) => w.startsWith("Schema drift for tool"));
|
|
169
|
+
const hasManifestMissing = result.warnings.some((w) => w.includes("missing tools"));
|
|
170
|
+
const hasProfileCount = result.warnings.some((w) => w.includes("profile tool count changed"));
|
|
171
|
+
assert.ok(hasDigestDrift, "应包含 registry digest drift 警告");
|
|
172
|
+
assert.ok(hasManifestMissing, "应包含 manifest 缺失工具警告");
|
|
173
|
+
assert.ok(hasProfileCount, "应包含 profile 数量变化警告");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|