patchwarden 1.1.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 +83 -8
- package/README.md +78 -10
- 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 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- 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/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -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 +261 -255
- 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.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- 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/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- 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 +45 -0
- package/dist/tools/evidencePack.js +375 -0
- 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/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- 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/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -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 +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -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 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- 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/policy/projectPolicy.ts +344 -0
- 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 +254 -252
- 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 +251 -0
- 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/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- 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 +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join, normalize } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { reloadConfig } from "../../config.js";
|
|
7
|
+
import { runTaskLoopWithDeps } from "../../tools/runTaskLoop.js";
|
|
8
|
+
import { getTaskLineage, writeTaskLineage } from "../../tools/taskLineage.js";
|
|
9
|
+
let tempDir;
|
|
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
|
+
agents: {
|
|
19
|
+
fake: { command: "fake-agent", args: [] },
|
|
20
|
+
},
|
|
21
|
+
allowedTestCommands: ["npm test", "npm run build"],
|
|
22
|
+
directAllowedCommands: ["npm test", "npm run build"],
|
|
23
|
+
defaultTaskTimeoutSeconds: 30,
|
|
24
|
+
maxTaskTimeoutSeconds: 120,
|
|
25
|
+
enableDirectProfile: true,
|
|
26
|
+
}), "utf-8");
|
|
27
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
28
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
29
|
+
reloadConfig();
|
|
30
|
+
}
|
|
31
|
+
function depsFor(options) {
|
|
32
|
+
const decisions = [...(options.decisions || ["allow"])];
|
|
33
|
+
const tasks = [...(options.tasks || ["task-main"])];
|
|
34
|
+
const calls = [];
|
|
35
|
+
const deps = {
|
|
36
|
+
createTask: ((input) => {
|
|
37
|
+
calls.push(input.execution_mode === "assess_only" ? "assess" : "execute");
|
|
38
|
+
if (input.execution_mode === "assess_only") {
|
|
39
|
+
const decision = decisions.shift() || "allow";
|
|
40
|
+
return {
|
|
41
|
+
assessment_id: `assessment-${calls.length}`,
|
|
42
|
+
decision,
|
|
43
|
+
reason_codes: decision === "allow" ? ["repo_scoped"] : ["release_template_needs_confirm"],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { task_id: tasks.shift() || `task-${calls.length}`, status: "pending" };
|
|
47
|
+
}),
|
|
48
|
+
waitForTask: (async (taskId) => ({
|
|
49
|
+
task_id: taskId,
|
|
50
|
+
status: options.statuses?.[taskId] || "done_by_agent",
|
|
51
|
+
phase: options.statuses?.[taskId] || "done_by_agent",
|
|
52
|
+
terminal: true,
|
|
53
|
+
continuation_required: false,
|
|
54
|
+
next_action: "safe_audit",
|
|
55
|
+
})),
|
|
56
|
+
safeResult: ((taskId) => ({
|
|
57
|
+
task_id: taskId,
|
|
58
|
+
status: options.statuses?.[taskId] || "done_by_agent",
|
|
59
|
+
terminal: true,
|
|
60
|
+
verification: { status: options.verifications?.[taskId] || "passed" },
|
|
61
|
+
next_action: "audit_or_accept",
|
|
62
|
+
})),
|
|
63
|
+
safeTestSummary: ((taskId) => ({
|
|
64
|
+
task_id: taskId,
|
|
65
|
+
status: options.verifications?.[taskId] || "passed",
|
|
66
|
+
commands: [{ command: "npm test", status: options.verifications?.[taskId] || "passed", exit_code: 0 }],
|
|
67
|
+
})),
|
|
68
|
+
safeAudit: ((taskId) => {
|
|
69
|
+
const audit = options.audits?.[taskId] || { verdict: "pass" };
|
|
70
|
+
return {
|
|
71
|
+
task_id: taskId,
|
|
72
|
+
verdict: audit.verdict,
|
|
73
|
+
fail_checks: (audit.fail || []).map((name) => ({ name, result: "fail" })),
|
|
74
|
+
warn_checks: (audit.warn || []).map((name) => ({ name, result: "warn" })),
|
|
75
|
+
recommended_next_actions: ["accept"],
|
|
76
|
+
};
|
|
77
|
+
}),
|
|
78
|
+
createDirectSession: ((input) => ({
|
|
79
|
+
session_id: "direct-test",
|
|
80
|
+
repo_path: input.repo_path,
|
|
81
|
+
resolved_repo_path: tempDir,
|
|
82
|
+
workspace_clean: true,
|
|
83
|
+
allowed_commands: ["npm test", "npm run build"],
|
|
84
|
+
expires_at: "2026-07-04T13:00:00.000Z",
|
|
85
|
+
next_action: "run_verification",
|
|
86
|
+
})),
|
|
87
|
+
runDirectVerificationBundle: (async () => ({
|
|
88
|
+
session_id: "direct-test",
|
|
89
|
+
status: options.directBundleStatus || "passed",
|
|
90
|
+
command_count: 1,
|
|
91
|
+
passed_commands: (options.directBundleStatus || "passed") === "passed" ? 1 : 0,
|
|
92
|
+
failed_commands: (options.directBundleStatus || "passed") === "passed" ? 0 : 1,
|
|
93
|
+
timed_out_commands: 0,
|
|
94
|
+
commands: [{
|
|
95
|
+
command: "npm test",
|
|
96
|
+
passed: (options.directBundleStatus || "passed") === "passed",
|
|
97
|
+
exit_code: (options.directBundleStatus || "passed") === "passed" ? 0 : 1,
|
|
98
|
+
timed_out: false,
|
|
99
|
+
redacted: false,
|
|
100
|
+
redaction_categories: [],
|
|
101
|
+
started_at: "2026-07-04T12:00:00.000Z",
|
|
102
|
+
finished_at: "2026-07-04T12:00:01.000Z",
|
|
103
|
+
}],
|
|
104
|
+
large_logs_omitted: true,
|
|
105
|
+
next_action: "safe_finalize_direct_session",
|
|
106
|
+
})),
|
|
107
|
+
safeFinalizeDirectSession: (() => ({
|
|
108
|
+
session_id: "direct-test",
|
|
109
|
+
finalized: true,
|
|
110
|
+
changed_files_total: 0,
|
|
111
|
+
next_action: "safe_audit_direct_session",
|
|
112
|
+
})),
|
|
113
|
+
safeAuditDirectSession: (() => ({
|
|
114
|
+
session_id: "direct-test",
|
|
115
|
+
decision: options.directAuditDecision || "pass",
|
|
116
|
+
reason_codes: [],
|
|
117
|
+
blocking_findings: [],
|
|
118
|
+
warnings: [],
|
|
119
|
+
evidence: { changed_files_total: 0, verification_runs: [] },
|
|
120
|
+
next_action: "accept",
|
|
121
|
+
})),
|
|
122
|
+
writeTaskLineage,
|
|
123
|
+
createLineageId: (() => "lineage_20260704_test"),
|
|
124
|
+
recommendAgentForTask: ((input) => ({
|
|
125
|
+
repo_path: input.repo_path,
|
|
126
|
+
resolved_repo_path: tempDir,
|
|
127
|
+
recommended_agent: "fake",
|
|
128
|
+
fallback_agent: null,
|
|
129
|
+
fallback: false,
|
|
130
|
+
reason: "test route",
|
|
131
|
+
risk_notes: [],
|
|
132
|
+
suggested_verify_commands: ["npm test"],
|
|
133
|
+
bounded: true,
|
|
134
|
+
})),
|
|
135
|
+
createWorktree: ((goalId, subgoalId, workspaceRoot) => {
|
|
136
|
+
calls.push(`worktree:${workspaceRoot}`);
|
|
137
|
+
return {
|
|
138
|
+
worktreeId: "wt-test",
|
|
139
|
+
worktreePath: tempDir,
|
|
140
|
+
branch: "pw-test",
|
|
141
|
+
};
|
|
142
|
+
}),
|
|
143
|
+
now: () => new Date("2026-07-04T12:00:00.000Z"),
|
|
144
|
+
sleep: async () => { },
|
|
145
|
+
};
|
|
146
|
+
return { deps, calls };
|
|
147
|
+
}
|
|
148
|
+
describe("runTaskLoop", () => {
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-loop-"));
|
|
151
|
+
writeConfig();
|
|
152
|
+
});
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
if (prevConfigEnv === undefined)
|
|
155
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
156
|
+
else
|
|
157
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
158
|
+
reloadConfig();
|
|
159
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
160
|
+
});
|
|
161
|
+
it("returns success with a bounded lineage summary", async () => {
|
|
162
|
+
const { deps } = depsFor({});
|
|
163
|
+
const result = await runTaskLoopWithDeps({
|
|
164
|
+
repo_path: ".",
|
|
165
|
+
goal: "Make a safe change",
|
|
166
|
+
agent: "fake",
|
|
167
|
+
verify_commands: ["npm test"],
|
|
168
|
+
}, deps);
|
|
169
|
+
assert.equal(result.stop_reason, "success");
|
|
170
|
+
assert.equal(result.final_status, "accepted");
|
|
171
|
+
assert.equal(result.tasks.main, "task-main");
|
|
172
|
+
const payload = JSON.stringify(result);
|
|
173
|
+
assert.ok(!payload.includes("stdout"));
|
|
174
|
+
assert.ok(!payload.includes("stderr"));
|
|
175
|
+
assert.ok(!payload.includes("diff.patch"));
|
|
176
|
+
});
|
|
177
|
+
it("keeps v1.3 behavior when direct_verify is false", async () => {
|
|
178
|
+
const { deps } = depsFor({});
|
|
179
|
+
const result = await runTaskLoopWithDeps({
|
|
180
|
+
repo_path: ".",
|
|
181
|
+
goal: "Run without Direct",
|
|
182
|
+
agent: "fake",
|
|
183
|
+
verify_commands: ["npm test"],
|
|
184
|
+
direct_verify: false,
|
|
185
|
+
}, deps);
|
|
186
|
+
assert.equal(result.stop_reason, "success");
|
|
187
|
+
assert.equal(result.direct_verify, false);
|
|
188
|
+
assert.deepEqual(result.tasks.direct_sessions, []);
|
|
189
|
+
assert.equal(result.isolation_mode, "current_repo");
|
|
190
|
+
assert.equal(result.worktree.status, "not_used");
|
|
191
|
+
});
|
|
192
|
+
it("records bounded agent routing when agent is auto", async () => {
|
|
193
|
+
const { deps } = depsFor({});
|
|
194
|
+
const result = await runTaskLoopWithDeps({
|
|
195
|
+
repo_path: ".",
|
|
196
|
+
goal: "Pick a safe agent",
|
|
197
|
+
agent: "auto",
|
|
198
|
+
verify_commands: ["npm test"],
|
|
199
|
+
scope_files: ["src/index.ts"],
|
|
200
|
+
}, deps);
|
|
201
|
+
assert.equal(result.stop_reason, "success");
|
|
202
|
+
assert.equal(result.agent_routing?.requested_agent, "auto");
|
|
203
|
+
assert.equal(result.agent_routing?.selected_agent, "fake");
|
|
204
|
+
assert.equal(result.agent_routing?.reason, "test route");
|
|
205
|
+
});
|
|
206
|
+
it("uses worktree isolation for task execution and records worktree evidence", async () => {
|
|
207
|
+
const { deps, calls } = depsFor({});
|
|
208
|
+
const result = await runTaskLoopWithDeps({
|
|
209
|
+
repo_path: "child-repo",
|
|
210
|
+
goal: "Run in a worktree",
|
|
211
|
+
agent: "fake",
|
|
212
|
+
verify_commands: ["npm test"],
|
|
213
|
+
isolation_mode: "worktree",
|
|
214
|
+
worktree_cleanup: "keep",
|
|
215
|
+
}, deps);
|
|
216
|
+
assert.equal(result.stop_reason, "success");
|
|
217
|
+
assert.equal(result.isolation_mode, "worktree");
|
|
218
|
+
assert.equal(result.worktree.worktree_id, "wt-test");
|
|
219
|
+
assert.equal(result.worktree.branch, "pw-test");
|
|
220
|
+
assert.equal(result.worktree.status, "active");
|
|
221
|
+
assert.ok(calls.includes(`worktree:${normalize(join(tempDir, "child-repo"))}`));
|
|
222
|
+
});
|
|
223
|
+
it("records Direct verification evidence when direct_verify succeeds", async () => {
|
|
224
|
+
const { deps } = depsFor({});
|
|
225
|
+
const result = await runTaskLoopWithDeps({
|
|
226
|
+
repo_path: ".",
|
|
227
|
+
goal: "Run with Direct verification",
|
|
228
|
+
agent: "fake",
|
|
229
|
+
verify_commands: ["npm test"],
|
|
230
|
+
direct_verify: true,
|
|
231
|
+
}, deps);
|
|
232
|
+
assert.equal(result.stop_reason, "success");
|
|
233
|
+
assert.equal(result.direct_verify, true);
|
|
234
|
+
assert.equal(result.tasks.direct_sessions.length, 1);
|
|
235
|
+
assert.equal(result.tasks.direct_sessions[0].session_id, "direct-test");
|
|
236
|
+
assert.equal(result.tasks.direct_sessions[0].status, "passed");
|
|
237
|
+
assert.equal(result.tasks.direct_sessions[0].audit_decision, "pass");
|
|
238
|
+
const payload = JSON.stringify(result);
|
|
239
|
+
assert.ok(!payload.includes("stdout_tail"));
|
|
240
|
+
assert.ok(!payload.includes("stderr_tail"));
|
|
241
|
+
assert.ok(!payload.includes("diff.patch"));
|
|
242
|
+
});
|
|
243
|
+
it("stops clearly when direct_verify is requested but Direct profile is disabled", async () => {
|
|
244
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
245
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
246
|
+
raw.enableDirectProfile = false;
|
|
247
|
+
writeFileSync(configPath, JSON.stringify(raw), "utf-8");
|
|
248
|
+
reloadConfig();
|
|
249
|
+
const { deps } = depsFor({});
|
|
250
|
+
const result = await runTaskLoopWithDeps({
|
|
251
|
+
repo_path: ".",
|
|
252
|
+
goal: "Run with unavailable Direct",
|
|
253
|
+
agent: "fake",
|
|
254
|
+
verify_commands: ["npm test"],
|
|
255
|
+
direct_verify: true,
|
|
256
|
+
}, deps);
|
|
257
|
+
assert.equal(result.stop_reason, "direct_profile_disabled");
|
|
258
|
+
assert.equal(result.final_status, "blocked");
|
|
259
|
+
assert.equal(result.tasks.main, "task-main");
|
|
260
|
+
assert.equal(result.tasks.direct_sessions[0].session_id, "not_created");
|
|
261
|
+
});
|
|
262
|
+
it("returns direct_verification_failed without leaking Direct logs", async () => {
|
|
263
|
+
const { deps } = depsFor({ directBundleStatus: "failed" });
|
|
264
|
+
const result = await runTaskLoopWithDeps({
|
|
265
|
+
repo_path: ".",
|
|
266
|
+
goal: "Run with failing Direct verification",
|
|
267
|
+
agent: "fake",
|
|
268
|
+
verify_commands: ["npm test"],
|
|
269
|
+
direct_verify: true,
|
|
270
|
+
}, deps);
|
|
271
|
+
assert.equal(result.stop_reason, "direct_verification_failed");
|
|
272
|
+
assert.equal(result.final_status, "needs_fix");
|
|
273
|
+
assert.equal(result.tasks.direct_sessions[0].failed_commands, 1);
|
|
274
|
+
const payload = JSON.stringify(result);
|
|
275
|
+
assert.ok(!payload.includes("stdout_tail"));
|
|
276
|
+
assert.ok(!payload.includes("stderr_tail"));
|
|
277
|
+
});
|
|
278
|
+
it("returns direct_audit_failed when Direct audit fails", async () => {
|
|
279
|
+
const { deps } = depsFor({ directAuditDecision: "fail" });
|
|
280
|
+
const result = await runTaskLoopWithDeps({
|
|
281
|
+
repo_path: ".",
|
|
282
|
+
goal: "Run with failing Direct audit",
|
|
283
|
+
agent: "fake",
|
|
284
|
+
verify_commands: ["npm test"],
|
|
285
|
+
direct_verify: true,
|
|
286
|
+
}, deps);
|
|
287
|
+
assert.equal(result.stop_reason, "direct_audit_failed");
|
|
288
|
+
assert.equal(result.final_status, "blocked");
|
|
289
|
+
assert.equal(result.tasks.direct_sessions[0].audit_decision, "fail");
|
|
290
|
+
});
|
|
291
|
+
it("creates a fix_tests follow-up after failed verification", async () => {
|
|
292
|
+
const { deps } = depsFor({
|
|
293
|
+
tasks: ["task-main", "task-fix"],
|
|
294
|
+
statuses: { "task-main": "failed_verification", "task-fix": "done_by_agent" },
|
|
295
|
+
verifications: { "task-main": "failed", "task-fix": "passed" },
|
|
296
|
+
audits: {
|
|
297
|
+
"task-main": { verdict: "warn", warn: ["test_exit_code"] },
|
|
298
|
+
"task-fix": { verdict: "pass" },
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
const result = await runTaskLoopWithDeps({
|
|
302
|
+
repo_path: ".",
|
|
303
|
+
goal: "Repair tests",
|
|
304
|
+
agent: "fake",
|
|
305
|
+
verify_commands: ["npm test"],
|
|
306
|
+
max_iterations: 2,
|
|
307
|
+
}, deps);
|
|
308
|
+
assert.equal(result.stop_reason, "success");
|
|
309
|
+
assert.equal(result.tasks.main, "task-main");
|
|
310
|
+
assert.deepEqual(result.tasks.fix, ["task-fix"]);
|
|
311
|
+
assert.equal(result.rounds.length, 2);
|
|
312
|
+
});
|
|
313
|
+
it("stops before execution when assessment needs confirmation", async () => {
|
|
314
|
+
const { deps, calls } = depsFor({ decisions: ["needs_confirm"] });
|
|
315
|
+
const result = await runTaskLoopWithDeps({
|
|
316
|
+
repo_path: ".",
|
|
317
|
+
goal: "Release-like work",
|
|
318
|
+
agent: "fake",
|
|
319
|
+
verify_commands: ["npm test"],
|
|
320
|
+
}, deps);
|
|
321
|
+
assert.equal(result.stop_reason, "user_confirmation_required");
|
|
322
|
+
assert.equal(result.stopped_before_execution, true);
|
|
323
|
+
assert.deepEqual(calls, ["assess"]);
|
|
324
|
+
});
|
|
325
|
+
it("returns max_iterations_reached when verification keeps failing", async () => {
|
|
326
|
+
const { deps } = depsFor({
|
|
327
|
+
statuses: { "task-main": "failed_verification" },
|
|
328
|
+
verifications: { "task-main": "failed" },
|
|
329
|
+
audits: { "task-main": { verdict: "warn", warn: ["test_exit_code"] } },
|
|
330
|
+
});
|
|
331
|
+
const result = await runTaskLoopWithDeps({
|
|
332
|
+
repo_path: ".",
|
|
333
|
+
goal: "Fix tests",
|
|
334
|
+
agent: "fake",
|
|
335
|
+
verify_commands: ["npm test"],
|
|
336
|
+
max_iterations: 1,
|
|
337
|
+
}, deps);
|
|
338
|
+
assert.equal(result.stop_reason, "max_iterations_reached");
|
|
339
|
+
assert.equal(result.final_status, "needs_fix");
|
|
340
|
+
});
|
|
341
|
+
it("writes BOM-free lineage JSON readable through get_task_lineage", async () => {
|
|
342
|
+
const { deps } = depsFor({});
|
|
343
|
+
const result = await runTaskLoopWithDeps({
|
|
344
|
+
repo_path: ".",
|
|
345
|
+
goal: "Persist lineage",
|
|
346
|
+
agent: "fake",
|
|
347
|
+
verify_commands: ["npm test"],
|
|
348
|
+
}, deps);
|
|
349
|
+
const lineagePath = join(tempDir, ".patchwarden", "lineages", result.lineage_id, "lineage.json");
|
|
350
|
+
const raw = readFileSync(lineagePath);
|
|
351
|
+
assert.notEqual(raw[0], 0xef);
|
|
352
|
+
const parsed = JSON.parse(raw.toString("utf-8"));
|
|
353
|
+
assert.equal(parsed.lineage_id, result.lineage_id);
|
|
354
|
+
const safe = getTaskLineage(result.lineage_id);
|
|
355
|
+
assert.equal(safe.lineage_id, result.lineage_id);
|
|
356
|
+
assert.equal(safe.rounds.length, 1);
|
|
357
|
+
});
|
|
358
|
+
it("reads legacy string direct_sessions as bounded evidence", () => {
|
|
359
|
+
const safe = writeTaskLineage({
|
|
360
|
+
lineage_id: "lineage_legacy_direct",
|
|
361
|
+
goal: "Legacy lineage",
|
|
362
|
+
repo_path: ".",
|
|
363
|
+
created_at: "2026-07-04T12:00:00.000Z",
|
|
364
|
+
updated_at: "2026-07-04T12:00:00.000Z",
|
|
365
|
+
final_status: "accepted",
|
|
366
|
+
stop_reason: "success",
|
|
367
|
+
next_action: "accept",
|
|
368
|
+
main_task: "task-main",
|
|
369
|
+
fix_tasks: [],
|
|
370
|
+
cleanup_tasks: [],
|
|
371
|
+
direct_sessions: ["direct-old"],
|
|
372
|
+
rounds: [],
|
|
373
|
+
warnings: [],
|
|
374
|
+
errors: [],
|
|
375
|
+
});
|
|
376
|
+
assert.equal(safe.tasks.direct_sessions[0].session_id, "direct-old");
|
|
377
|
+
const readBack = getTaskLineage("lineage_legacy_direct");
|
|
378
|
+
assert.equal(readBack.tasks.direct_sessions[0].session_id, "direct-old");
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -18,6 +18,7 @@ const OTHER_SCHEMA = {
|
|
|
18
18
|
},
|
|
19
19
|
required: ["plan_id"],
|
|
20
20
|
};
|
|
21
|
+
const CORE_COUNT = CHATGPT_CORE_TOOL_NAMES.length;
|
|
21
22
|
/** 构造一个满足 PatchWardenToolMeta 接口的元数据对象。 */
|
|
22
23
|
function makeMeta(opts) {
|
|
23
24
|
const schema = opts.inputSchema ?? SAMPLE_SCHEMA;
|
|
@@ -100,39 +101,39 @@ describe("checkChatgptCoreManifestStable", () => {
|
|
|
100
101
|
});
|
|
101
102
|
it("缺少工具时 ok=false", () => {
|
|
102
103
|
// 移除最后一个工具,使 chatgpt_core 工具数为 20
|
|
103
|
-
const names = [...CHATGPT_CORE_TOOL_NAMES].slice(0,
|
|
104
|
+
const names = [...CHATGPT_CORE_TOOL_NAMES].slice(0, CORE_COUNT - 1);
|
|
104
105
|
const toolDefs = makeToolDefs(names.map((name) => ({ name })));
|
|
105
106
|
const result = checkChatgptCoreManifestStable(toolDefs);
|
|
106
107
|
assert.equal(result.ok, false);
|
|
107
108
|
assert.equal(result.warnings.length, 1);
|
|
108
109
|
assert.ok(result.warnings[0].includes("missing tools"), "warning 应包含 missing tools");
|
|
109
|
-
assert.ok(result.warnings[0].includes(CHATGPT_CORE_TOOL_NAMES[
|
|
110
|
+
assert.ok(result.warnings[0].includes(CHATGPT_CORE_TOOL_NAMES[CORE_COUNT - 1]), "warning 应包含缺失的工具名");
|
|
110
111
|
});
|
|
111
112
|
});
|
|
112
113
|
// ── 3. checkNewToolsProfileAppend ────────────────────────────────
|
|
113
114
|
describe("checkNewToolsProfileAppend", () => {
|
|
114
115
|
it("chatgpt_core 21 工具时 ok=true", () => {
|
|
115
|
-
const registry = makeCoreRegistry(
|
|
116
|
+
const registry = makeCoreRegistry(CORE_COUNT);
|
|
116
117
|
const result = checkNewToolsProfileAppend(registry);
|
|
117
118
|
assert.equal(result.ok, true);
|
|
118
119
|
assert.deepEqual(result.warnings, []);
|
|
119
120
|
});
|
|
120
121
|
it("chatgpt_core 工具数不为 21 时 ok=false", () => {
|
|
121
122
|
// 22 个 chatgpt_core 工具:21 个标准工具 + 1 个重复名(模拟新工具追加)
|
|
122
|
-
const registry = makeCoreRegistry(
|
|
123
|
+
const registry = makeCoreRegistry(CORE_COUNT);
|
|
123
124
|
registry.push(makeMeta({ name: "extra_tool", profiles: ["chatgpt_core"] }));
|
|
124
125
|
const result = checkNewToolsProfileAppend(registry);
|
|
125
126
|
assert.equal(result.ok, false);
|
|
126
127
|
assert.equal(result.warnings.length, 1);
|
|
127
|
-
assert.ok(result.warnings[0].includes(
|
|
128
|
-
assert.ok(result.warnings[0].includes(
|
|
128
|
+
assert.ok(result.warnings[0].includes(`expected ${CORE_COUNT}`), "warning 应包含预期数量 21");
|
|
129
|
+
assert.ok(result.warnings[0].includes(`got ${CORE_COUNT + 1}`), "warning 应包含实际数量 22");
|
|
129
130
|
});
|
|
130
131
|
});
|
|
131
132
|
// ── 4. runAllSchemaDriftChecks ───────────────────────────────────
|
|
132
133
|
describe("runAllSchemaDriftChecks", () => {
|
|
133
134
|
it("无 drift 时 ok=true", () => {
|
|
134
135
|
// registry 与 toolDefs 完全一致,且 chatgpt_core 工具数为 21
|
|
135
|
-
const registry = makeCoreRegistry(
|
|
136
|
+
const registry = makeCoreRegistry(CORE_COUNT);
|
|
136
137
|
const toolDefs = makeCoreToolDefs();
|
|
137
138
|
const result = runAllSchemaDriftChecks(registry, toolDefs);
|
|
138
139
|
assert.equal(result.ok, true);
|
|
@@ -144,7 +145,7 @@ describe("runAllSchemaDriftChecks", () => {
|
|
|
144
145
|
// - check 2: toolDefs 缺少一个 chatgpt_core 工具(数量 16)
|
|
145
146
|
// - check 3: registry 有 22 个 chatgpt_core 工具
|
|
146
147
|
const driftedSchema = OTHER_SCHEMA;
|
|
147
|
-
const registry = makeCoreRegistry(
|
|
148
|
+
const registry = makeCoreRegistry(CORE_COUNT);
|
|
148
149
|
// 第一个工具的 digest 指向 SAMPLE_SCHEMA,但 toolDefs 中该工具用 driftedSchema
|
|
149
150
|
registry[0] = makeMeta({
|
|
150
151
|
name: CHATGPT_CORE_TOOL_NAMES[0],
|
|
@@ -155,7 +156,7 @@ describe("runAllSchemaDriftChecks", () => {
|
|
|
155
156
|
registry.push(makeMeta({ name: "extra_tool", profiles: ["chatgpt_core"] }));
|
|
156
157
|
// toolDefs:第一个工具用 driftedSchema(触发 check 1),且移除最后一个核心工具(触发 check 2)
|
|
157
158
|
const coreNames = [...CHATGPT_CORE_TOOL_NAMES];
|
|
158
|
-
const toolDefEntries = coreNames.slice(0,
|
|
159
|
+
const toolDefEntries = coreNames.slice(0, CORE_COUNT - 1).map((name) => ({
|
|
159
160
|
name,
|
|
160
161
|
inputSchema: name === CHATGPT_CORE_TOOL_NAMES[0] ? driftedSchema : SAMPLE_SCHEMA,
|
|
161
162
|
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|