patchwarden 0.6.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PatchWarden-Control-Tray.cmd +11 -0
- package/PatchWarden-Control.cmd +6 -0
- package/PatchWarden-Desktop.cmd +5 -0
- package/PatchWarden.cmd +1 -1
- package/README.en.md +112 -24
- package/README.md +36 -25
- package/Restart-PatchWarden-Control.cmd +6 -0
- package/Stop-PatchWarden.cmd +11 -0
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.d.ts +14 -0
- package/dist/controlCenter.js +2015 -0
- package/dist/doctor.js +35 -4
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -0
- package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -0
- package/dist/test/unit/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +175 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +550 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +256 -0
- package/dist/tools/safeViews.js +250 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +33 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +724 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/docs/control-center/README.md +33 -0
- package/docs/control-center/control-center-daily-driver.md +211 -0
- package/docs/control-center/control-center-mvp.md +205 -0
- package/docs/control-center/control-center-phase2.md +159 -0
- package/docs/demo.md +3 -0
- package/docs/release-v0.6.4.md +45 -0
- package/examples/openai-tunnel/README.md +5 -5
- package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
- package/package.json +25 -16
- package/scripts/README.md +47 -0
- package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
- package/scripts/checks/control-center-smoke.js +1098 -0
- package/scripts/{control-smoke.js → checks/control-smoke.js} +19 -5
- package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +2 -1
- package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
- package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +25 -21
- package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +54 -7
- package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +40 -11
- package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
- package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
- package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
- package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +13 -7
- package/scripts/control/control-center-tray.ps1 +281 -0
- package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
- package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
- package/scripts/control/restart-control-center.ps1 +173 -0
- package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
- package/scripts/control/start-control-center.ps1 +263 -0
- package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
- package/scripts/control/stop-patchwarden.ps1 +114 -0
- package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
- package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
- package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
- package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
- package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +2166 -0
- package/src/doctor.ts +40 -5
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +258 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +618 -1
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +271 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/toolCatalog.ts +34 -11
- package/src/tools/toolRegistry.ts +786 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
- package/ui/colors_and_type.css +141 -0
- package/ui/pages/audit.html +743 -0
- package/ui/pages/dashboard.html +1154 -0
- package/ui/pages/direct-sessions.html +652 -0
- package/ui/pages/logs.html +502 -0
- package/ui/pages/task-detail.html +1229 -0
- package/ui/pages/tasks.html +702 -0
- package/ui/pages/workspace.html +947 -0
- package/ui/partials/project-shell.html +362 -0
- package/ui/vendor/lucide.js +12 -0
- package/ui/vendor/tailwindcss-browser.js +947 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.1: toolInvocationGuard — invoke_discovered_tool 调用前强制校验。
|
|
3
|
+
*
|
|
4
|
+
* consumeToken 已完成 token 存在性与过期校验,guard 收到的是已消费的 record。
|
|
5
|
+
* guard 依次执行 8 项校验,任一失败抛出对应错误码:
|
|
6
|
+
* 1. token_tool_mismatch — toolName 与 record.toolName 一致性
|
|
7
|
+
* 2. profile_not_allowed — toolMeta.profiles 必须包含当前 profile
|
|
8
|
+
* 3. risk_exceeded — toolMeta.risk 不能超过 record.risk(schema 漂移拦截)
|
|
9
|
+
* 4. sensitive_path_blocked — workspace_read_sensitive 路径敏感校验
|
|
10
|
+
* 5. assessment_required — workspace_write 需要 assessmentId
|
|
11
|
+
* 6. command_not_allowed — command 工具 args.command 元字符预检
|
|
12
|
+
* 7. release_confirmation_required — release 工具需要 assessmentId
|
|
13
|
+
* 8. credential_sensitive_blocked — credential_sensitive 总是拒绝
|
|
14
|
+
*
|
|
15
|
+
* 加上 consumeToken 内的 token_not_found / token_expired,共 10 项调用前强制校验。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { PatchWardenError } from "../errors.js";
|
|
19
|
+
import { guardSensitivePath } from "./sensitiveGuard.js";
|
|
20
|
+
import { TOOL_RISK_RANK, type PatchWardenToolMeta } from "../tools/toolRegistry.js";
|
|
21
|
+
import type { ToolProfile } from "../tools/toolCatalog.js";
|
|
22
|
+
import type { DiscoveryTokenRecord } from "./discoveryTokenStore.js";
|
|
23
|
+
|
|
24
|
+
// ── 类型定义 ──────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export interface GuardInput {
|
|
27
|
+
toolName: string; // 要调用的工具名
|
|
28
|
+
toolMeta: PatchWardenToolMeta; // 工具元数据
|
|
29
|
+
args: Record<string, unknown>; // 调用参数
|
|
30
|
+
discoveryTokenRecord: DiscoveryTokenRecord; // token 记录(已 consume)
|
|
31
|
+
profile: ToolProfile; // 当前 profile
|
|
32
|
+
assessmentId?: string; // 可选 assessmentId
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface GuardResult {
|
|
36
|
+
allowed: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 错误码常量
|
|
40
|
+
export type InvocationErrorCode =
|
|
41
|
+
| "token_tool_mismatch"
|
|
42
|
+
| "profile_not_allowed"
|
|
43
|
+
| "risk_exceeded"
|
|
44
|
+
| "sensitive_path_blocked"
|
|
45
|
+
| "assessment_required"
|
|
46
|
+
| "command_not_allowed"
|
|
47
|
+
| "release_confirmation_required"
|
|
48
|
+
| "credential_sensitive_blocked";
|
|
49
|
+
|
|
50
|
+
// ── 内部常量 ──────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 常见路径参数字段名,用于 workspace_read_sensitive 校验时扫描 args。
|
|
54
|
+
* 任一字段为字符串路径都会触发 guardSensitivePath。
|
|
55
|
+
*/
|
|
56
|
+
const PATH_ARG_FIELDS = [
|
|
57
|
+
"path",
|
|
58
|
+
"file",
|
|
59
|
+
"filePath",
|
|
60
|
+
"file_path",
|
|
61
|
+
"filename",
|
|
62
|
+
"repo_path",
|
|
63
|
+
"repoPath",
|
|
64
|
+
"target_path",
|
|
65
|
+
"targetPath",
|
|
66
|
+
"destination",
|
|
67
|
+
"dest",
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* shell 元字符黑名单,用于 command 风险工具 args.command 预检。
|
|
72
|
+
* 真正的白名单校验在 handler 内通过 commandGuard 完成,guard 只做元字符预检。
|
|
73
|
+
*/
|
|
74
|
+
const SHELL_METACHAR_PATTERN = /[|&;`$()<>]/;
|
|
75
|
+
|
|
76
|
+
// ── 公共 API ──────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 调用前强制校验。任一校验失败抛 PatchWardenError,全部通过返回 { allowed: true }。
|
|
80
|
+
*/
|
|
81
|
+
export function checkInvocation(input: GuardInput): GuardResult {
|
|
82
|
+
const { toolName, toolMeta, args, discoveryTokenRecord, profile, assessmentId } = input;
|
|
83
|
+
|
|
84
|
+
// ① toolName 一致性:toolName 必须等于 record.toolName
|
|
85
|
+
if (toolName !== discoveryTokenRecord.toolName) {
|
|
86
|
+
throw new PatchWardenError(
|
|
87
|
+
"token_tool_mismatch",
|
|
88
|
+
`Token tool mismatch: token was issued for "${discoveryTokenRecord.toolName}" but invocation targets "${toolName}".`,
|
|
89
|
+
"Call discover_tools again and use a token issued for the intended tool name.",
|
|
90
|
+
true,
|
|
91
|
+
{
|
|
92
|
+
token: discoveryTokenRecord.token,
|
|
93
|
+
expected_tool: discoveryTokenRecord.toolName,
|
|
94
|
+
actual_tool: toolName,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ② profile 允许:toolMeta.profiles 必须包含当前 profile
|
|
100
|
+
if (!toolMeta.profiles.includes(profile)) {
|
|
101
|
+
throw new PatchWardenError(
|
|
102
|
+
"profile_not_allowed",
|
|
103
|
+
`Profile "${profile}" is not allowed for tool "${toolName}". Allowed profiles: ${toolMeta.profiles.join(", ")}.`,
|
|
104
|
+
"Switch to a profile that exposes this tool, or call discover_tools under a supported profile.",
|
|
105
|
+
true,
|
|
106
|
+
{
|
|
107
|
+
tool: toolName,
|
|
108
|
+
current_profile: profile,
|
|
109
|
+
allowed_profiles: toolMeta.profiles,
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ③ 风险等级校验:toolMeta.risk 不能超过 record.risk
|
|
115
|
+
// token record 的 risk 是 discover 时记录的工具 risk,应与 toolMeta.risk 一致。
|
|
116
|
+
// 如果工具 schema 漂移导致 risk 变了,这里会拦截。
|
|
117
|
+
const toolRiskRank = TOOL_RISK_RANK[toolMeta.risk];
|
|
118
|
+
const tokenRiskRank = TOOL_RISK_RANK[discoveryTokenRecord.risk];
|
|
119
|
+
if (toolRiskRank > tokenRiskRank) {
|
|
120
|
+
throw new PatchWardenError(
|
|
121
|
+
"risk_exceeded",
|
|
122
|
+
`Risk exceeded: tool "${toolName}" risk is "${toolMeta.risk}" (rank ${toolRiskRank}) but discovery token was issued for risk "${discoveryTokenRecord.risk}" (rank ${tokenRiskRank}). Tool schema may have drifted.`,
|
|
123
|
+
"Call discover_tools again to obtain a fresh token reflecting the current tool risk.",
|
|
124
|
+
true,
|
|
125
|
+
{
|
|
126
|
+
tool: toolName,
|
|
127
|
+
tool_risk: toolMeta.risk,
|
|
128
|
+
token_risk: discoveryTokenRecord.risk,
|
|
129
|
+
tool_risk_rank: toolRiskRank,
|
|
130
|
+
token_risk_rank: tokenRiskRank,
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ④ workspace_read_sensitive:检查 args 中的路径参数
|
|
136
|
+
checkSensitivePaths(toolMeta, toolName, args);
|
|
137
|
+
|
|
138
|
+
// ⑤ workspace_write:要求 assessmentId 非空
|
|
139
|
+
if (toolMeta.risk === "workspace_write") {
|
|
140
|
+
if (!assessmentId || assessmentId.trim() === "") {
|
|
141
|
+
throw new PatchWardenError(
|
|
142
|
+
"assessment_required",
|
|
143
|
+
`Assessment required: tool "${toolName}" has workspace_write risk and requires a non-empty assessmentId.`,
|
|
144
|
+
"Run the assessment flow first and pass the resulting assessmentId to invoke_discovered_tool.",
|
|
145
|
+
true,
|
|
146
|
+
{
|
|
147
|
+
tool: toolName,
|
|
148
|
+
risk: toolMeta.risk,
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ⑥ command:args.command 元字符预检
|
|
155
|
+
if (toolMeta.risk === "command") {
|
|
156
|
+
checkCommandMetachars(toolMeta, toolName, args);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ⑦ release:要求 assessmentId 非空(二次确认语义)
|
|
160
|
+
if (toolMeta.risk === "release") {
|
|
161
|
+
if (!assessmentId || assessmentId.trim() === "") {
|
|
162
|
+
throw new PatchWardenError(
|
|
163
|
+
"release_confirmation_required",
|
|
164
|
+
`Release confirmation required: tool "${toolName}" has release risk and requires a non-empty assessmentId as explicit confirmation.`,
|
|
165
|
+
"Run the release assessment flow first and pass the resulting assessmentId to confirm the release.",
|
|
166
|
+
true,
|
|
167
|
+
{
|
|
168
|
+
tool: toolName,
|
|
169
|
+
risk: toolMeta.risk,
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ⑧ credential_sensitive:总是拒绝
|
|
176
|
+
if (toolMeta.risk === "credential_sensitive") {
|
|
177
|
+
throw new PatchWardenError(
|
|
178
|
+
"credential_sensitive_blocked",
|
|
179
|
+
`Credential-sensitive tool "${toolName}" cannot be invoked through invoke_discovered_tool.`,
|
|
180
|
+
"Use the dedicated credential-handling tooling outside the dynamic invoke path.",
|
|
181
|
+
true,
|
|
182
|
+
{
|
|
183
|
+
tool: toolName,
|
|
184
|
+
risk: toolMeta.risk,
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { allowed: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── 内部校验函数 ──────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* workspace_read_sensitive 风险工具:扫描 args 中的路径参数字段,
|
|
196
|
+
* 对每个字符串路径调用 guardSensitivePath(敏感路径会抛 sensitive_path_blocked)。
|
|
197
|
+
* 没有路径参数则跳过。
|
|
198
|
+
*/
|
|
199
|
+
function checkSensitivePaths(
|
|
200
|
+
toolMeta: PatchWardenToolMeta,
|
|
201
|
+
toolName: string,
|
|
202
|
+
args: Record<string, unknown>
|
|
203
|
+
): void {
|
|
204
|
+
if (toolMeta.risk !== "workspace_read_sensitive") {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const field of PATH_ARG_FIELDS) {
|
|
209
|
+
const value = args[field];
|
|
210
|
+
if (typeof value === "string" && value.length > 0) {
|
|
211
|
+
// guardSensitivePath 内部会在敏感时抛 PatchWardenError("sensitive_path_blocked", ...)
|
|
212
|
+
guardSensitivePath(value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* command 风险工具:args.command 元字符预检。
|
|
219
|
+
* - args.command 不存在或为空 → 跳过(handler 内部会再校验)
|
|
220
|
+
* - 含 shell 元字符 → 抛 command_not_allowed
|
|
221
|
+
* 真正的白名单校验在 handler 内通过 commandGuard 完成。
|
|
222
|
+
*/
|
|
223
|
+
function checkCommandMetachars(
|
|
224
|
+
_toolMeta: PatchWardenToolMeta,
|
|
225
|
+
toolName: string,
|
|
226
|
+
args: Record<string, unknown>
|
|
227
|
+
): void {
|
|
228
|
+
const command = args.command;
|
|
229
|
+
if (command === undefined || command === null) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (typeof command !== "string") {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (command.trim() === "") {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (SHELL_METACHAR_PATTERN.test(command)) {
|
|
240
|
+
throw new PatchWardenError(
|
|
241
|
+
"command_not_allowed",
|
|
242
|
+
`Command not allowed: tool "${toolName}" args.command contains shell metacharacters. Detected pattern in "${command}".`,
|
|
243
|
+
"Provide a plain command without shell metacharacters (| & ; ` $ ( ) < >). The handler will validate against the command allowlist.",
|
|
244
|
+
true,
|
|
245
|
+
{
|
|
246
|
+
tool: toolName,
|
|
247
|
+
command,
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
package/src/smoke-test.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
26
26
|
import { tmpdir } from "node:os";
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
28
28
|
import { spawnSync } from "node:child_process";
|
|
29
|
-
import { loadConfig, getConfig, reloadConfig } from "./config.js";
|
|
29
|
+
import { loadConfig, getConfig, getTasksDir, reloadConfig } from "./config.js";
|
|
30
30
|
import { savePlan } from "./tools/savePlan.js";
|
|
31
31
|
import { getPlan } from "./tools/getPlan.js";
|
|
32
32
|
import { createTask } from "./tools/createTask.js";
|
|
@@ -592,7 +592,7 @@ test("D8b. tool profiles are exact and schema changes alter the manifest hash",
|
|
|
592
592
|
try {
|
|
593
593
|
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
594
594
|
const fullTools = getToolDefs();
|
|
595
|
-
if (fullTools.length !==
|
|
595
|
+
if (fullTools.length !== 54) throw new Error(`Expected 54 full tools, got ${fullTools.length}`);
|
|
596
596
|
|
|
597
597
|
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", getConfig().enableDirectProfile);
|
|
598
598
|
const names = coreTools.map((tool) => tool.name);
|
|
@@ -1023,6 +1023,11 @@ test("I1. list_tasks returns tasks array", () => {
|
|
|
1023
1023
|
mgmtPlanId = savePlan({ title: "Mgmt Test", content: "# Test" }).plan_id;
|
|
1024
1024
|
mgmtTaskId = createTask({ plan_id: mgmtPlanId, agent: "codex", repo_path: "." }).task_id;
|
|
1025
1025
|
mgmtTaskId2 = createTask({ plan_id: mgmtPlanId, agent: "codex", repo_path: "." }).task_id;
|
|
1026
|
+
const collectingStatusFile = join(getTasksDir(getConfig()), mgmtTaskId2, "status.json");
|
|
1027
|
+
const collectingStatus = JSON.parse(readFileSync(collectingStatusFile, "utf-8"));
|
|
1028
|
+
collectingStatus.status = "collecting_artifacts";
|
|
1029
|
+
collectingStatus.phase = "collecting_artifacts";
|
|
1030
|
+
writeFileSync(collectingStatusFile, JSON.stringify(collectingStatus, null, 2), "utf-8");
|
|
1026
1031
|
const result = listTasks({ limit: 5 });
|
|
1027
1032
|
if (!Array.isArray(result.tasks)) throw new Error("tasks not array");
|
|
1028
1033
|
if (result.tasks.length < 2) throw new Error(`Expected >=2 tasks, got ${result.tasks.length}`);
|
|
@@ -1039,9 +1044,12 @@ test("I2b. list_tasks filters by repo and active status with watcher evidence",
|
|
|
1039
1044
|
if (result.returned !== result.tasks.length || !result.watcher?.status) {
|
|
1040
1045
|
throw new Error(`Missing list_tasks pagination or watcher evidence: ${JSON.stringify(result)}`);
|
|
1041
1046
|
}
|
|
1042
|
-
if (result.tasks.some((task) => !["pending", "running"].includes(task.status) || task.repo_path !== ".")) {
|
|
1047
|
+
if (result.tasks.some((task) => !["pending", "running", "collecting_artifacts"].includes(task.status) || task.repo_path !== ".")) {
|
|
1043
1048
|
throw new Error(`list_tasks active/repo filter mismatch: ${JSON.stringify(result.tasks)}`);
|
|
1044
1049
|
}
|
|
1050
|
+
if (!result.tasks.some((task) => task.task_id === mgmtTaskId2 && task.status === "collecting_artifacts")) {
|
|
1051
|
+
throw new Error(`list_tasks active_only omitted collecting_artifacts task: ${JSON.stringify(result.tasks)}`);
|
|
1052
|
+
}
|
|
1045
1053
|
});
|
|
1046
1054
|
|
|
1047
1055
|
test("I3. cancel_task cancels pending task", () => {
|
|
@@ -1926,10 +1934,10 @@ reloadConfig();
|
|
|
1926
1934
|
|
|
1927
1935
|
let directSessionId = "";
|
|
1928
1936
|
|
|
1929
|
-
test("M1. chatgpt_core still has
|
|
1937
|
+
test("M1. chatgpt_core still has 21 tools", () => {
|
|
1930
1938
|
const tools = getToolDefs();
|
|
1931
1939
|
const coreTools = selectToolsForProfile(tools, "chatgpt_core", true);
|
|
1932
|
-
if (coreTools.length !==
|
|
1940
|
+
if (coreTools.length !== 21) throw new Error(`Expected 21, got ${coreTools.length}`);
|
|
1933
1941
|
if (JSON.stringify(coreTools.map((t) => t.name)) !== JSON.stringify(CHATGPT_CORE_TOOL_NAMES)) {
|
|
1934
1942
|
throw new Error("Tool names mismatch");
|
|
1935
1943
|
}
|
|
@@ -1942,10 +1950,10 @@ test("M2. chatgpt_direct disabled exposes only health_check", () => {
|
|
|
1942
1950
|
if (disabledTools[0].name !== "health_check") throw new Error(`Expected health_check, got ${disabledTools[0].name}`);
|
|
1943
1951
|
});
|
|
1944
1952
|
|
|
1945
|
-
test("M3. chatgpt_direct enabled has
|
|
1953
|
+
test("M3. chatgpt_direct enabled has 13 tools", () => {
|
|
1946
1954
|
const tools = getToolDefs();
|
|
1947
1955
|
const directTools = selectToolsForProfile(tools, "chatgpt_direct", true);
|
|
1948
|
-
if (directTools.length !==
|
|
1956
|
+
if (directTools.length !== 13) throw new Error(`Expected 13, got ${directTools.length}`);
|
|
1949
1957
|
if (JSON.stringify(directTools.map((t) => t.name)) !== JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES)) {
|
|
1950
1958
|
throw new Error("Tool names mismatch");
|
|
1951
1959
|
}
|
package/src/taskRuntime.ts
CHANGED
|
@@ -8,6 +8,23 @@ export interface TaskRuntimeData {
|
|
|
8
8
|
current_command: string | null;
|
|
9
9
|
runner_pid?: number;
|
|
10
10
|
child_pid?: number;
|
|
11
|
+
/**
|
|
12
|
+
* v0.7.0: ISO timestamp when the child process was spawned.
|
|
13
|
+
* Used by diagnose_task to detect PID reuse: if a live PID exists but
|
|
14
|
+
* its start time does not match child_started_at, the OS likely reused the PID.
|
|
15
|
+
*/
|
|
16
|
+
child_started_at?: string;
|
|
17
|
+
/**
|
|
18
|
+
* v0.7.0: ISO timestamp when the task itself started (runner began executing).
|
|
19
|
+
* Distinct from child_started_at, which records when the agent child process spawned.
|
|
20
|
+
*/
|
|
21
|
+
task_started_at?: string;
|
|
22
|
+
/**
|
|
23
|
+
* v0.7.0: Watcher instance ID that picked up and is executing this task.
|
|
24
|
+
* Used by diagnose_task to detect orphaned tasks when the current watcher
|
|
25
|
+
* instance_id differs from the one recorded at task start.
|
|
26
|
+
*/
|
|
27
|
+
watcher_instance_id?: string;
|
|
11
28
|
}
|
|
12
29
|
|
|
13
30
|
export function readTaskRuntime(taskDir: string): Partial<TaskRuntimeData> {
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import {
|
|
4
|
+
evaluateAcceptance,
|
|
5
|
+
verdictToStatus,
|
|
6
|
+
type AcceptanceEvidence,
|
|
7
|
+
} from "../../goal/acceptanceEngine.js";
|
|
8
|
+
import { renderAcceptanceMarkdown } from "../../goal/acceptanceTemplate.js";
|
|
9
|
+
|
|
10
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function makeEvidence(overrides: Partial<AcceptanceEvidence> = {}): AcceptanceEvidence {
|
|
13
|
+
return {
|
|
14
|
+
task_id: "task_test_001",
|
|
15
|
+
task_status: "done_by_agent",
|
|
16
|
+
result_md_exists: true,
|
|
17
|
+
result_json_exists: true,
|
|
18
|
+
verify_json_exists: true,
|
|
19
|
+
test_log_exists: true,
|
|
20
|
+
git_diff_exists: true,
|
|
21
|
+
verify_status: "passed",
|
|
22
|
+
new_out_of_scope_changes: [],
|
|
23
|
+
goal: null,
|
|
24
|
+
scope: null,
|
|
25
|
+
forbidden: null,
|
|
26
|
+
verification: null,
|
|
27
|
+
done_evidence: null,
|
|
28
|
+
artifact_status: "collected",
|
|
29
|
+
release_claims_unverified: false,
|
|
30
|
+
checks: [
|
|
31
|
+
{ name: "task_status", result: "pass", detail: "Task status is done_by_agent." },
|
|
32
|
+
{ name: "result_md_exists", result: "pass", detail: "result.md found." },
|
|
33
|
+
{ name: "verify_status", result: "pass", detail: "Verification passed." },
|
|
34
|
+
],
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe("acceptanceEngine", () => {
|
|
42
|
+
|
|
43
|
+
describe("evaluateAcceptance — 基础场景", () => {
|
|
44
|
+
|
|
45
|
+
it("所有检查通过 → ACCEPTED", () => {
|
|
46
|
+
const evidence = makeEvidence();
|
|
47
|
+
const result = evaluateAcceptance(evidence);
|
|
48
|
+
assert.equal(result.verdict, "ACCEPTED");
|
|
49
|
+
assert.equal(result.acceptance_status, "accepted");
|
|
50
|
+
assert.equal(result.fail_checks.length, 0);
|
|
51
|
+
assert.equal(result.warn_checks.length, 0);
|
|
52
|
+
assert.ok(result.reason.includes("All acceptance checks passed"));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("存在 fail 级检查项 → REJECTED", () => {
|
|
56
|
+
const evidence = makeEvidence({
|
|
57
|
+
checks: [
|
|
58
|
+
{ name: "task_status", result: "pass", detail: "ok" },
|
|
59
|
+
{ name: "result_md_exists", result: "fail", detail: "result.md is missing." },
|
|
60
|
+
{ name: "verify_status", result: "pass", detail: "ok" },
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
const result = evaluateAcceptance(evidence);
|
|
64
|
+
assert.equal(result.verdict, "REJECTED");
|
|
65
|
+
assert.equal(result.acceptance_status, "rejected");
|
|
66
|
+
assert.equal(result.fail_checks.length, 1);
|
|
67
|
+
assert.equal(result.fail_checks[0].name, "result_md_exists");
|
|
68
|
+
assert.ok(result.reason.includes("fail-level"));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("存在 warn 级检查项(无 fail)→ NEEDS_FIX", () => {
|
|
72
|
+
const evidence = makeEvidence({
|
|
73
|
+
checks: [
|
|
74
|
+
{ name: "task_status", result: "pass", detail: "ok" },
|
|
75
|
+
{ name: "git_diff_exists", result: "warn", detail: "git.diff is missing." },
|
|
76
|
+
{ name: "test_log_exists", result: "warn", detail: "test.log is missing." },
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
const result = evaluateAcceptance(evidence);
|
|
80
|
+
assert.equal(result.verdict, "NEEDS_FIX");
|
|
81
|
+
assert.equal(result.acceptance_status, "needs_fix");
|
|
82
|
+
assert.equal(result.warn_checks.length, 2);
|
|
83
|
+
assert.ok(result.reason.includes("warn-level"));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("存在 release claims 但无法验证 → BLOCKED_BY_APPROVAL(即使有 warn)", () => {
|
|
87
|
+
const evidence = makeEvidence({
|
|
88
|
+
release_claims_unverified: true,
|
|
89
|
+
checks: [
|
|
90
|
+
{ name: "task_status", result: "pass", detail: "ok" },
|
|
91
|
+
{ name: "release_claims_unverified", result: "warn", detail: "Found release claims." },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
const result = evaluateAcceptance(evidence);
|
|
95
|
+
assert.equal(result.verdict, "BLOCKED_BY_APPROVAL");
|
|
96
|
+
assert.equal(result.acceptance_status, "blocked");
|
|
97
|
+
assert.ok(result.reason.includes("Release claims"));
|
|
98
|
+
assert.ok(result.required_evidence.some((e) => e.includes("Verify remote")));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("fail 级检查项优先于 release claims", () => {
|
|
102
|
+
const evidence = makeEvidence({
|
|
103
|
+
release_claims_unverified: true,
|
|
104
|
+
checks: [
|
|
105
|
+
{ name: "task_status", result: "fail", detail: "Task failed." },
|
|
106
|
+
{ name: "release_claims_unverified", result: "warn", detail: "Found release claims." },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
const result = evaluateAcceptance(evidence);
|
|
110
|
+
assert.equal(result.verdict, "REJECTED");
|
|
111
|
+
assert.equal(result.acceptance_status, "rejected");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("多个 fail 级检查项全部收集", () => {
|
|
115
|
+
const evidence = makeEvidence({
|
|
116
|
+
checks: [
|
|
117
|
+
{ name: "result_md_exists", result: "fail", detail: "missing" },
|
|
118
|
+
{ name: "verify_status", result: "fail", detail: "failed" },
|
|
119
|
+
{ name: "scope_changes", result: "fail", detail: "out of scope" },
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
const result = evaluateAcceptance(evidence);
|
|
123
|
+
assert.equal(result.verdict, "REJECTED");
|
|
124
|
+
assert.equal(result.fail_checks.length, 3);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("verdictToStatus", () => {
|
|
129
|
+
it("ACCEPTED → accepted", () => {
|
|
130
|
+
assert.equal(verdictToStatus("ACCEPTED"), "accepted");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("REJECTED → rejected", () => {
|
|
134
|
+
assert.equal(verdictToStatus("REJECTED"), "rejected");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("NEEDS_FIX → needs_fix", () => {
|
|
138
|
+
assert.equal(verdictToStatus("NEEDS_FIX"), "needs_fix");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("BLOCKED_BY_APPROVAL → blocked", () => {
|
|
142
|
+
assert.equal(verdictToStatus("BLOCKED_BY_APPROVAL"), "blocked");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("evaluateAcceptance — next_suggested_task", () => {
|
|
147
|
+
it("ACCEPTED 的 next_suggested_task 包含 'accepted'", () => {
|
|
148
|
+
const result = evaluateAcceptance(makeEvidence());
|
|
149
|
+
assert.ok(result.next_suggested_task.includes("accepted"));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("REJECTED 的 next_suggested_task 包含 fail 检查名", () => {
|
|
153
|
+
const result = evaluateAcceptance(makeEvidence({
|
|
154
|
+
checks: [{ name: "result_md_exists", result: "fail", detail: "missing" }],
|
|
155
|
+
}));
|
|
156
|
+
assert.ok(result.next_suggested_task.includes("result_md_exists"));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("NEEDS_FIX 的 next_suggested_task 包含 warn 检查名", () => {
|
|
160
|
+
const result = evaluateAcceptance(makeEvidence({
|
|
161
|
+
checks: [{ name: "git_diff_exists", result: "warn", detail: "missing" }],
|
|
162
|
+
}));
|
|
163
|
+
assert.ok(result.next_suggested_task.includes("git_diff_exists"));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("BLOCKED_BY_APPROVAL 的 next_suggested_task 包含 'verify'", () => {
|
|
167
|
+
const result = evaluateAcceptance(makeEvidence({ release_claims_unverified: true }));
|
|
168
|
+
assert.ok(result.next_suggested_task.toLowerCase().includes("verify"));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("evaluateAcceptance — reasons 可追溯", () => {
|
|
173
|
+
it("REJECTED 的 reasons 包含 [FAIL] 前缀", () => {
|
|
174
|
+
const result = evaluateAcceptance(makeEvidence({
|
|
175
|
+
checks: [{ name: "result_md_exists", result: "fail", detail: "missing" }],
|
|
176
|
+
}));
|
|
177
|
+
assert.ok(result.reasons.some((r) => r.includes("[FAIL]")));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("NEEDS_FIX 的 reasons 包含 [WARN] 前缀", () => {
|
|
181
|
+
const result = evaluateAcceptance(makeEvidence({
|
|
182
|
+
checks: [{ name: "git_diff_exists", result: "warn", detail: "missing" }],
|
|
183
|
+
}));
|
|
184
|
+
assert.ok(result.reasons.some((r) => r.includes("[WARN]")));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("BLOCKED_BY_APPROVAL 的 reasons 包含 [BLOCKED] 前缀", () => {
|
|
188
|
+
const result = evaluateAcceptance(makeEvidence({ release_claims_unverified: true }));
|
|
189
|
+
assert.ok(result.reasons.some((r) => r.includes("[BLOCKED]")));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("ACCEPTED 的 reasons 非空", () => {
|
|
193
|
+
const result = evaluateAcceptance(makeEvidence());
|
|
194
|
+
assert.ok(result.reasons.length > 0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("renderAcceptanceMarkdown", () => {
|
|
199
|
+
it("生成包含 verdict 的 ACCEPTANCE.md 内容", () => {
|
|
200
|
+
const evidence = makeEvidence();
|
|
201
|
+
const result = evaluateAcceptance(evidence);
|
|
202
|
+
const md = renderAcceptanceMarkdown("task_test_001", result, evidence);
|
|
203
|
+
assert.ok(md.includes("# Acceptance Report — task_test_001"));
|
|
204
|
+
assert.ok(md.includes("**ACCEPTED**"));
|
|
205
|
+
assert.ok(md.includes("## Evidence Summary"));
|
|
206
|
+
assert.ok(md.includes("## Verdict"));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("REJECTED 的 Markdown 包含 fail 检查", () => {
|
|
210
|
+
const evidence = makeEvidence({
|
|
211
|
+
checks: [{ name: "result_md_exists", result: "fail", detail: "missing" }],
|
|
212
|
+
});
|
|
213
|
+
const result = evaluateAcceptance(evidence);
|
|
214
|
+
const md = renderAcceptanceMarkdown("task_test_002", result, evidence);
|
|
215
|
+
assert.ok(md.includes("**REJECTED**"));
|
|
216
|
+
assert.ok(md.includes("## Fail-Level Checks"));
|
|
217
|
+
assert.ok(md.includes("result_md_exists"));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("NEEDS_FIX 的 Markdown 包含 warn 检查", () => {
|
|
221
|
+
const evidence = makeEvidence({
|
|
222
|
+
checks: [{ name: "git_diff_exists", result: "warn", detail: "missing" }],
|
|
223
|
+
});
|
|
224
|
+
const result = evaluateAcceptance(evidence);
|
|
225
|
+
const md = renderAcceptanceMarkdown("task_test_003", result, evidence);
|
|
226
|
+
assert.ok(md.includes("**NEEDS_FIX**"));
|
|
227
|
+
assert.ok(md.includes("## Warn-Level Checks"));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("BLOCKED 的 Markdown 包含 required_evidence", () => {
|
|
231
|
+
const evidence = makeEvidence({ release_claims_unverified: true });
|
|
232
|
+
const result = evaluateAcceptance(evidence);
|
|
233
|
+
const md = renderAcceptanceMarkdown("task_test_004", result, evidence);
|
|
234
|
+
assert.ok(md.includes("**BLOCKED_BY_APPROVAL**"));
|
|
235
|
+
assert.ok(md.includes("## Required Evidence"));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("有 goal/scope 时显示 Task Acceptance Criteria", () => {
|
|
239
|
+
const evidence = makeEvidence({
|
|
240
|
+
goal: "Implement feature X",
|
|
241
|
+
scope: ["src/feature.ts"],
|
|
242
|
+
forbidden: [".env"],
|
|
243
|
+
verification: ["npm test"],
|
|
244
|
+
done_evidence: ["result.md", "test.log"],
|
|
245
|
+
});
|
|
246
|
+
const result = evaluateAcceptance(evidence);
|
|
247
|
+
const md = renderAcceptanceMarkdown("task_test_005", result, evidence);
|
|
248
|
+
assert.ok(md.includes("## Task Acceptance Criteria"));
|
|
249
|
+
assert.ok(md.includes("Implement feature X"));
|
|
250
|
+
assert.ok(md.includes("src/feature.ts"));
|
|
251
|
+
assert.ok(md.includes(".env"));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("无 goal/scope 时不显示 Task Acceptance Criteria section", () => {
|
|
255
|
+
const evidence = makeEvidence();
|
|
256
|
+
const result = evaluateAcceptance(evidence);
|
|
257
|
+
const md = renderAcceptanceMarkdown("task_test_006", result, evidence);
|
|
258
|
+
assert.ok(!md.includes("## Task Acceptance Criteria"));
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|