patchwarden 1.5.0 → 1.5.1
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 +43 -2
- package/README.md +43 -2
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directGuards.js +30 -8
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +257 -251
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.js +95 -1
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +6 -0
- package/dist/tools/evidencePack.js +213 -6
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +60 -503
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runTaskLoop.js +4 -4
- package/dist/tools/safeViews.d.ts +2 -2
- package/dist/tools/safeViews.js +2 -2
- package/dist/tools/toolRegistry.js +22 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +2 -2
- package/scripts/checks/control-center-smoke.js +356 -0
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-smoke.js +3 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directGuards.ts +28 -7
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +252 -250
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +110 -1
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +291 -6
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/registry.ts +68 -628
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runTaskLoop.ts +4 -4
- package/src/tools/safeViews.ts +2 -2
- package/src/tools/toolRegistry.ts +22 -0
- package/src/version.ts +1 -1
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +959 -44
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
2
|
import { strict as assert } from "node:assert";
|
|
3
|
-
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { reloadConfig } from "../../config.js";
|
|
@@ -126,5 +126,99 @@ describe("v1.5 evidence packs and agent recommendations", () => {
|
|
|
126
126
|
const listed = listEvidencePacks();
|
|
127
127
|
assert.equal(listed.total, 1);
|
|
128
128
|
assert.equal(listed.evidence_packs[0].lineage_id, "lineage_v15_pack");
|
|
129
|
+
// v2: all six structured artifact files should exist.
|
|
130
|
+
for (const key of ["risk", "verify", "diffstat", "lineage", "attestation", "redactions"]) {
|
|
131
|
+
assert.ok(existsSync(pack.files[key]), `${key} file should exist`);
|
|
132
|
+
}
|
|
133
|
+
// v2: attestation.json must carry the required provenance fields.
|
|
134
|
+
const attestation = JSON.parse(readFileSync(pack.files.attestation, "utf-8"));
|
|
135
|
+
assert.ok(attestation.patchwarden_version, "attestation should have patchwarden_version");
|
|
136
|
+
assert.ok(attestation.package_version, "attestation should have package_version");
|
|
137
|
+
assert.ok(attestation.commit, "attestation should have commit");
|
|
138
|
+
assert.ok(attestation.node_version, "attestation should have node_version");
|
|
139
|
+
assert.ok(attestation.os, "attestation should have os");
|
|
140
|
+
assert.ok(attestation.schema_epoch, "attestation should have schema_epoch");
|
|
141
|
+
// v2: redactions.json must have the audit structure even when empty.
|
|
142
|
+
const redactions = JSON.parse(readFileSync(pack.files.redactions, "utf-8"));
|
|
143
|
+
assert.ok(Array.isArray(redactions.redactions), "redactions should be an array");
|
|
144
|
+
assert.equal(typeof redactions.total_redacted, "number", "total_redacted should be a number");
|
|
145
|
+
});
|
|
146
|
+
it("exports v2 structured artifacts and redacts secrets in diffstat", () => {
|
|
147
|
+
const secretValue = "ghp_1234567890abcdefghijklmnop";
|
|
148
|
+
// Create a task directory with file-stats.json containing a secret in a file path.
|
|
149
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", "task-v2-secret");
|
|
150
|
+
mkdirSync(taskDir, { recursive: true });
|
|
151
|
+
writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
|
|
152
|
+
task_id: "task-v2-secret",
|
|
153
|
+
additions: 5,
|
|
154
|
+
deletions: 2,
|
|
155
|
+
files: [{
|
|
156
|
+
path: `secrets/${secretValue}.json`,
|
|
157
|
+
status: "modified",
|
|
158
|
+
additions: 5,
|
|
159
|
+
deletions: 2,
|
|
160
|
+
}],
|
|
161
|
+
}), "utf-8");
|
|
162
|
+
writeTaskLineage({
|
|
163
|
+
lineage_id: "lineage_v2_secret",
|
|
164
|
+
goal: "Export v2 evidence with diffstat",
|
|
165
|
+
repo_path: ".",
|
|
166
|
+
created_at: "2026-07-09T12:00:00.000Z",
|
|
167
|
+
updated_at: "2026-07-09T12:01:00.000Z",
|
|
168
|
+
final_status: "accepted",
|
|
169
|
+
stop_reason: "success",
|
|
170
|
+
next_action: "accept",
|
|
171
|
+
main_task: "task-v2-secret",
|
|
172
|
+
fix_tasks: [],
|
|
173
|
+
cleanup_tasks: [],
|
|
174
|
+
direct_sessions: [],
|
|
175
|
+
rounds: [{
|
|
176
|
+
iteration: 1,
|
|
177
|
+
task_id: "task-v2-secret",
|
|
178
|
+
role: "main",
|
|
179
|
+
status: "done_by_agent",
|
|
180
|
+
terminal: true,
|
|
181
|
+
verification_status: "passed",
|
|
182
|
+
audit_verdict: "pass",
|
|
183
|
+
fail_checks: [],
|
|
184
|
+
warn_checks: ["minor scope drift"],
|
|
185
|
+
next_action: "accept",
|
|
186
|
+
}],
|
|
187
|
+
warnings: [],
|
|
188
|
+
errors: [],
|
|
189
|
+
worktree: {
|
|
190
|
+
isolation_mode: "current_repo",
|
|
191
|
+
cleanup: "keep",
|
|
192
|
+
status: "not_used",
|
|
193
|
+
next_action: "none",
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
const pack = exportTaskEvidencePack({ lineage_id: "lineage_v2_secret" });
|
|
197
|
+
// All six v2 files should exist.
|
|
198
|
+
for (const key of ["risk", "verify", "diffstat", "lineage", "attestation", "redactions"]) {
|
|
199
|
+
assert.ok(existsSync(pack.files[key]), `${key} file should exist`);
|
|
200
|
+
}
|
|
201
|
+
// The original secret must not appear in any v2 file.
|
|
202
|
+
for (const key of ["risk", "verify", "diffstat", "lineage", "attestation", "redactions"]) {
|
|
203
|
+
const content = readFileSync(pack.files[key], "utf-8");
|
|
204
|
+
assert.ok(!content.includes(secretValue), `${key} should not contain the original secret value`);
|
|
205
|
+
}
|
|
206
|
+
// redactions.json should have detected the secret via diffstat aggregation.
|
|
207
|
+
const redactions = JSON.parse(readFileSync(pack.files.redactions, "utf-8"));
|
|
208
|
+
assert.ok(redactions.total_redacted > 0, "redactions.json should have detected the secret");
|
|
209
|
+
assert.ok(!JSON.stringify(redactions).includes(secretValue), "redactions.json should not contain the original secret value");
|
|
210
|
+
// risk.json should carry the warn_check as a medium-severity risk.
|
|
211
|
+
const risk = JSON.parse(readFileSync(pack.files.risk, "utf-8"));
|
|
212
|
+
assert.ok(risk.count > 0, "risk.json should have at least one risk item");
|
|
213
|
+
assert.equal(risk.by_severity.medium, 1, "should have one medium-severity risk");
|
|
214
|
+
// verify.json should carry the round verification record.
|
|
215
|
+
const verify = JSON.parse(readFileSync(pack.files.verify, "utf-8"));
|
|
216
|
+
assert.ok(verify.count > 0, "verify.json should have at least one record");
|
|
217
|
+
assert.equal(verify.records[0].verification_status, "passed");
|
|
218
|
+
// lineage.json should carry the bounded summary.
|
|
219
|
+
const lineageSummary = JSON.parse(readFileSync(pack.files.lineage, "utf-8"));
|
|
220
|
+
assert.equal(lineageSummary.lineage_id, "lineage_v2_secret");
|
|
221
|
+
assert.equal(lineageSummary.iterations_count, 1);
|
|
222
|
+
assert.equal(lineageSummary.final_status, "accepted");
|
|
129
223
|
});
|
|
130
224
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
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 { getTaskStatus } from "../../tools/getTaskStatus.js";
|
|
8
|
+
let tempDir;
|
|
9
|
+
let tasksDir;
|
|
10
|
+
let prevConfigEnv;
|
|
11
|
+
function writeConfig() {
|
|
12
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
13
|
+
writeFileSync(configPath, JSON.stringify({
|
|
14
|
+
workspaceRoot: tempDir,
|
|
15
|
+
tasksDir: ".patchwarden/tasks",
|
|
16
|
+
plansDir: ".patchwarden/plans",
|
|
17
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
18
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
19
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
20
|
+
allowedTestCommands: ["npm test"],
|
|
21
|
+
}), "utf-8");
|
|
22
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
23
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
24
|
+
reloadConfig();
|
|
25
|
+
}
|
|
26
|
+
function writeTaskStatus(taskId, status) {
|
|
27
|
+
const taskDir = join(tasksDir, taskId);
|
|
28
|
+
mkdirSync(taskDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
|
|
30
|
+
return taskDir;
|
|
31
|
+
}
|
|
32
|
+
function writeTaskRuntime(taskDir, runtime) {
|
|
33
|
+
writeFileSync(join(taskDir, "runtime.json"), JSON.stringify(runtime, null, 2), "utf-8");
|
|
34
|
+
}
|
|
35
|
+
describe("getTaskStatus", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-getstatus-"));
|
|
38
|
+
tasksDir = join(tempDir, ".patchwarden", "tasks");
|
|
39
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
40
|
+
writeConfig();
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
if (prevConfigEnv === undefined)
|
|
44
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
45
|
+
else
|
|
46
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
47
|
+
reloadConfig();
|
|
48
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
// ── pending ──
|
|
51
|
+
it("reads pending status task", () => {
|
|
52
|
+
writeTaskStatus("task-pending-001", {
|
|
53
|
+
task_id: "task-pending-001",
|
|
54
|
+
plan_id: "plan-001",
|
|
55
|
+
agent: "codex",
|
|
56
|
+
workspace_root: tempDir,
|
|
57
|
+
repo_path: "repo",
|
|
58
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
59
|
+
status: "pending",
|
|
60
|
+
phase: "queued",
|
|
61
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
62
|
+
updated_at: "2026-07-12T10:00:01Z",
|
|
63
|
+
timeout_seconds: 900,
|
|
64
|
+
error: null,
|
|
65
|
+
});
|
|
66
|
+
const result = getTaskStatus("task-pending-001");
|
|
67
|
+
assert.equal(result.task_id, "task-pending-001");
|
|
68
|
+
assert.equal(result.status, "pending");
|
|
69
|
+
assert.equal(result.phase, "queued");
|
|
70
|
+
assert.equal(result.agent, "codex");
|
|
71
|
+
assert.equal(result.plan_id, "plan-001");
|
|
72
|
+
assert.equal(result.error, null);
|
|
73
|
+
// No watcher heartbeat → watcher missing → pending task is execution_blocked
|
|
74
|
+
assert.equal(result.execution_blocked, true);
|
|
75
|
+
assert.equal(result.watcher_status, "missing");
|
|
76
|
+
assert.equal(result.pending_reason, "queued_but_watcher_missing");
|
|
77
|
+
});
|
|
78
|
+
// ── running ──
|
|
79
|
+
it("reads running status task and merges runtime.json", () => {
|
|
80
|
+
const taskDir = writeTaskStatus("task-running-001", {
|
|
81
|
+
task_id: "task-running-001",
|
|
82
|
+
plan_id: "plan-001",
|
|
83
|
+
agent: "codex",
|
|
84
|
+
workspace_root: tempDir,
|
|
85
|
+
repo_path: "repo",
|
|
86
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
87
|
+
status: "running",
|
|
88
|
+
phase: "executing_agent",
|
|
89
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
90
|
+
started_at: "2026-07-12T10:00:05Z",
|
|
91
|
+
updated_at: "2026-07-12T10:00:10Z",
|
|
92
|
+
timeout_seconds: 900,
|
|
93
|
+
error: null,
|
|
94
|
+
});
|
|
95
|
+
writeTaskRuntime(taskDir, {
|
|
96
|
+
phase: "executing_agent",
|
|
97
|
+
last_heartbeat_at: "2026-07-12T10:00:12Z",
|
|
98
|
+
current_command: "codex exec --prompt",
|
|
99
|
+
});
|
|
100
|
+
const result = getTaskStatus("task-running-001");
|
|
101
|
+
assert.equal(result.status, "running");
|
|
102
|
+
assert.equal(result.phase, "executing_agent");
|
|
103
|
+
assert.equal(result.current_command, "codex exec --prompt");
|
|
104
|
+
assert.equal(result.last_heartbeat_at, "2026-07-12T10:00:12Z");
|
|
105
|
+
assert.equal(result.started_at, "2026-07-12T10:00:05Z");
|
|
106
|
+
// running (not pending) → execution_blocked is false
|
|
107
|
+
assert.equal(result.execution_blocked, false);
|
|
108
|
+
assert.equal(result.pending_reason, "agent_running");
|
|
109
|
+
});
|
|
110
|
+
// ── done ──
|
|
111
|
+
it("reads done status task", () => {
|
|
112
|
+
writeTaskStatus("task-done-001", {
|
|
113
|
+
task_id: "task-done-001",
|
|
114
|
+
plan_id: "plan-001",
|
|
115
|
+
agent: "codex",
|
|
116
|
+
workspace_root: tempDir,
|
|
117
|
+
repo_path: "repo",
|
|
118
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
119
|
+
status: "done",
|
|
120
|
+
phase: "completed",
|
|
121
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
122
|
+
started_at: "2026-07-12T10:00:05Z",
|
|
123
|
+
finished_at: "2026-07-12T10:05:00Z",
|
|
124
|
+
updated_at: "2026-07-12T10:05:00Z",
|
|
125
|
+
timeout_seconds: 900,
|
|
126
|
+
verify_status: "passed",
|
|
127
|
+
error: null,
|
|
128
|
+
});
|
|
129
|
+
const result = getTaskStatus("task-done-001");
|
|
130
|
+
assert.equal(result.status, "done");
|
|
131
|
+
assert.equal(result.phase, "completed");
|
|
132
|
+
assert.equal(result.finished_at, "2026-07-12T10:05:00Z");
|
|
133
|
+
assert.equal(result.verify_status, "passed");
|
|
134
|
+
assert.equal(result.execution_blocked, false);
|
|
135
|
+
assert.equal(result.pending_reason, null);
|
|
136
|
+
});
|
|
137
|
+
// ── failed ──
|
|
138
|
+
it("reads failed status task", () => {
|
|
139
|
+
writeTaskStatus("task-failed-001", {
|
|
140
|
+
task_id: "task-failed-001",
|
|
141
|
+
plan_id: "plan-001",
|
|
142
|
+
agent: "codex",
|
|
143
|
+
workspace_root: tempDir,
|
|
144
|
+
repo_path: "repo",
|
|
145
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
146
|
+
status: "failed",
|
|
147
|
+
phase: "failed",
|
|
148
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
149
|
+
started_at: "2026-07-12T10:00:05Z",
|
|
150
|
+
finished_at: "2026-07-12T10:05:00Z",
|
|
151
|
+
updated_at: "2026-07-12T10:05:00Z",
|
|
152
|
+
timeout_seconds: 900,
|
|
153
|
+
error: "Agent exited with code 1",
|
|
154
|
+
});
|
|
155
|
+
const result = getTaskStatus("task-failed-001");
|
|
156
|
+
assert.equal(result.status, "failed");
|
|
157
|
+
assert.equal(result.phase, "failed");
|
|
158
|
+
assert.equal(result.error, "Agent exited with code 1");
|
|
159
|
+
assert.equal(result.finished_at, "2026-07-12T10:05:00Z");
|
|
160
|
+
assert.equal(result.execution_blocked, false);
|
|
161
|
+
});
|
|
162
|
+
// ── status.json missing ──
|
|
163
|
+
it("throws when status.json is missing (task not found)", () => {
|
|
164
|
+
// guardReadPath throws "File not found" before the existsSync "Task not found" check
|
|
165
|
+
assert.throws(() => getTaskStatus("non-existent-task"), /(File not found|Task not found)/);
|
|
166
|
+
});
|
|
167
|
+
// ── status.json corrupt ──
|
|
168
|
+
it("throws when status.json is corrupt", () => {
|
|
169
|
+
const taskDir = join(tasksDir, "task-corrupt-001");
|
|
170
|
+
mkdirSync(taskDir, { recursive: true });
|
|
171
|
+
writeFileSync(join(taskDir, "status.json"), "{not valid json", "utf-8");
|
|
172
|
+
assert.throws(() => getTaskStatus("task-corrupt-001"), SyntaxError);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
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 { getTaskSummary } from "../../tools/getTaskSummary.js";
|
|
8
|
+
let tempDir;
|
|
9
|
+
let tasksDir;
|
|
10
|
+
let prevConfigEnv;
|
|
11
|
+
function writeConfig() {
|
|
12
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
13
|
+
writeFileSync(configPath, JSON.stringify({
|
|
14
|
+
workspaceRoot: tempDir,
|
|
15
|
+
tasksDir: ".patchwarden/tasks",
|
|
16
|
+
plansDir: ".patchwarden/plans",
|
|
17
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
18
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
19
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
20
|
+
allowedTestCommands: ["npm test"],
|
|
21
|
+
}), "utf-8");
|
|
22
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
23
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
24
|
+
reloadConfig();
|
|
25
|
+
}
|
|
26
|
+
function writeTaskStatus(taskId, status) {
|
|
27
|
+
const taskDir = join(tasksDir, taskId);
|
|
28
|
+
mkdirSync(taskDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
|
|
30
|
+
return taskDir;
|
|
31
|
+
}
|
|
32
|
+
function baseStatus(taskId, overrides = {}) {
|
|
33
|
+
return {
|
|
34
|
+
task_id: taskId,
|
|
35
|
+
plan_id: "plan-001",
|
|
36
|
+
agent: "codex",
|
|
37
|
+
workspace_root: tempDir,
|
|
38
|
+
repo_path: "repo",
|
|
39
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
40
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
41
|
+
started_at: "2026-07-12T10:00:05Z",
|
|
42
|
+
finished_at: "2026-07-12T10:05:00Z",
|
|
43
|
+
updated_at: "2026-07-12T10:05:00Z",
|
|
44
|
+
timeout_seconds: 900,
|
|
45
|
+
error: null,
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
describe("getTaskSummary", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-getsummary-"));
|
|
52
|
+
tasksDir = join(tempDir, ".patchwarden", "tasks");
|
|
53
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
54
|
+
writeConfig();
|
|
55
|
+
});
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
if (prevConfigEnv === undefined)
|
|
58
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
59
|
+
else
|
|
60
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
61
|
+
reloadConfig();
|
|
62
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
// ── compact mode ──
|
|
65
|
+
it("returns compact summary in compact mode", () => {
|
|
66
|
+
writeTaskStatus("task-compact-001", baseStatus("task-compact-001", {
|
|
67
|
+
status: "done",
|
|
68
|
+
phase: "completed",
|
|
69
|
+
verify_status: "passed",
|
|
70
|
+
}));
|
|
71
|
+
const result = getTaskSummary("task-compact-001", { view: "compact" });
|
|
72
|
+
assert.equal(result.view, "compact");
|
|
73
|
+
assert.equal(result.task_id, "task-compact-001");
|
|
74
|
+
assert.equal(result.status, "done");
|
|
75
|
+
assert.equal(result.terminal, true);
|
|
76
|
+
// compact-only fields
|
|
77
|
+
assert.equal(typeof result.changed_files_total, "number");
|
|
78
|
+
assert.equal(typeof result.release_artifacts_count, "number");
|
|
79
|
+
assert.ok(result.artifact_hygiene);
|
|
80
|
+
assert.ok("max_items" in result.artifact_hygiene);
|
|
81
|
+
});
|
|
82
|
+
// ── full / standard mode ──
|
|
83
|
+
it("returns full summary in standard mode", () => {
|
|
84
|
+
writeTaskStatus("task-full-001", baseStatus("task-full-001", {
|
|
85
|
+
status: "done",
|
|
86
|
+
phase: "completed",
|
|
87
|
+
verify_status: "passed",
|
|
88
|
+
}));
|
|
89
|
+
const result = getTaskSummary("task-full-001");
|
|
90
|
+
// standard mode has no `view` field
|
|
91
|
+
assert.equal(result.view, undefined);
|
|
92
|
+
assert.equal(result.task_id, "task-full-001");
|
|
93
|
+
assert.equal(result.status, "done");
|
|
94
|
+
assert.equal(result.terminal, true);
|
|
95
|
+
// full-mode-only fields
|
|
96
|
+
assert.ok("log_tails" in result);
|
|
97
|
+
assert.ok("artifacts" in result);
|
|
98
|
+
assert.ok("failed_command_detail" in result);
|
|
99
|
+
assert.ok("acceptance_reviewed_at" in result);
|
|
100
|
+
});
|
|
101
|
+
// ── task not found ──
|
|
102
|
+
it("throws when task does not exist", () => {
|
|
103
|
+
assert.throws(() => getTaskSummary("non-existent-task"), /(File not found|Task not found)/);
|
|
104
|
+
});
|
|
105
|
+
// ── terminal: done ──
|
|
106
|
+
it("returns terminal summary for done task with passed verification", () => {
|
|
107
|
+
writeTaskStatus("task-done-001", baseStatus("task-done-001", {
|
|
108
|
+
status: "done",
|
|
109
|
+
phase: "completed",
|
|
110
|
+
verify_status: "passed",
|
|
111
|
+
}));
|
|
112
|
+
const result = getTaskSummary("task-done-001");
|
|
113
|
+
assert.equal(result.terminal, true);
|
|
114
|
+
assert.equal(result.status, "done");
|
|
115
|
+
assert.equal(result.verify_status, "passed");
|
|
116
|
+
// done + verify passed → ready_for_review
|
|
117
|
+
assert.equal(result.acceptance_status, "ready_for_review");
|
|
118
|
+
});
|
|
119
|
+
// ── terminal: failed ──
|
|
120
|
+
it("returns terminal summary for failed task", () => {
|
|
121
|
+
writeTaskStatus("task-failed-001", baseStatus("task-failed-001", {
|
|
122
|
+
status: "failed",
|
|
123
|
+
phase: "failed",
|
|
124
|
+
error: "Agent exited with code 1",
|
|
125
|
+
}));
|
|
126
|
+
const result = getTaskSummary("task-failed-001");
|
|
127
|
+
assert.equal(result.terminal, true);
|
|
128
|
+
assert.equal(result.status, "failed");
|
|
129
|
+
assert.equal(result.acceptance_status, "failed");
|
|
130
|
+
assert.ok(result.errors.includes("Agent exited with code 1"));
|
|
131
|
+
});
|
|
132
|
+
// ── running (non-terminal) ──
|
|
133
|
+
it("returns non-terminal summary for running task", () => {
|
|
134
|
+
writeTaskStatus("task-running-001", baseStatus("task-running-001", {
|
|
135
|
+
status: "running",
|
|
136
|
+
phase: "executing_agent",
|
|
137
|
+
started_at: "2026-07-12T10:00:00Z",
|
|
138
|
+
finished_at: undefined,
|
|
139
|
+
}));
|
|
140
|
+
const result = getTaskSummary("task-running-001");
|
|
141
|
+
assert.equal(result.terminal, false);
|
|
142
|
+
assert.equal(result.status, "running");
|
|
143
|
+
assert.equal(result.phase, "executing_agent");
|
|
144
|
+
assert.equal(result.acceptance_status, "pending");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { reloadConfig } from "../../config.js";
|
|
7
|
+
import { exportGoalReport } from "../../goal/goalReport.js";
|
|
8
|
+
import { getGoalDir, writeGoalStatus } from "../../goal/goalStore.js";
|
|
9
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
10
|
+
function makeSubgoal(overrides) {
|
|
11
|
+
return {
|
|
12
|
+
title: "Subgoal " + overrides.id,
|
|
13
|
+
status: "ready",
|
|
14
|
+
depends_on: [],
|
|
15
|
+
task_ids: [],
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function makeGoalStatus(subgoals, overrides = {}) {
|
|
20
|
+
const now = "2026-07-10T00:00:00.000Z";
|
|
21
|
+
return {
|
|
22
|
+
goal_id: "goal_test_001",
|
|
23
|
+
title: "Test Goal",
|
|
24
|
+
status: "active",
|
|
25
|
+
repo_path: "/repo/test",
|
|
26
|
+
created_at: now,
|
|
27
|
+
updated_at: now,
|
|
28
|
+
subgoals,
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
let tempDir;
|
|
33
|
+
let prevConfigEnv;
|
|
34
|
+
function writeConfig() {
|
|
35
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
36
|
+
writeFileSync(configPath, JSON.stringify({
|
|
37
|
+
workspaceRoot: tempDir,
|
|
38
|
+
tasksDir: ".patchwarden/tasks",
|
|
39
|
+
plansDir: ".patchwarden/plans",
|
|
40
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
41
|
+
agents: {
|
|
42
|
+
codex: { command: "codex", args: [] },
|
|
43
|
+
opencode: { command: "opencode", args: [] },
|
|
44
|
+
},
|
|
45
|
+
allowedTestCommands: ["npm test", "npm run build"],
|
|
46
|
+
defaultTaskTimeoutSeconds: 30,
|
|
47
|
+
maxTaskTimeoutSeconds: 120,
|
|
48
|
+
}), "utf-8");
|
|
49
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
50
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
51
|
+
reloadConfig();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 创建 goal 目录并写入 goal_status.json(writeGoalStatus 不自动创建目录)。
|
|
55
|
+
*/
|
|
56
|
+
function seedGoalStatus(goalId, status, workspaceRoot) {
|
|
57
|
+
mkdirSync(getGoalDir(goalId, workspaceRoot), { recursive: true });
|
|
58
|
+
writeGoalStatus(goalId, status, workspaceRoot);
|
|
59
|
+
}
|
|
60
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
61
|
+
describe("goalReport", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-goal-report-"));
|
|
64
|
+
writeConfig();
|
|
65
|
+
});
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
if (prevConfigEnv === undefined)
|
|
68
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
69
|
+
else
|
|
70
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
71
|
+
reloadConfig();
|
|
72
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
it("完成 Goal 报告:所有 subgoal accepted,completion_rate=100", () => {
|
|
75
|
+
const goal = makeGoalStatus([
|
|
76
|
+
makeSubgoal({
|
|
77
|
+
id: "subgoal-001",
|
|
78
|
+
title: "Feature A",
|
|
79
|
+
status: "accepted",
|
|
80
|
+
accepted_at: "2026-07-01T00:00:00.000Z",
|
|
81
|
+
}),
|
|
82
|
+
makeSubgoal({
|
|
83
|
+
id: "subgoal-002",
|
|
84
|
+
title: "Feature B",
|
|
85
|
+
status: "accepted",
|
|
86
|
+
accepted_at: "2026-07-02T00:00:00.000Z",
|
|
87
|
+
}),
|
|
88
|
+
], { goal_id: "goal_done_001" });
|
|
89
|
+
seedGoalStatus("goal_done_001", goal, tempDir);
|
|
90
|
+
const report = exportGoalReport("goal_done_001", { workspaceRoot: tempDir });
|
|
91
|
+
assert.equal(report.completion_rate, 100);
|
|
92
|
+
assert.equal(report.goal_id, "goal_done_001");
|
|
93
|
+
assert.equal(report.subgoals.length, 2);
|
|
94
|
+
assert.equal(report.bounded, true);
|
|
95
|
+
assert.ok(existsSync(report.files.report_md));
|
|
96
|
+
assert.ok(existsSync(report.files.report_json));
|
|
97
|
+
});
|
|
98
|
+
it("未完成 Goal 报告:有 running/ready subgoal,报告含 '未完成'", () => {
|
|
99
|
+
const goal = makeGoalStatus([
|
|
100
|
+
makeSubgoal({
|
|
101
|
+
id: "subgoal-001",
|
|
102
|
+
title: "Done",
|
|
103
|
+
status: "accepted",
|
|
104
|
+
accepted_at: "2026-07-01T00:00:00.000Z",
|
|
105
|
+
}),
|
|
106
|
+
makeSubgoal({ id: "subgoal-002", title: "WIP", status: "running" }),
|
|
107
|
+
]);
|
|
108
|
+
seedGoalStatus("goal_wip_001", goal, tempDir);
|
|
109
|
+
const report = exportGoalReport("goal_wip_001", { workspaceRoot: tempDir });
|
|
110
|
+
const md = readFileSync(report.files.report_md, "utf-8");
|
|
111
|
+
assert.ok(md.includes("未完成"), "REPORT.md should contain '未完成' annotation");
|
|
112
|
+
assert.ok(report.risks.some((r) => r.includes("未完成")), "risks array should contain '未完成' entry");
|
|
113
|
+
});
|
|
114
|
+
it("空 Goal 报告:无 subgoal,completion_rate=0,报告含 '无子目标'", () => {
|
|
115
|
+
const goal = makeGoalStatus([]);
|
|
116
|
+
seedGoalStatus("goal_empty_001", goal, tempDir);
|
|
117
|
+
const report = exportGoalReport("goal_empty_001", { workspaceRoot: tempDir });
|
|
118
|
+
assert.equal(report.completion_rate, 0);
|
|
119
|
+
const md = readFileSync(report.files.report_md, "utf-8");
|
|
120
|
+
assert.ok(md.includes("无子目标"), "REPORT.md should contain '无子目标' annotation");
|
|
121
|
+
});
|
|
122
|
+
it("双格式输出:REPORT.md 和 report.json 同时存在", () => {
|
|
123
|
+
const goal = makeGoalStatus([
|
|
124
|
+
makeSubgoal({
|
|
125
|
+
id: "subgoal-001",
|
|
126
|
+
title: "Task A",
|
|
127
|
+
status: "accepted",
|
|
128
|
+
accepted_at: "2026-07-01T00:00:00.000Z",
|
|
129
|
+
}),
|
|
130
|
+
]);
|
|
131
|
+
seedGoalStatus("goal_dual_001", goal, tempDir);
|
|
132
|
+
const report = exportGoalReport("goal_dual_001", { workspaceRoot: tempDir });
|
|
133
|
+
const reportDir = resolve(tempDir, ".patchwarden", "goals", "goal_dual_001", "report");
|
|
134
|
+
const mdPath = join(reportDir, "REPORT.md");
|
|
135
|
+
const jsonPath = join(reportDir, "report.json");
|
|
136
|
+
assert.ok(existsSync(mdPath), "REPORT.md should exist");
|
|
137
|
+
assert.ok(existsSync(jsonPath), "report.json should exist");
|
|
138
|
+
assert.equal(report.files.report_md, mdPath);
|
|
139
|
+
assert.equal(report.files.report_json, jsonPath);
|
|
140
|
+
});
|
|
141
|
+
it("脱敏验证:subgoal title 含敏感信息,输出中不含原始值", () => {
|
|
142
|
+
const goal = makeGoalStatus([
|
|
143
|
+
makeSubgoal({
|
|
144
|
+
id: "subgoal-001",
|
|
145
|
+
title: "Deploy with token=abcd1234",
|
|
146
|
+
status: "accepted",
|
|
147
|
+
accepted_at: "2026-07-01T00:00:00.000Z",
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
seedGoalStatus("goal_redact_001", goal, tempDir);
|
|
151
|
+
const report = exportGoalReport("goal_redact_001", { workspaceRoot: tempDir });
|
|
152
|
+
const md = readFileSync(report.files.report_md, "utf-8");
|
|
153
|
+
const json = readFileSync(report.files.report_json, "utf-8");
|
|
154
|
+
assert.ok(!md.includes("abcd1234"), "REPORT.md must not contain raw secret value");
|
|
155
|
+
assert.ok(!json.includes("abcd1234"), "report.json must not contain raw secret value");
|
|
156
|
+
assert.ok(md.includes("[REDACTED]"), "REPORT.md should contain redaction marker");
|
|
157
|
+
assert.ok(json.includes("[REDACTED]"), "report.json should contain redaction marker");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -11,8 +11,8 @@ import { PatchWardenError } from "../../errors.js";
|
|
|
11
11
|
// - goal_not_found:readGoalStatus 在默认工作区找不到 goal 即抛错
|
|
12
12
|
describe("createSubgoalTask", () => {
|
|
13
13
|
describe("错误路径", () => {
|
|
14
|
-
it("execution_mode=assess_only 抛 invalid_execution_mode", () => {
|
|
15
|
-
assert.
|
|
14
|
+
it("execution_mode=assess_only 抛 invalid_execution_mode", async () => {
|
|
15
|
+
await assert.rejects(() => createSubgoalTask({
|
|
16
16
|
goal_id: "goal_nonexistent_test",
|
|
17
17
|
subgoal_title: "Sub A",
|
|
18
18
|
repo_path: "repo",
|
|
@@ -24,8 +24,8 @@ describe("createSubgoalTask", () => {
|
|
|
24
24
|
return true;
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
|
-
it("goal_id 不存在抛 goal_not_found", () => {
|
|
28
|
-
assert.
|
|
27
|
+
it("goal_id 不存在抛 goal_not_found", async () => {
|
|
28
|
+
await assert.rejects(() => createSubgoalTask({
|
|
29
29
|
goal_id: "goal_definitely_does_not_exist_99999",
|
|
30
30
|
subgoal_title: "Sub A",
|
|
31
31
|
repo_path: "repo",
|
|
@@ -37,10 +37,10 @@ describe("createSubgoalTask", () => {
|
|
|
37
37
|
return true;
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
|
-
it("assess_only 优先于 goal_not_found(在读取 goal 前校验)", () => {
|
|
40
|
+
it("assess_only 优先于 goal_not_found(在读取 goal 前校验)", async () => {
|
|
41
41
|
// execution_mode=assess_only 应先抛 invalid_execution_mode,
|
|
42
42
|
// 而不是去读不存在的 goal
|
|
43
|
-
assert.
|
|
43
|
+
await assert.rejects(() => createSubgoalTask({
|
|
44
44
|
goal_id: "goal_definitely_does_not_exist_99999",
|
|
45
45
|
subgoal_title: "Sub A",
|
|
46
46
|
repo_path: "repo",
|