patchwarden 1.5.0 → 1.6.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/PatchWarden.cmd +32 -2
- package/README.en.md +194 -26
- package/README.md +160 -22
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +24 -15
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +44 -36
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +22 -0
- package/dist/config.js +98 -11
- package/dist/control/fileManager.d.ts +2 -0
- package/dist/control/fileManager.js +39 -0
- package/dist/control/middleware/auth.d.ts +16 -0
- package/dist/control/middleware/auth.js +17 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +78 -0
- package/dist/control/routeTable.d.ts +24 -0
- package/dist/control/routeTable.js +263 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +287 -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 +12 -0
- package/dist/control/routes/process.js +487 -0
- package/dist/control/routes/sessions.d.ts +30 -0
- package/dist/control/routes/sessions.js +287 -0
- package/dist/control/routes/status.d.ts +51 -0
- package/dist/control/routes/status.js +307 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +197 -0
- package/dist/control/routes/tasks.d.ts +24 -0
- package/dist/control/routes/tasks.js +309 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +220 -0
- package/dist/control/runtime.d.ts +92 -0
- package/dist/control/runtime.js +425 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +150 -0
- package/dist/control/shared.d.ts +59 -0
- package/dist/control/shared.js +341 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directAudit.js +234 -225
- package/dist/direct/directGuards.d.ts +2 -1
- package/dist/direct/directGuards.js +72 -37
- package/dist/direct/directPatch.d.ts +6 -1
- package/dist/direct/directPatch.js +55 -8
- package/dist/direct/directSessionStore.d.ts +16 -6
- package/dist/direct/directSessionStore.js +127 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +614 -373
- package/dist/goal/acceptanceEngine.d.ts +1 -1
- package/dist/goal/goalProgress.js +75 -69
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +197 -0
- package/dist/goal/goalStatus.d.ts +8 -0
- package/dist/goal/goalStatus.js +10 -3
- package/dist/goal/goalStore.d.ts +9 -2
- package/dist/goal/goalStore.js +152 -45
- package/dist/goal/handoffExport.js +6 -6
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +221 -0
- package/dist/goal/subgoalSync.js +13 -10
- package/dist/goal/worktreeManager.d.ts +2 -0
- package/dist/goal/worktreeManager.js +209 -136
- package/dist/httpServer.js +126 -44
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +13 -3
- package/dist/policy/projectPolicy.js +5 -2
- package/dist/release/releaseGate.d.ts +1 -1
- package/dist/release/releaseGate.js +78 -25
- package/dist/runner/agentInvocation.d.ts +14 -0
- package/dist/runner/agentInvocation.js +96 -4
- package/dist/runner/changeCapture.d.ts +14 -6
- package/dist/runner/changeCapture.js +287 -81
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +60 -7
- package/dist/runner/processSecurity.d.ts +56 -0
- package/dist/runner/processSecurity.js +373 -0
- package/dist/runner/runTask.d.ts +1 -1
- package/dist/runner/runTask.js +339 -287
- package/dist/runner/simpleProcess.d.ts +3 -0
- package/dist/runner/simpleProcess.js +52 -38
- package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
- package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
- package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
- package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
- package/dist/runner/taskStatusStore.d.ts +11 -0
- package/dist/runner/taskStatusStore.js +24 -0
- package/dist/runner/watch.d.ts +14 -1
- package/dist/runner/watch.js +265 -54
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +24 -2
- package/dist/security/discoveryTokenStore.d.ts +3 -2
- package/dist/security/discoveryTokenStore.js +17 -2
- package/dist/security/loopbackHost.d.ts +2 -0
- package/dist/security/loopbackHost.js +16 -0
- package/dist/security/pathGuard.js +32 -17
- package/dist/security/planGuard.js +2 -1
- package/dist/security/runtimeGuard.js +9 -6
- package/dist/security/secretComparison.d.ts +3 -0
- package/dist/security/secretComparison.js +11 -0
- package/dist/security/sensitiveGuard.d.ts +5 -0
- package/dist/security/sensitiveGuard.js +27 -8
- package/dist/security/toolInvocationGuard.d.ts +2 -2
- package/dist/security/toolInvocationGuard.js +1 -1
- package/dist/security/workspaceRootGuard.d.ts +8 -0
- package/dist/security/workspaceRootGuard.js +47 -0
- package/dist/smoke-test.js +280 -273
- package/dist/tools/catalog/index.d.ts +4 -0
- package/dist/tools/catalog/index.js +4 -0
- package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
- package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
- package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
- package/dist/tools/definitions/toolDefs.d.ts +18 -0
- package/dist/tools/definitions/toolDefs.js +1177 -0
- package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
- package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
- package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
- package/dist/tools/diagnostics/index.d.ts +6 -0
- package/dist/tools/diagnostics/index.js +6 -0
- package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
- package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
- package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
- package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
- package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
- package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
- package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
- package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
- package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
- package/dist/tools/direct/finalizeDirectSession.js +86 -0
- package/dist/tools/direct/index.d.ts +3 -0
- package/dist/tools/direct/index.js +3 -0
- package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
- package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
- package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
- package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
- package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
- package/dist/tools/discovery/index.d.ts +3 -0
- package/dist/tools/discovery/index.js +3 -0
- package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
- package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +283 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +116 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +92 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +46 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/dispatch/validation.d.ts +7 -0
- package/dist/tools/dispatch/validation.js +71 -0
- package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
- package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
- package/dist/tools/goals/goalSubgoalTask.js +118 -0
- package/dist/tools/goals/index.d.ts +3 -0
- package/dist/tools/goals/index.js +3 -0
- package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
- package/dist/tools/registry.d.ts +10 -14
- package/dist/tools/registry.js +47 -1651
- package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
- package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
- package/dist/tools/release/index.d.ts +2 -0
- package/dist/tools/release/index.js +2 -0
- package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
- package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
- package/dist/tools/tasks/cancelTask.d.ts +11 -0
- package/dist/tools/tasks/cancelTask.js +85 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
- package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
- package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
- package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
- package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
- package/dist/tools/tasks/evidencePack.js +387 -0
- package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
- package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
- package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
- package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
- package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
- package/dist/tools/tasks/index.d.ts +18 -0
- package/dist/tools/tasks/index.js +18 -0
- package/dist/tools/tasks/killTask.d.ts +1 -0
- package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
- package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
- package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
- package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
- package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
- package/dist/tools/tasks/retryTask.js +60 -0
- package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
- package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
- package/dist/tools/tasks/runVerification.js +38 -0
- package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
- package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
- package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
- package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
- package/dist/tools/workspace/applyPatch.js +53 -0
- package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
- package/dist/tools/workspace/index.d.ts +10 -0
- package/dist/tools/workspace/index.js +10 -0
- package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
- package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
- package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
- package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
- package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
- package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
- package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
- package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
- package/dist/tools/workspace/syncFile.js +134 -0
- package/dist/utils/atomicFile.d.ts +7 -0
- package/dist/utils/atomicFile.js +48 -0
- package/dist/utils/boundedFile.d.ts +8 -0
- package/dist/utils/boundedFile.js +93 -0
- package/dist/utils/lockedJsonFile.d.ts +15 -0
- package/dist/utils/lockedJsonFile.js +224 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +26 -0
- package/dist/utils/stableJson.d.ts +2 -0
- package/dist/utils/stableJson.js +12 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.js +3 -2
- package/dist/watcherStatus.d.ts +2 -2
- package/dist/watcherStatus.js +18 -0
- package/docs/CODE_WIKI.md +1648 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +23 -0
- package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/control-center/README.md +4 -4
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/desktop-app.md +175 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +168 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +82 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +97 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/examples/config.example.json +4 -2
- package/package.json +28 -11
- package/scripts/README.md +15 -6
- package/scripts/build.js +11 -0
- package/scripts/checks/brand-check.js +3 -0
- package/scripts/checks/build-output-check.js +37 -0
- package/scripts/checks/control-center-smoke.js +447 -6
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +39 -33
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +30 -4
- package/scripts/checks/package-manifest-check.js +20 -7
- package/scripts/checks/unit-tests.js +3 -5
- package/scripts/checks/watcher-supervisor-smoke.js +9 -0
- package/scripts/control/get-patchwarden-health.ps1 +1 -1
- package/scripts/control/manage-patchwarden.ps1 +69 -4
- package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
- package/scripts/control/run-background-supervisor.ps1 +41 -0
- package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
- package/scripts/generate-demo-gif.py +320 -0
- package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
- package/scripts/launchers/PatchWarden-Control.cmd +6 -0
- package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
- package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
- package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
- package/scripts/lib/clean-generated-output.js +11 -0
- package/scripts/lib/file-discovery.js +12 -0
- package/scripts/release/desktop-preflight.js +211 -0
- package/scripts/release/pack-clean.js +59 -14
- package/src/assessments/agentAssessor.ts +29 -15
- package/src/assessments/assessmentStore.ts +55 -43
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +105 -11
- package/src/control/fileManager.ts +40 -0
- package/src/control/middleware/auth.ts +35 -0
- package/src/control/middleware/static.ts +84 -0
- package/src/control/routeTable.ts +301 -0
- package/src/control/routes/audit.ts +329 -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 +505 -0
- package/src/control/routes/sessions.ts +314 -0
- package/src/control/routes/status.ts +405 -0
- package/src/control/routes/taskActions.ts +214 -0
- package/src/control/routes/tasks.ts +331 -0
- package/src/control/routes/workspace.ts +234 -0
- package/src/control/runtime.ts +508 -0
- package/src/control/server.ts +165 -0
- package/src/control/shared.ts +360 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directAudit.ts +17 -9
- package/src/direct/directGuards.ts +100 -51
- package/src/direct/directPatch.ts +95 -8
- package/src/direct/directSessionStore.ts +211 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +766 -492
- package/src/goal/acceptanceEngine.ts +1 -1
- package/src/goal/goalProgress.ts +17 -13
- package/src/goal/goalReport.ts +263 -0
- package/src/goal/goalStatus.ts +25 -3
- package/src/goal/goalStore.ts +241 -54
- package/src/goal/handoffExport.ts +6 -6
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +15 -10
- package/src/goal/worktreeManager.ts +139 -22
- package/src/httpServer.ts +130 -44
- package/src/index.ts +7 -4
- package/src/logging.ts +20 -4
- package/src/policy/projectPolicy.ts +5 -2
- package/src/release/releaseGate.ts +74 -27
- package/src/runner/agentInvocation.ts +126 -4
- package/src/runner/changeCapture.ts +342 -80
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +55 -7
- package/src/runner/processSecurity.ts +433 -0
- package/src/runner/runTask.ts +441 -292
- package/src/runner/simpleProcess.ts +68 -33
- package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
- package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
- package/src/runner/taskStatusStore.ts +54 -0
- package/src/runner/watch.ts +290 -57
- package/src/security/contentRedaction.ts +31 -2
- package/src/security/discoveryTokenStore.ts +18 -4
- package/src/security/loopbackHost.ts +19 -0
- package/src/security/pathGuard.ts +35 -16
- package/src/security/planGuard.ts +2 -1
- package/src/security/runtimeGuard.ts +9 -6
- package/src/security/secretComparison.ts +13 -0
- package/src/security/sensitiveGuard.ts +27 -8
- package/src/security/toolInvocationGuard.ts +2 -2
- package/src/security/workspaceRootGuard.ts +53 -0
- package/src/smoke-test.ts +276 -273
- package/src/tools/catalog/index.ts +4 -0
- package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
- package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
- package/src/tools/definitions/toolDefs.ts +1264 -0
- package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
- package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
- package/src/tools/diagnostics/index.ts +6 -0
- package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
- package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
- package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
- package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
- package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
- package/src/tools/direct/index.ts +3 -0
- package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
- package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
- package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
- package/src/tools/discovery/index.ts +3 -0
- package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
- package/src/tools/dispatch/coreDispatch.ts +379 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
- package/src/tools/dispatch/directDispatch.ts +168 -0
- package/src/tools/dispatch/goalDispatch.ts +128 -0
- package/src/tools/dispatch/releaseDispatch.ts +66 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/dispatch/validation.ts +78 -0
- package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
- package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
- package/src/tools/goals/index.ts +3 -0
- package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
- package/src/tools/registry.ts +54 -1858
- package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
- package/src/tools/release/index.ts +2 -0
- package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
- package/src/tools/tasks/cancelTask.ts +109 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
- package/src/tools/tasks/evidencePack.ts +504 -0
- package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
- package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
- package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
- package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
- package/src/tools/tasks/index.ts +18 -0
- package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
- package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
- package/src/tools/tasks/retryTask.ts +69 -0
- package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
- package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
- package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
- package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
- package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
- package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
- package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
- package/src/tools/workspace/index.ts +10 -0
- package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
- package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
- package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
- package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
- package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
- package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
- package/src/tools/workspace/syncFile.ts +292 -0
- package/src/utils/atomicFile.ts +58 -0
- package/src/utils/boundedFile.ts +116 -0
- package/src/utils/lockedJsonFile.ts +251 -0
- package/src/utils/platform.ts +31 -0
- package/src/utils/stableJson.ts +12 -0
- package/src/version.ts +3 -2
- package/src/watcherStatus.ts +19 -1
- package/ui/desktop-bootstrap.js +11 -0
- package/ui/desktop-bridge.js +36 -0
- package/ui/desktop.css +160 -0
- package/ui/getting-started.js +93 -0
- package/ui/i18n.js +484 -0
- package/ui/log-parser.js +30 -0
- package/ui/pages/audit.html +204 -3
- package/ui/pages/dashboard.html +937 -79
- package/ui/pages/direct-sessions.html +609 -60
- package/ui/pages/getting-started.html +43 -0
- package/ui/pages/logs.html +105 -22
- package/ui/pages/settings.html +101 -0
- package/ui/pages/task-detail.html +468 -438
- package/ui/pages/tasks.html +648 -65
- package/ui/pages/workspace.html +38 -1
- package/ui/settings.js +245 -0
- package/PatchWarden-Control.cmd +0 -6
- package/Restart-PatchWarden-Control.cmd +0 -6
- package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
- package/dist/test/unit/acceptance-engine.test.js +0 -228
- package/dist/test/unit/agent-router.test.d.ts +0 -1
- package/dist/test/unit/agent-router.test.js +0 -287
- package/dist/test/unit/android-doctor.test.d.ts +0 -1
- package/dist/test/unit/android-doctor.test.js +0 -118
- package/dist/test/unit/audit-checks.test.d.ts +0 -1
- package/dist/test/unit/audit-checks.test.js +0 -350
- package/dist/test/unit/chinese-path.test.d.ts +0 -1
- package/dist/test/unit/chinese-path.test.js +0 -91
- package/dist/test/unit/command-guard.test.d.ts +0 -1
- package/dist/test/unit/command-guard.test.js +0 -160
- package/dist/test/unit/diagnose-task.test.d.ts +0 -1
- package/dist/test/unit/diagnose-task.test.js +0 -457
- package/dist/test/unit/direct-guards.test.d.ts +0 -1
- package/dist/test/unit/direct-guards.test.js +0 -213
- package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
- package/dist/test/unit/discovery-token-store.test.js +0 -139
- package/dist/test/unit/evidence-pack.test.d.ts +0 -1
- package/dist/test/unit/evidence-pack.test.js +0 -130
- package/dist/test/unit/goal-graph.test.d.ts +0 -1
- package/dist/test/unit/goal-graph.test.js +0 -298
- package/dist/test/unit/goal-progress.test.d.ts +0 -1
- package/dist/test/unit/goal-progress.test.js +0 -381
- package/dist/test/unit/goal-status.test.d.ts +0 -1
- package/dist/test/unit/goal-status.test.js +0 -215
- package/dist/test/unit/goal-store.test.d.ts +0 -1
- package/dist/test/unit/goal-store.test.js +0 -253
- package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
- package/dist/test/unit/goal-subgoal-task.test.js +0 -55
- package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
- package/dist/test/unit/goal-tools-registry.test.js +0 -190
- package/dist/test/unit/handoff-export.test.d.ts +0 -1
- package/dist/test/unit/handoff-export.test.js +0 -263
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
- package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
- package/dist/test/unit/logging.test.d.ts +0 -1
- package/dist/test/unit/logging.test.js +0 -397
- package/dist/test/unit/path-guard.test.d.ts +0 -1
- package/dist/test/unit/path-guard.test.js +0 -109
- package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
- package/dist/test/unit/post-task-cleanup.test.js +0 -48
- package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
- package/dist/test/unit/project-policy-release-mode.test.js +0 -125
- package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
- package/dist/test/unit/reconcile-tasks.test.js +0 -456
- package/dist/test/unit/release-gate.test.d.ts +0 -1
- package/dist/test/unit/release-gate.test.js +0 -242
- package/dist/test/unit/run-task-loop.test.d.ts +0 -1
- package/dist/test/unit/run-task-loop.test.js +0 -380
- package/dist/test/unit/safe-status.test.d.ts +0 -1
- package/dist/test/unit/safe-status.test.js +0 -165
- package/dist/test/unit/safe-views.test.d.ts +0 -1
- package/dist/test/unit/safe-views.test.js +0 -171
- package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
- package/dist/test/unit/schema-drift-check.test.js +0 -176
- package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
- package/dist/test/unit/sensitive-guard.test.js +0 -104
- package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
- package/dist/test/unit/subgoal-sync.test.js +0 -183
- package/dist/test/unit/sync-file.test.d.ts +0 -1
- package/dist/test/unit/sync-file.test.js +0 -154
- package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
- package/dist/test/unit/tool-invocation-guard.test.js +0 -432
- package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
- package/dist/test/unit/tool-usage-stats.test.js +0 -300
- package/dist/test/unit/toolSearch.test.d.ts +0 -1
- package/dist/test/unit/toolSearch.test.js +0 -571
- package/dist/test/unit/watcher-status.test.d.ts +0 -1
- package/dist/test/unit/watcher-status.test.js +0 -169
- package/dist/test/unit/worktree-manager.test.d.ts +0 -1
- package/dist/test/unit/worktree-manager.test.js +0 -176
- package/dist/tools/applyPatch.js +0 -41
- package/dist/tools/cancelTask.d.ts +0 -30
- package/dist/tools/cancelTask.js +0 -64
- package/dist/tools/evidencePack.js +0 -168
- package/dist/tools/finalizeDirectSession.js +0 -84
- package/dist/tools/goalSubgoalTask.js +0 -110
- package/dist/tools/killTask.d.ts +0 -15
- package/dist/tools/retryTask.js +0 -45
- package/dist/tools/runVerification.js +0 -36
- package/dist/tools/syncFile.js +0 -65
- package/docs/control-center/control-center-daily-driver.md +0 -211
- package/docs/control-center/control-center-mvp.md +0 -205
- package/docs/control-center/control-center-phase2.md +0 -159
- package/docs/release-v0.6.0.md +0 -71
- package/docs/release-v0.6.1.md +0 -75
- package/docs/release-v0.6.4.md +0 -45
- package/src/test/unit/acceptance-engine.test.ts +0 -261
- package/src/test/unit/agent-router.test.ts +0 -342
- package/src/test/unit/android-doctor.test.ts +0 -158
- package/src/test/unit/audit-checks.test.ts +0 -567
- package/src/test/unit/chinese-path.test.ts +0 -106
- package/src/test/unit/command-guard.test.ts +0 -221
- package/src/test/unit/diagnose-task.test.ts +0 -544
- package/src/test/unit/direct-guards.test.ts +0 -297
- package/src/test/unit/discovery-token-store.test.ts +0 -181
- package/src/test/unit/evidence-pack.test.ts +0 -142
- package/src/test/unit/goal-graph.test.ts +0 -347
- package/src/test/unit/goal-progress.test.ts +0 -538
- package/src/test/unit/goal-status.test.ts +0 -270
- package/src/test/unit/goal-store.test.ts +0 -318
- package/src/test/unit/goal-subgoal-task.test.ts +0 -72
- package/src/test/unit/goal-tools-registry.test.ts +0 -243
- package/src/test/unit/handoff-export.test.ts +0 -295
- package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
- package/src/test/unit/logging.test.ts +0 -497
- package/src/test/unit/path-guard.test.ts +0 -150
- package/src/test/unit/post-task-cleanup.test.ts +0 -53
- package/src/test/unit/project-policy-release-mode.test.ts +0 -156
- package/src/test/unit/reconcile-tasks.test.ts +0 -551
- package/src/test/unit/release-gate.test.ts +0 -314
- package/src/test/unit/run-task-loop.test.ts +0 -425
- package/src/test/unit/safe-status.test.ts +0 -187
- package/src/test/unit/safe-views.test.ts +0 -184
- package/src/test/unit/schema-drift-check.test.ts +0 -260
- package/src/test/unit/sensitive-guard.test.ts +0 -124
- package/src/test/unit/subgoal-sync.test.ts +0 -236
- package/src/test/unit/sync-file.test.ts +0 -231
- package/src/test/unit/tool-invocation-guard.test.ts +0 -542
- package/src/test/unit/tool-usage-stats.test.ts +0 -384
- package/src/test/unit/toolSearch.test.ts +0 -756
- package/src/test/unit/watcher-status.test.ts +0 -190
- package/src/test/unit/worktree-manager.test.ts +0 -247
- package/src/tools/cancelTask.ts +0 -70
- package/src/tools/evidencePack.ts +0 -205
- package/src/tools/retryTask.ts +0 -53
- package/src/tools/syncFile.ts +0 -122
- /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
- /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
- /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
- /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
- /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
- /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
- /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
- /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
- /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
- /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
- /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
- /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
- /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
- /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
- /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
- /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
- /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
- /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
- /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
- /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
- /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
- /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
- /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
- /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
- /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
- /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
- /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
- /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
- /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { relative, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { cleanGeneratedOutput } from "../lib/clean-generated-output.js";
|
|
8
|
+
import { collectMatchingFiles } from "../lib/file-discovery.js";
|
|
9
|
+
|
|
10
|
+
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
11
|
+
const sourceRoot = resolve(root, "src", "test", "unit");
|
|
12
|
+
const compiledRoot = resolve(root, "dist", "test", "unit");
|
|
13
|
+
|
|
14
|
+
const sourceTests = collectMatchingFiles(sourceRoot, (name) => name.endsWith(".test.ts"))
|
|
15
|
+
.map((path) => relative(sourceRoot, path).replace(/\\/g, "/").replace(/\.ts$/, ".js"));
|
|
16
|
+
const compiledTests = collectMatchingFiles(compiledRoot, (name) => name.endsWith(".test.js"))
|
|
17
|
+
.map((path) => relative(compiledRoot, path).replace(/\\/g, "/"));
|
|
18
|
+
|
|
19
|
+
assert.deepEqual(compiledTests, sourceTests, "compiled unit tests must exactly match source test paths");
|
|
20
|
+
assert.ok(compiledTests.some((path) => path.includes("/")), "unit discovery must include nested tests");
|
|
21
|
+
|
|
22
|
+
const fixtureRoot = mkdtempSync(resolve(tmpdir(), "patchwarden-clean-build-"));
|
|
23
|
+
try {
|
|
24
|
+
const fixtureDist = resolve(fixtureRoot, "dist");
|
|
25
|
+
const outside = resolve(fixtureRoot, "keep.txt");
|
|
26
|
+
mkdirSync(fixtureDist);
|
|
27
|
+
writeFileSync(resolve(fixtureDist, "stale.js"), "stale\n", "utf8");
|
|
28
|
+
writeFileSync(outside, "keep\n", "utf8");
|
|
29
|
+
cleanGeneratedOutput(fixtureRoot, fixtureDist);
|
|
30
|
+
assert.equal(existsSync(fixtureDist), false, "clean build must remove stale dist contents");
|
|
31
|
+
assert.equal(existsSync(outside), true, "clean build must preserve files outside dist");
|
|
32
|
+
assert.throws(() => cleanGeneratedOutput(fixtureRoot, resolve(fixtureRoot, "release")), /Refusing/);
|
|
33
|
+
} finally {
|
|
34
|
+
rmSync(fixtureRoot, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(`[build-output-check] OK: ${sourceTests.length} source tests match ${compiledTests.length} compiled tests.`);
|
|
@@ -121,9 +121,9 @@ process.on("SIGTERM", () => {
|
|
|
121
121
|
process.exit(143);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
function httpGet(url) {
|
|
124
|
+
function httpGet(url, headers = {}) {
|
|
125
125
|
return new Promise((resolve, reject) => {
|
|
126
|
-
const req = http.get(url, (res) => {
|
|
126
|
+
const req = http.get(url, { headers }, (res) => {
|
|
127
127
|
let data = "";
|
|
128
128
|
res.on("data", (chunk) => (data += chunk));
|
|
129
129
|
res.on("end", () => resolve({ status: res.statusCode, headers: res.headers, body: data }));
|
|
@@ -133,6 +133,34 @@ function httpGet(url) {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
async function testLoopbackHostBoundary() {
|
|
137
|
+
const name = "Control Center rejects DNS-rebinding Host headers and cannot be framed";
|
|
138
|
+
try {
|
|
139
|
+
const hostileToken = await httpGet(`${BASE_URL}/control-token.json`, {
|
|
140
|
+
Host: "patchwarden.attacker.invalid",
|
|
141
|
+
});
|
|
142
|
+
const hostilePage = await httpGet(`${BASE_URL}/`, {
|
|
143
|
+
Host: "127.0.0.1.attacker.invalid",
|
|
144
|
+
});
|
|
145
|
+
const localPage = await httpGet(`${BASE_URL}/`);
|
|
146
|
+
const problems = [];
|
|
147
|
+
if (hostileToken.status !== 421 || hostileToken.body.includes("token")) {
|
|
148
|
+
problems.push(`hostile token request returned ${hostileToken.status}: ${hostileToken.body.slice(0, 120)}`);
|
|
149
|
+
}
|
|
150
|
+
if (hostilePage.status !== 421) problems.push(`hostile page request returned ${hostilePage.status}`);
|
|
151
|
+
if (localPage.status !== 200) problems.push(`loopback page returned ${localPage.status}`);
|
|
152
|
+
if (String(localPage.headers["x-frame-options"] || "").toUpperCase() !== "DENY") {
|
|
153
|
+
problems.push("X-Frame-Options is not DENY");
|
|
154
|
+
}
|
|
155
|
+
if (!String(localPage.headers["content-security-policy"] || "").includes("frame-ancestors 'none'")) {
|
|
156
|
+
problems.push("CSP does not deny frame ancestors");
|
|
157
|
+
}
|
|
158
|
+
record(name, problems.length === 0, problems.join("; "));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
record(name, false, `error: ${err.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
136
164
|
function httpPost(url, headers = {}) {
|
|
137
165
|
return new Promise((resolve, reject) => {
|
|
138
166
|
const parsed = new URL(url);
|
|
@@ -221,6 +249,12 @@ async function testStaticFiles() {
|
|
|
221
249
|
const root = await httpGet(`${BASE_URL}/`);
|
|
222
250
|
const vendor = await httpGet(`${BASE_URL}/vendor/tailwindcss-browser.js`);
|
|
223
251
|
const css = await httpGet(`${BASE_URL}/colors_and_type.css`);
|
|
252
|
+
const desktopCss = await httpGet(`${BASE_URL}/desktop.css`);
|
|
253
|
+
const desktopBridge = await httpGet(`${BASE_URL}/desktop-bridge.js`);
|
|
254
|
+
const i18n = await httpGet(`${BASE_URL}/i18n.js`);
|
|
255
|
+
const gettingStarted = await httpGet(`${BASE_URL}/pages/getting-started.html`);
|
|
256
|
+
const gettingStartedJs = await httpGet(`${BASE_URL}/getting-started.js`);
|
|
257
|
+
const settings = await httpGet(`${BASE_URL}/pages/settings.html`);
|
|
224
258
|
const favicon = await httpGet(`${BASE_URL}/favicon.ico`);
|
|
225
259
|
|
|
226
260
|
const checks = [];
|
|
@@ -228,7 +262,8 @@ async function testStaticFiles() {
|
|
|
228
262
|
const rootCt = root.headers["content-type"] || "";
|
|
229
263
|
if (!rootCt.includes("text/html")) checks.push(`GET / -> Content-Type ${rootCt} (expected text/html)`);
|
|
230
264
|
if (!root.body.includes("setup-checklist-card")) checks.push("dashboard missing setup checklist card");
|
|
231
|
-
if (!root.body.includes("
|
|
265
|
+
if (!root.body.includes("查看 Core / Direct 本次日志尾部")) checks.push("dashboard activity log is not collapsed behind a summary");
|
|
266
|
+
if (!root.body.includes("pw-dashboard-toolbar")) checks.push("dashboard missing stable desktop toolbar layout");
|
|
232
267
|
if (!root.body.includes("evidence-pack-card")) checks.push("dashboard missing v1.5 evidence pack card");
|
|
233
268
|
|
|
234
269
|
if (vendor.status !== 200) checks.push(`GET /vendor/tailwindcss-browser.js -> status ${vendor.status}`);
|
|
@@ -239,6 +274,31 @@ async function testStaticFiles() {
|
|
|
239
274
|
const cssCt = css.headers["content-type"] || "";
|
|
240
275
|
if (!cssCt.includes("text/css")) checks.push(`GET /colors_and_type.css -> Content-Type ${cssCt}`);
|
|
241
276
|
|
|
277
|
+
if (desktopCss.status !== 200 || !(desktopCss.headers["content-type"] || "").includes("text/css")) {
|
|
278
|
+
checks.push("desktop refinement stylesheet is not served as CSS");
|
|
279
|
+
}
|
|
280
|
+
if (desktopBridge.status !== 200 || !(desktopBridge.headers["content-type"] || "").includes("javascript")) {
|
|
281
|
+
checks.push("desktop bridge is not served as JavaScript");
|
|
282
|
+
}
|
|
283
|
+
if (i18n.status !== 200 || !i18n.body.includes("applyTranslations") || !i18n.body.includes('"zh-CN"') || !i18n.body.includes("en:")) {
|
|
284
|
+
checks.push("shared Chinese/English translation dictionary is unavailable");
|
|
285
|
+
}
|
|
286
|
+
if (gettingStarted.status !== 200 || (gettingStarted.body.match(/class="readiness-item"/g) || []).length !== 4 || !gettingStarted.body.includes("home.manual")) {
|
|
287
|
+
checks.push("desktop getting-started page is missing its four readiness checks or manual ChatGPT state");
|
|
288
|
+
}
|
|
289
|
+
if (gettingStartedJs.status !== 200 || !gettingStartedJs.body.includes('fetchJson("/api/status")') || !gettingStartedJs.body.includes("health_check")) {
|
|
290
|
+
checks.push("getting-started controller is missing bounded Tunnel status or health_check guidance");
|
|
291
|
+
}
|
|
292
|
+
if (settings.status !== 200 || !settings.body.includes("/settings.js")) {
|
|
293
|
+
checks.push("desktop settings page is unavailable or missing its bounded bridge");
|
|
294
|
+
}
|
|
295
|
+
for (const marker of ["detectTunnel", "chooseTunnel", "enableDirectProfile", "proxyScope", "saveRuntime", "tunnelId", "runtimeKey", "provisionTunnel", "forgetCredential", "language"]) {
|
|
296
|
+
if (!settings.body.includes(marker)) checks.push(`desktop settings missing runtime control: ${marker}`);
|
|
297
|
+
}
|
|
298
|
+
if (!desktopBridge.body.includes("patchwardenApplyTheme") || !desktopBridge.body.includes("请求超过 10 秒")) {
|
|
299
|
+
checks.push("desktop bridge is missing renderer theme propagation or bounded loading timeout");
|
|
300
|
+
}
|
|
301
|
+
|
|
242
302
|
if (favicon.status !== 200) checks.push(`GET /favicon.ico -> status ${favicon.status}`);
|
|
243
303
|
const faviconCt = favicon.headers["content-type"] || "";
|
|
244
304
|
if (!faviconCt.includes("image/svg+xml")) checks.push(`GET /favicon.ico -> Content-Type ${faviconCt}`);
|
|
@@ -268,6 +328,9 @@ async function testPageNavigationRoutes() {
|
|
|
268
328
|
"/pages/workspace.html",
|
|
269
329
|
"/pages/audit.html",
|
|
270
330
|
"/pages/task-detail.html?id=smoke",
|
|
331
|
+
"/settings.html",
|
|
332
|
+
"/pages/settings.html",
|
|
333
|
+
"/pages/getting-started.html",
|
|
271
334
|
];
|
|
272
335
|
|
|
273
336
|
for (const route of routes) {
|
|
@@ -411,6 +474,13 @@ async function testTasksJson() {
|
|
|
411
474
|
if (!Array.isArray(json.tasks)) problems.push("'tasks' is not an array");
|
|
412
475
|
if (typeof json.total !== "number") problems.push("'total' is not a number");
|
|
413
476
|
if (typeof json.returned !== "number") problems.push("'returned' is not a number");
|
|
477
|
+
if (!(json.nextCursor === null || typeof json.nextCursor === "string")) problems.push("'nextCursor' is neither null nor string");
|
|
478
|
+
if (!isObject(json.filters) || !isObject(json.filters.applied) || !isObject(json.filters.options)) problems.push("'filters' contract is missing");
|
|
479
|
+
|
|
480
|
+
const traversal = await httpGet(`${BASE_URL}/api/tasks/..%2Foutside`);
|
|
481
|
+
if (traversal.status !== 400) {
|
|
482
|
+
problems.push(`encoded task traversal returned ${traversal.status} (expected 400)`);
|
|
483
|
+
}
|
|
414
484
|
|
|
415
485
|
if (problems.length === 0) {
|
|
416
486
|
record(name, true);
|
|
@@ -823,7 +893,7 @@ async function testStaleTasksApi() {
|
|
|
823
893
|
|
|
824
894
|
// ── Test 12: GET /api/workspace/<traversal>/status -> 400 ───────
|
|
825
895
|
async function testWorkspaceRepoPathTraversal() {
|
|
826
|
-
const name = "Test 12:
|
|
896
|
+
const name = "Test 12: workspace status rejects traversal without rejecting dotted names";
|
|
827
897
|
// Use percent-encoded dots to bypass URL-parser segment normalization;
|
|
828
898
|
// after decode the repoParam is "../secret" which contains ".." and is
|
|
829
899
|
// rejected by the server's traversal guard.
|
|
@@ -833,7 +903,12 @@ async function testWorkspaceRepoPathTraversal() {
|
|
|
833
903
|
if (res.status === 400) {
|
|
834
904
|
const json = tryJson(res.body);
|
|
835
905
|
if (json && typeof json.error === "string" && /traversal/i.test(json.error)) {
|
|
836
|
-
|
|
906
|
+
const dotted = await httpGet(`${BASE_URL}/api/workspace/release..candidate/status`);
|
|
907
|
+
if (dotted.status === 404) {
|
|
908
|
+
record(name, true);
|
|
909
|
+
} else {
|
|
910
|
+
record(name, false, `valid dotted repo name returned ${dotted.status} (expected 404 for missing directory)`);
|
|
911
|
+
}
|
|
837
912
|
} else {
|
|
838
913
|
record(name, false, `status 400 but error field missing/invalid: ${res.body.slice(0, 200)}`);
|
|
839
914
|
}
|
|
@@ -882,10 +957,17 @@ async function testDirectSessionsEmptyList() {
|
|
|
882
957
|
const problems = [];
|
|
883
958
|
if (!Array.isArray(json.sessions)) problems.push("'sessions' is not an array");
|
|
884
959
|
if (typeof json.total !== "number") problems.push("'total' is not a number");
|
|
960
|
+
if (!(json.nextCursor === null || typeof json.nextCursor === "string")) problems.push("'nextCursor' is neither null nor string");
|
|
961
|
+
if (!isObject(json.filters) || !isObject(json.filters.options)) problems.push("'filters' contract is missing");
|
|
962
|
+
if (typeof json.direct_profile_enabled !== "boolean") problems.push("'direct_profile_enabled' is not a boolean");
|
|
885
963
|
if (json.reason !== null && typeof json.reason !== "string") problems.push("'reason' is neither null nor string");
|
|
886
964
|
if (Array.isArray(json.sessions) && json.sessions.length !== json.total) {
|
|
887
965
|
problems.push(`sessions.length (${json.sessions.length}) != total (${json.total})`);
|
|
888
966
|
}
|
|
967
|
+
const invalidDetail = await httpGet(`${BASE_URL}/api/direct-sessions/invalid.id`);
|
|
968
|
+
const invalidSummary = await httpGet(`${BASE_URL}/api/direct-sessions/invalid.id/summary`);
|
|
969
|
+
if (invalidDetail.status !== 400) problems.push(`invalid detail id returned ${invalidDetail.status} (expected 400)`);
|
|
970
|
+
if (invalidSummary.status !== 400) problems.push(`invalid summary id returned ${invalidSummary.status} (expected 400)`);
|
|
889
971
|
if (problems.length === 0) {
|
|
890
972
|
record(name, true);
|
|
891
973
|
} else {
|
|
@@ -915,6 +997,8 @@ async function testLogsTailParam() {
|
|
|
915
997
|
if (typeof json.stderr !== "string") problems.push("'stderr' is not a string");
|
|
916
998
|
if (json.tail !== 300) problems.push(`'tail' is ${json.tail} (expected 300)`);
|
|
917
999
|
if (json.category !== "core") problems.push(`'category' is ${json.category} (expected "core")`);
|
|
1000
|
+
if (json.historical_snapshot !== true) problems.push("'historical_snapshot' is not true");
|
|
1001
|
+
if (json.redacted !== true) problems.push("'redacted' is not true");
|
|
918
1002
|
if (problems.length === 0) {
|
|
919
1003
|
record(name, true);
|
|
920
1004
|
} else {
|
|
@@ -1088,6 +1172,351 @@ async function testControlCenterStatusApi() {
|
|
|
1088
1172
|
}
|
|
1089
1173
|
}
|
|
1090
1174
|
|
|
1175
|
+
// ── Test 21: GET /api/workspace/repos returns valid JSON with repos array ──
|
|
1176
|
+
async function testWorkspaceReposApi() {
|
|
1177
|
+
const name = "Test 21: /api/workspace/repos returns valid JSON with repos array";
|
|
1178
|
+
try {
|
|
1179
|
+
const res = await httpGet(`${BASE_URL}/api/workspace/repos`);
|
|
1180
|
+
if (res.status !== 200) {
|
|
1181
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const json = tryJson(res.body);
|
|
1185
|
+
if (!json || !isObject(json)) {
|
|
1186
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const problems = [];
|
|
1190
|
+
if (!Array.isArray(json.repos)) problems.push("'repos' is not an array");
|
|
1191
|
+
if (typeof json.workspace_root !== "string" && json.workspace_root !== null) {
|
|
1192
|
+
problems.push("'workspace_root' is neither string nor null");
|
|
1193
|
+
}
|
|
1194
|
+
if (Array.isArray(json.repos)) {
|
|
1195
|
+
for (let i = 0; i < json.repos.length; i++) {
|
|
1196
|
+
const repo = json.repos[i];
|
|
1197
|
+
if (!isObject(repo)) {
|
|
1198
|
+
problems.push(`repos[${i}] is not an object`);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (typeof repo.name !== "string") problems.push(`repos[${i}].name not string`);
|
|
1202
|
+
if (typeof repo.path !== "string") problems.push(`repos[${i}].path not string`);
|
|
1203
|
+
if (typeof repo.has_package_json !== "boolean") problems.push(`repos[${i}].has_package_json not boolean`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (problems.length === 0) {
|
|
1207
|
+
record(name, true);
|
|
1208
|
+
} else {
|
|
1209
|
+
record(name, false, problems.join("; "));
|
|
1210
|
+
}
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
record(name, false, `error: ${err.message}`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// ── Test 22: /api/release/status no longer mentions release_prepare and has ready_state ──
|
|
1217
|
+
async function testReleaseStatusFields() {
|
|
1218
|
+
const name = "Test 22: /api/release/status has ready_state and no release_prepare text";
|
|
1219
|
+
try {
|
|
1220
|
+
const res = await httpGet(`${BASE_URL}/api/release/status?repo_path=.`);
|
|
1221
|
+
if (res.status !== 200) {
|
|
1222
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const json = tryJson(res.body);
|
|
1226
|
+
if (!json || !isObject(json)) {
|
|
1227
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const problems = [];
|
|
1231
|
+
if (!("ready_state" in json)) problems.push("missing 'ready_state' field");
|
|
1232
|
+
if (typeof json.ready_state !== "string") problems.push("'ready_state' is not a string");
|
|
1233
|
+
if (!("package_name" in json)) problems.push("missing 'package_name' field");
|
|
1234
|
+
if (!("version_source" in json)) problems.push("missing 'version_source' field");
|
|
1235
|
+
if (!("version_consistent" in json)) problems.push("missing 'version_consistent' field");
|
|
1236
|
+
if (!Array.isArray(json.required_commands)) problems.push("'required_commands' is not an array");
|
|
1237
|
+
if (typeof json.commands_blocked_count !== "number") problems.push("'commands_blocked_count' is not a number");
|
|
1238
|
+
// The old misleading "release_prepare" text must no longer appear.
|
|
1239
|
+
if (res.body.includes("release_prepare")) {
|
|
1240
|
+
problems.push("response still contains 'release_prepare' text");
|
|
1241
|
+
}
|
|
1242
|
+
if (Array.isArray(json.required_commands)) {
|
|
1243
|
+
for (let i = 0; i < json.required_commands.length; i++) {
|
|
1244
|
+
const cmd = json.required_commands[i];
|
|
1245
|
+
if (!isObject(cmd)) {
|
|
1246
|
+
problems.push(`required_commands[${i}] is not an object`);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (typeof cmd.command !== "string") problems.push(`required_commands[${i}].command not string`);
|
|
1250
|
+
if (typeof cmd.allowed !== "boolean") problems.push(`required_commands[${i}].allowed not boolean`);
|
|
1251
|
+
if (!("blocked_reason" in cmd)) problems.push(`required_commands[${i}] missing 'blocked_reason'`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (problems.length === 0) {
|
|
1255
|
+
record(name, true);
|
|
1256
|
+
} else {
|
|
1257
|
+
record(name, false, problems.join("; "));
|
|
1258
|
+
}
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
record(name, false, `error: ${err.message}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// ── Test 23: Safe task endpoints return valid JSON without full content ──
|
|
1265
|
+
async function testSafeTaskEndpoints() {
|
|
1266
|
+
const name = "Test 23: safe task endpoints return valid JSON without stdout/stderr/diff";
|
|
1267
|
+
const endpoints = [
|
|
1268
|
+
"safe-result",
|
|
1269
|
+
"safe-audit",
|
|
1270
|
+
"safe-test-summary",
|
|
1271
|
+
"safe-diff-summary",
|
|
1272
|
+
];
|
|
1273
|
+
const problems = [];
|
|
1274
|
+
for (const ep of endpoints) {
|
|
1275
|
+
try {
|
|
1276
|
+
const res = await httpGet(`${BASE_URL}/api/tasks/nonexistent-smoke-task/${ep}`);
|
|
1277
|
+
if (res.status !== 200) {
|
|
1278
|
+
problems.push(`/api/tasks/:id/${ep} -> status ${res.status} (expected 200, not 500)`);
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
const json = tryJson(res.body);
|
|
1282
|
+
if (!json || !isObject(json)) {
|
|
1283
|
+
problems.push(`/api/tasks/:id/${ep} body is not a JSON object: ${res.body.slice(0, 120)}`);
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
// Safe endpoints must NOT expose full stdout/stderr/diff content.
|
|
1287
|
+
const text = res.body.toLowerCase();
|
|
1288
|
+
if (text.includes('"stdout"') && text.includes('"stderr"')) {
|
|
1289
|
+
// Some safe endpoints may include a "stdout" key as a bounded field; the
|
|
1290
|
+
// invariant we care about is that they don't return the full raw logs.
|
|
1291
|
+
// We only flag if both stdout AND stderr appear together (log-shaped).
|
|
1292
|
+
problems.push(`/api/tasks/:id/${ep} may leak stdout+stderr shaped fields`);
|
|
1293
|
+
}
|
|
1294
|
+
if (text.includes('"diff_patch"')) {
|
|
1295
|
+
problems.push(`/api/tasks/:id/${ep} leaks 'diff_patch' field`);
|
|
1296
|
+
}
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
problems.push(`/api/tasks/:id/${ep} error: ${err.message}`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (problems.length === 0) {
|
|
1302
|
+
record(name, true);
|
|
1303
|
+
} else {
|
|
1304
|
+
record(name, false, problems.join("; "));
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// ── Test 24: /api/tasks?status=running returns valid filtered JSON ──
|
|
1309
|
+
async function testTasksFilter() {
|
|
1310
|
+
const name = "Test 24: /api/tasks?status=running returns valid filtered JSON";
|
|
1311
|
+
try {
|
|
1312
|
+
const res = await httpGet(`${BASE_URL}/api/tasks?status=running`);
|
|
1313
|
+
if (res.status !== 200) {
|
|
1314
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const json = tryJson(res.body);
|
|
1318
|
+
if (!json || !isObject(json)) {
|
|
1319
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const problems = [];
|
|
1323
|
+
if (!Array.isArray(json.tasks)) problems.push("'tasks' is not an array");
|
|
1324
|
+
if (typeof json.total !== "number") problems.push("'total' is not a number");
|
|
1325
|
+
if (typeof json.returned !== "number") problems.push("'returned' is not a number");
|
|
1326
|
+
// Every returned task must match the filter (if any tasks exist).
|
|
1327
|
+
if (Array.isArray(json.tasks)) {
|
|
1328
|
+
for (let i = 0; i < json.tasks.length; i++) {
|
|
1329
|
+
if (json.tasks[i].status !== "running") {
|
|
1330
|
+
problems.push(`tasks[${i}].status is '${json.tasks[i].status}' (expected 'running')`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (json.tasks.length !== json.total) {
|
|
1334
|
+
problems.push(`tasks.length (${json.tasks.length}) != total (${json.total})`);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (problems.length === 0) {
|
|
1338
|
+
record(name, true);
|
|
1339
|
+
} else {
|
|
1340
|
+
record(name, false, problems.join("; "));
|
|
1341
|
+
}
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
record(name, false, `error: ${err.message}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// ── Test 25: /api/tasks/stale has explanation and next_action on stale tasks ──
|
|
1348
|
+
async function testStaleTasksExplanation() {
|
|
1349
|
+
const name = "Test 25: /api/tasks/stale stale tasks have explanation and next_action";
|
|
1350
|
+
try {
|
|
1351
|
+
const res = await httpGet(`${BASE_URL}/api/tasks/stale`);
|
|
1352
|
+
if (res.status !== 200) {
|
|
1353
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const json = tryJson(res.body);
|
|
1357
|
+
if (!json || !isObject(json)) {
|
|
1358
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const problems = [];
|
|
1362
|
+
if (!Array.isArray(json.stale_tasks)) problems.push("'stale_tasks' is not an array");
|
|
1363
|
+
if (Array.isArray(json.stale_tasks) && json.stale_tasks.length > 0) {
|
|
1364
|
+
for (let i = 0; i < json.stale_tasks.length; i++) {
|
|
1365
|
+
const t = json.stale_tasks[i];
|
|
1366
|
+
if (!isObject(t)) {
|
|
1367
|
+
problems.push(`stale_tasks[${i}] is not an object`);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (typeof t.explanation !== "string" || t.explanation.length === 0) {
|
|
1371
|
+
problems.push(`stale_tasks[${i}] missing 'explanation' string`);
|
|
1372
|
+
}
|
|
1373
|
+
if (typeof t.next_action !== "string" || t.next_action.length === 0) {
|
|
1374
|
+
problems.push(`stale_tasks[${i}] missing 'next_action' string`);
|
|
1375
|
+
}
|
|
1376
|
+
if (typeof t.reason_code !== "string") {
|
|
1377
|
+
problems.push(`stale_tasks[${i}] missing 'reason_code' string`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (problems.length === 0) {
|
|
1382
|
+
record(name, true);
|
|
1383
|
+
} else {
|
|
1384
|
+
record(name, false, problems.join("; "));
|
|
1385
|
+
}
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
record(name, false, `error: ${err.message}`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// ── Test 26: POST /api/tasks/:taskId/hide-stale token gate + valid response ──
|
|
1392
|
+
async function testHideStaleNoToken() {
|
|
1393
|
+
const name = "Test 26: POST /api/tasks/:taskId/hide-stale without token -> 403";
|
|
1394
|
+
try {
|
|
1395
|
+
const res = await httpPost(`${BASE_URL}/api/tasks/nonexistent/hide-stale`, {});
|
|
1396
|
+
if (res.status !== 403) {
|
|
1397
|
+
record(name, false, `status ${res.status} (expected 403 — token gate must fire before hide-stale runs)`);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const json = tryJson(res.body);
|
|
1401
|
+
if (!json || typeof json.error !== "string") {
|
|
1402
|
+
record(name, false, `response body missing 'error' field: ${res.body.slice(0, 200)}`);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
record(name, true);
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
record(name, false, `error: ${err.message}`);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async function testHideStaleWithToken(token) {
|
|
1412
|
+
const name = "Test 27: POST /api/tasks/:taskId/hide-stale with token returns valid JSON";
|
|
1413
|
+
try {
|
|
1414
|
+
const res = await httpPost(`${BASE_URL}/api/tasks/nonexistent/hide-stale`, {
|
|
1415
|
+
"X-PatchWarden-Control-Token": token,
|
|
1416
|
+
});
|
|
1417
|
+
if (res.status !== 200) {
|
|
1418
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const json = tryJson(res.body);
|
|
1422
|
+
if (!json || !isObject(json)) {
|
|
1423
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (json.ok !== true) {
|
|
1427
|
+
record(name, false, `expected ok=true, got: ${res.body.slice(0, 200)}`);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (json.hidden !== "nonexistent") {
|
|
1431
|
+
record(name, false, `expected hidden='nonexistent', got: ${json.hidden}`);
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
record(name, true);
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
record(name, false, `error: ${err.message}`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// ── Test 28: GET /api/warnings returns valid JSON ───────────────
|
|
1441
|
+
async function testWarningsApi() {
|
|
1442
|
+
const name = "Test 28: /api/warnings returns valid JSON with warnings array";
|
|
1443
|
+
try {
|
|
1444
|
+
const res = await httpGet(`${BASE_URL}/api/warnings`);
|
|
1445
|
+
if (res.status !== 200) {
|
|
1446
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const json = tryJson(res.body);
|
|
1450
|
+
if (!json || !isObject(json)) {
|
|
1451
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const problems = [];
|
|
1455
|
+
if (!Array.isArray(json.warnings)) problems.push("'warnings' is not an array");
|
|
1456
|
+
if (typeof json.total !== "number") problems.push("'total' is not a number");
|
|
1457
|
+
if (Array.isArray(json.warnings)) {
|
|
1458
|
+
for (let i = 0; i < json.warnings.length; i++) {
|
|
1459
|
+
const w = json.warnings[i];
|
|
1460
|
+
if (!isObject(w)) {
|
|
1461
|
+
problems.push(`warnings[${i}] is not an object`);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (typeof w.type !== "string") problems.push(`warnings[${i}].type is not a string`);
|
|
1465
|
+
if (typeof w.severity !== "string") problems.push(`warnings[${i}].severity is not a string`);
|
|
1466
|
+
if (typeof w.affected_tasks_count !== "number") problems.push(`warnings[${i}].affected_tasks_count is not a number`);
|
|
1467
|
+
if (typeof w.recommended_action !== "string") problems.push(`warnings[${i}].recommended_action is not a string`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (problems.length === 0) {
|
|
1471
|
+
record(name, true);
|
|
1472
|
+
} else {
|
|
1473
|
+
record(name, false, problems.join("; "));
|
|
1474
|
+
}
|
|
1475
|
+
} catch (err) {
|
|
1476
|
+
record(name, false, `error: ${err.message}`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// ── Test 29: GET /api/diagnostics returns valid JSON with redaction ─
|
|
1481
|
+
async function testDiagnosticsApi() {
|
|
1482
|
+
const name = "Test 29: /api/diagnostics returns valid JSON with required fields and redaction";
|
|
1483
|
+
try {
|
|
1484
|
+
const res = await httpGet(`${BASE_URL}/api/diagnostics`);
|
|
1485
|
+
if (res.status !== 200) {
|
|
1486
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const json = tryJson(res.body);
|
|
1490
|
+
if (!json || !isObject(json)) {
|
|
1491
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const problems = [];
|
|
1495
|
+
if (typeof json.server_version !== "string") problems.push("'server_version' is not a string");
|
|
1496
|
+
if (typeof json.schema_epoch !== "string") problems.push("'schema_epoch' is not a string");
|
|
1497
|
+
if (typeof json.watcher_status !== "string") problems.push("'watcher_status' is not a string");
|
|
1498
|
+
if (typeof json.workspace_root !== "string" && json.workspace_root !== null) {
|
|
1499
|
+
problems.push("'workspace_root' is neither string nor null");
|
|
1500
|
+
}
|
|
1501
|
+
if (typeof json.direct_profile_enabled !== "boolean") problems.push("'direct_profile_enabled' is not a boolean");
|
|
1502
|
+
// Redaction: response body must not contain sensitive keywords
|
|
1503
|
+
const lower = res.body.toLowerCase();
|
|
1504
|
+
const forbidden = ["token", "password", "secret", "credential", "api_key"];
|
|
1505
|
+
for (const word of forbidden) {
|
|
1506
|
+
if (lower.includes(word)) {
|
|
1507
|
+
problems.push(`response body contains forbidden word '${word}' (redaction failure)`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (problems.length === 0) {
|
|
1511
|
+
record(name, true);
|
|
1512
|
+
} else {
|
|
1513
|
+
record(name, false, problems.join("; "));
|
|
1514
|
+
}
|
|
1515
|
+
} catch (err) {
|
|
1516
|
+
record(name, false, `error: ${err.message}`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1091
1520
|
async function main() {
|
|
1092
1521
|
// Sanity: ensure the compiled server exists before spawning.
|
|
1093
1522
|
if (!existsSync(serverPath) || !statSync(serverPath).isFile()) {
|
|
@@ -1095,7 +1524,7 @@ async function main() {
|
|
|
1095
1524
|
process.exit(1);
|
|
1096
1525
|
}
|
|
1097
1526
|
|
|
1098
|
-
child = spawn(
|
|
1527
|
+
child = spawn(process.execPath, [serverPath], {
|
|
1099
1528
|
cwd: projectRoot,
|
|
1100
1529
|
env: {
|
|
1101
1530
|
...process.env,
|
|
@@ -1135,6 +1564,7 @@ async function main() {
|
|
|
1135
1564
|
console.log(`[control-center-smoke] server ready at ${BASE_URL}`);
|
|
1136
1565
|
|
|
1137
1566
|
await testStaticFiles();
|
|
1567
|
+
await testLoopbackHostBoundary();
|
|
1138
1568
|
await testPageNavigationRoutes();
|
|
1139
1569
|
await testStatusJson();
|
|
1140
1570
|
await testTasksJson();
|
|
@@ -1159,6 +1589,17 @@ async function main() {
|
|
|
1159
1589
|
await testControlCenterLogApi();
|
|
1160
1590
|
await testRestartAllNoToken();
|
|
1161
1591
|
await testControlCenterStatusApi();
|
|
1592
|
+
// v1.5.1 Dashboard UI optimization coverage
|
|
1593
|
+
await testWorkspaceReposApi();
|
|
1594
|
+
await testReleaseStatusFields();
|
|
1595
|
+
await testSafeTaskEndpoints();
|
|
1596
|
+
await testTasksFilter();
|
|
1597
|
+
await testStaleTasksExplanation();
|
|
1598
|
+
await testHideStaleNoToken();
|
|
1599
|
+
if (token) await testHideStaleWithToken(token);
|
|
1600
|
+
// P1 Task 10/11 coverage
|
|
1601
|
+
await testWarningsApi();
|
|
1602
|
+
await testDiagnosticsApi();
|
|
1162
1603
|
|
|
1163
1604
|
// Use token to silence unused-var linters and confirm we actually fetched it.
|
|
1164
1605
|
if (token) {
|
|
@@ -139,12 +139,13 @@ try {
|
|
|
139
139
|
|
|
140
140
|
const expectedFiles = [
|
|
141
141
|
"PatchWarden.cmd",
|
|
142
|
-
"PatchWarden-Control.cmd",
|
|
143
|
-
"PatchWarden-Control-Tray.cmd",
|
|
144
|
-
"PatchWarden-Desktop.cmd",
|
|
145
|
-
"Restart-PatchWarden-Control.cmd",
|
|
146
|
-
"Stop-PatchWarden.cmd",
|
|
142
|
+
"scripts/launchers/PatchWarden-Control.cmd",
|
|
143
|
+
"scripts/launchers/PatchWarden-Control-Tray.cmd",
|
|
144
|
+
"scripts/launchers/PatchWarden-Desktop.cmd",
|
|
145
|
+
"scripts/launchers/Restart-PatchWarden-Control.cmd",
|
|
146
|
+
"scripts/launchers/Stop-PatchWarden.cmd",
|
|
147
147
|
"scripts/control/manage-patchwarden.ps1",
|
|
148
|
+
"scripts/control/run-background-supervisor.ps1",
|
|
148
149
|
"scripts/control/stop-patchwarden.ps1",
|
|
149
150
|
"scripts/launchers/Start-PatchWarden-Tunnel.cmd",
|
|
150
151
|
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
@@ -158,14 +159,24 @@ try {
|
|
|
158
159
|
if (!rootEntry.includes("manage-patchwarden.ps1")) {
|
|
159
160
|
throw new Error("PatchWarden.cmd does not invoke the consolidated manager");
|
|
160
161
|
}
|
|
161
|
-
const stopEntry = readFileSync(join(root, "Stop-PatchWarden.cmd"), "utf8");
|
|
162
|
-
if (!stopEntry.includes("
|
|
162
|
+
const stopEntry = readFileSync(join(root, "scripts", "launchers", "Stop-PatchWarden.cmd"), "utf8");
|
|
163
|
+
if (!stopEntry.includes("control\\stop-patchwarden.ps1")) {
|
|
163
164
|
throw new Error("Stop-PatchWarden.cmd does not invoke the one-click shutdown script");
|
|
164
165
|
}
|
|
165
|
-
const desktopEntry = readFileSync(join(root, "PatchWarden-Desktop.cmd"), "utf8");
|
|
166
|
+
const desktopEntry = readFileSync(join(root, "scripts", "launchers", "PatchWarden-Desktop.cmd"), "utf8");
|
|
166
167
|
if (!desktopEntry.includes("control-center-tray.ps1") || !desktopEntry.includes("WindowStyle Hidden")) {
|
|
167
168
|
throw new Error("PatchWarden-Desktop.cmd must launch the tray hidden as the daily desktop entry");
|
|
168
169
|
}
|
|
170
|
+
const managerSource = readFileSync(manager, "utf8");
|
|
171
|
+
const wrapperSource = readFileSync(join(root, "scripts", "control", "run-background-supervisor.ps1"), "utf8");
|
|
172
|
+
for (const expected of ["supervisor-status.json", "supervisor.stdout.log", "supervisor.stderr.log", "run-background-supervisor.ps1", "Write-BackgroundStartingState"]) {
|
|
173
|
+
if (!managerSource.includes(expected)) {
|
|
174
|
+
throw new Error(`background manager is missing supervisor observability: ${expected}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!wrapperSource.includes("1> $stdout 2> $stderr") || wrapperSource.includes("CONTROL_PLANE_API_KEY")) {
|
|
178
|
+
throw new Error("background wrapper must redirect supervisor logs without credential arguments");
|
|
179
|
+
}
|
|
169
180
|
console.log("ok - control handles orphan cleanup, scoped kill, port conflicts, health fallback, and Core/Direct lifecycle actions");
|
|
170
181
|
} finally {
|
|
171
182
|
if (fakeTunnel?.exitCode === null) fakeTunnel.kill();
|