claude-code-session-manager 0.87.0 → 0.88.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -53
- package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-kQF8_wAw.js} +1 -1
- package/dist/assets/{DataModel--mISIJ6h.js → DataModel-CY_mMnsr.js} +1 -1
- package/dist/assets/{History-C2ahUXTg.js → History-Sji2brb0.js} +1 -1
- package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CtqpO-p5.js} +1 -1
- package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-DN0ydOCr.js} +1 -1
- package/dist/assets/{Library-Dc8Qst1R.js → Library-CoiLZTKh.js} +1 -1
- package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CmqiQ_io.js} +1 -1
- package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-HPoNeKhT.js} +1 -1
- package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-Bj1X3_NT.js} +1 -1
- package/dist/assets/{Memory-CW62MXlh.js → Memory-1PKjsM3b.js} +1 -1
- package/dist/assets/{Panel-Bw1FhRuF.js → Panel-I1s6ghPo.js} +1 -1
- package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-BhiiZjHQ.js} +1 -1
- package/dist/assets/{Plugins-BnKx9flD.js → Plugins-gv3_fkPX.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-BGcgBd8V.js} +1 -1
- package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BGr9eZ4g.js} +1 -1
- package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-Bdo_awPq.js} +7 -7
- package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-ukl0qWaA.js} +1 -1
- package/dist/assets/{Settings-DluB-vN1.js → Settings-DJkFj22G.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-0t1Qblp6.js} +1 -1
- package/dist/assets/{Skills-gNdo_HNK.js → Skills-CeVdBGE-.js} +1 -1
- package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-CfKvNHi3.js} +1 -1
- package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-DAzNaUPd.js} +1 -1
- package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody--S_rGXoA.js} +1 -1
- package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-Dszm0xnp.js} +1 -1
- package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/{index-fc_JjdxL.js → index-Dp4Rc--q.js} +356 -356
- package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-BLbssMYo.js} +1 -1
- package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
- package/dist/index.html +2 -2
- package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
- package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
- package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
- package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
- package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
- package/package.json +10 -13
- package/scripts/README.md +59 -0
- package/scripts/audit-ops-hygiene.cjs +350 -0
- package/scripts/hooks/guard-destructive-git.cjs +10 -34
- package/scripts/hooks/guard-inline-implementation.cjs +14 -8
- package/scripts/hooks/guard-prd-writes.cjs +7 -36
- package/scripts/hooks/guard-self-schedule.cjs +175 -0
- package/scripts/ops-sweep.cjs +355 -0
- package/scripts/scheduler-mcp-server.cjs +28 -71
- package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
- package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
- package/src/main/__tests__/chat-queue.test.cjs +2 -2
- package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
- package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
- package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
- package/src/main/__tests__/dod-report.test.cjs +2 -2
- package/src/main/__tests__/dod-reverify.test.cjs +2 -2
- package/src/main/__tests__/epicMint.test.cjs +2 -2
- package/src/main/__tests__/exchanges.test.cjs +2 -2
- package/src/main/__tests__/extractJson.test.cjs +2 -2
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
- package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +15 -1
- package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
- package/src/main/__tests__/health-tick-liveness.test.cjs +27 -3
- package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
- package/src/main/__tests__/historyRollup.test.cjs +2 -2
- package/src/main/__tests__/kg-augment.test.cjs +2 -2
- package/src/main/__tests__/mcpStatus.test.cjs +2 -2
- package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
- package/src/main/__tests__/memoryStale.test.cjs +1 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +25 -1
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +33 -3
- package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
- package/src/main/__tests__/prdLocationsArchived.test.cjs +20 -13
- package/src/main/__tests__/proc-role-env.test.cjs +125 -0
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
- package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
- package/src/main/__tests__/projectPages.test.cjs +63 -149
- package/src/main/__tests__/queue-health-verdict.test.cjs +9 -9
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
- package/src/main/__tests__/queueHistory.test.cjs +2 -2
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
- package/src/main/__tests__/runVerify.test.cjs +5 -5
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +2 -2
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +78 -97
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +46 -0
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
- package/src/main/__tests__/scheduler-looks-done.test.cjs +26 -7
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +23 -4
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +81 -1
- package/src/main/__tests__/scheduler-stall-per-project.test.cjs +14 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +4 -4
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +37 -11
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
- package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
- package/src/main/bilkoHost.cjs +16 -7
- package/src/main/build-info.json +8 -0
- package/src/main/chatRunner.cjs +4 -2
- package/src/main/config.cjs +2 -3
- package/src/main/docEdit.cjs +4 -2
- package/src/main/health.cjs +276 -52
- package/src/main/heapSnapshot.cjs +2 -2
- package/src/main/historyAggregator.cjs +1 -1
- package/src/main/index.cjs +61 -10
- package/src/main/ipcSchemas.cjs +6 -17
- package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
- package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
- package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +1 -1
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
- package/src/main/lib/__tests__/gateFixtures.json +20 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +8 -1
- package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
- package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
- package/src/main/lib/__tests__/procName.test.cjs +92 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
- package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +61 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +63 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +73 -0
- package/src/main/lib/activeIndexRebuild.cjs +1 -3
- package/src/main/lib/activeSessions.cjs +20 -88
- package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
- package/src/main/lib/agentModelResolve.cjs +33 -1
- package/src/main/lib/agentPersonaSchema.cjs +2 -2
- package/src/main/lib/auditLog.cjs +30 -5
- package/src/main/lib/buildIdentity.cjs +113 -0
- package/src/main/lib/classifyPromptTicket.cjs +3 -2
- package/src/main/lib/claudeBin.cjs +37 -2
- package/src/main/lib/cleanEnv.cjs +27 -1
- package/src/main/lib/credentials.cjs +4 -2
- package/src/main/lib/cwdClassify.cjs +185 -0
- package/src/main/lib/definitionOfDone.cjs +296 -52
- package/src/main/lib/dispatchLoop.cjs +40 -0
- package/src/main/lib/effectiveModelInfo.cjs +9 -10
- package/src/main/lib/ephemeralCwd.cjs +7 -28
- package/src/main/lib/gitWorktree.cjs +95 -8
- package/src/main/lib/guardShims.cjs +3 -3
- package/src/main/lib/historyRollup.cjs +6 -7
- package/src/main/lib/instanceLock.cjs +32 -5
- package/src/main/lib/jobSupervisorRecord.cjs +147 -0
- package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
- package/src/main/lib/localAdminHttp.cjs +9 -27
- package/src/main/lib/mcpToolCatalog.cjs +29 -77
- package/src/main/lib/opsOwnership.cjs +27 -19
- package/src/main/lib/prdAuthoringSeed.cjs +36 -0
- package/src/main/lib/prdLocations.cjs +48 -1
- package/src/main/lib/procIdentity.cjs +126 -0
- package/src/main/lib/procName.cjs +98 -0
- package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
- package/src/main/lib/queueHistory.cjs +8 -7
- package/src/main/lib/queueStore.cjs +57 -37
- package/src/main/lib/quietMachineLease.cjs +19 -2
- package/src/main/lib/reservationExpiry.cjs +30 -0
- package/src/main/lib/runClaudeP.cjs +4 -2
- package/src/main/lib/runLogRetention.cjs +3 -2
- package/src/main/lib/scheduleJobSchema.cjs +2 -2
- package/src/main/lib/scheduleJobTransitions.cjs +29 -2
- package/src/main/lib/schedulerBatch.cjs +27 -2
- package/src/main/lib/schedulerPaths.cjs +175 -0
- package/src/main/lib/schedulerRuntimeState.cjs +59 -0
- package/src/main/lib/sessionSlots.cjs +47 -9
- package/src/main/lib/smProcNames.cjs +51 -0
- package/src/main/lib/upgradeDrain.cjs +188 -0
- package/src/main/lib/usageCircuit.cjs +53 -6
- package/src/main/lib/watchdogHelpers.cjs +70 -37
- package/src/main/lib/withTimeout.cjs +33 -0
- package/src/main/mcpStatus.cjs +4 -2
- package/src/main/pluginInstall.cjs +6 -2
- package/src/main/projectPages.cjs +41 -145
- package/src/main/pty.cjs +8 -1
- package/src/main/queueOps.cjs +10 -10
- package/src/main/runVerify.cjs +69 -3
- package/src/main/scheduler.cjs +1607 -383
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +7 -6
- package/src/main/seedStatus.cjs +1 -1
- package/src/main/supervisor.cjs +5 -3
- package/src/main/usage.cjs +126 -62
- package/src/preload/api.d.ts +18 -64
- package/src/preload/index.cjs +2 -4
- package/src/seed/agents/project-home-builder.md +31 -48
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +0 -13
- package/src/main/lib/projectPageSummarySchema.cjs +0 -181
- package/src/main/teams.cjs +0 -95
- package/src/main/templates/project-pages-catalog.json +0 -741
- package/src/main/templates/project-pages-default-home.html +0 -123
- package/src/main/templates/project-pages-pipeline.md +0 -417
- package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
- package/src/seed/prompts/code-review/correctness-only.md +0 -8
- package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
- package/src/seed/prompts/code-review/hallucination-check.md +0 -8
- package/src/seed/prompts/code-review/public-api-compat.md +0 -8
- package/src/seed/prompts/code-review/readability-naming.md +0 -8
- package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
- package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
- package/src/seed/prompts/documentation/adr-from-change.md +0 -8
- package/src/seed/prompts/documentation/module-readme.md +0 -8
- package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
- package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
- package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
- package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
- package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
- package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
- package/src/seed/prompts/performance/complexity-audit.md +0 -8
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
- package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
- package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
- package/src/seed/prompts/qa/api-contract-tests.md +0 -8
- package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
- package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
- package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
- package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
- package/src/seed/prompts/qa/tdd-red-first.md +0 -8
- package/src/seed/prompts/qa/visual-regression-review.md +0 -8
- package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
- package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
- package/src/seed/prompts/security/authz-audit.md +0 -8
- package/src/seed/prompts/security/crypto-correctness.md +0 -8
- package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
- package/src/seed/prompts/security/dependency-audit.md +0 -8
- package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
- package/src/seed/prompts/security/secret-credential-scan.md +0 -8
- package/web/README.md +0 -41
- package/web/project-pages/logic/dist/logic.cjs +0 -4709
- package/web/project-pages/render.cjs +0 -70
- package/web/project-pages/renderer/dist/renderer.cjs +0 -18900
- package/web/project-pages/validate-summary.cjs +0 -62
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-stranded-autofix-park.test.cjs — PRD "1218-fo-01 stranded
|
|
3
|
+
* auto-fix park" (2026-09-13, findings filed at session-manager-operations/
|
|
4
|
+
* reviews/2026-09-13-scheduler-stability-investigation.md, "post-run
|
|
5
|
+
* adjudication" section).
|
|
6
|
+
*
|
|
7
|
+
* spawnInvestigation restores a job's status from 'investigating' back to
|
|
8
|
+
* needs_review in ONE mutate() call (scheduler.cjs, source
|
|
9
|
+
* 'spawnInvestigation:onExit') and stamps autoFixOutcome in a SEPARATE,
|
|
10
|
+
* later mutate() call. A crash/restart between the two leaves a row with
|
|
11
|
+
* autoFixAttempted: true and autoFixOutcome permanently undefined —
|
|
12
|
+
* isExhaustedAutoFix wants autoFixRetries >= 1 (never incremented here) and
|
|
13
|
+
* isGuardParkedWithoutAutoFix wants autoFixAttempted === false, so the row
|
|
14
|
+
* falls through every resolving door and the periodic reverify pass just
|
|
15
|
+
* re-scans the same frozen transcript forever. Job 1218-fo-01 sat exactly
|
|
16
|
+
* like this with a landed commit while every dependent PRD waited.
|
|
17
|
+
*
|
|
18
|
+
* This file covers the new isStrandedAutoFixPark predicate and its wiring
|
|
19
|
+
* into isEligibleForNeedsReviewAutoResolve / selectExhaustedNeedsReviewTargets
|
|
20
|
+
* / applyNeedsReviewAutoResolve — the SAME bounded ladder the exhausted-
|
|
21
|
+
* auto-fix and guard-parked doors already use, unchanged.
|
|
22
|
+
*
|
|
23
|
+
* HOME is overridden to a tmp dir BEFORE requiring scheduler.cjs — same
|
|
24
|
+
* reason as scheduler-looks-done.test.cjs (appendAuditEvent writes under
|
|
25
|
+
* $HOME/.claude/session-manager/audit-log.jsonl).
|
|
26
|
+
*
|
|
27
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-stranded-autofix-park.test.cjs
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const assert = require('node:assert/strict');
|
|
33
|
+
const fs = require('node:fs');
|
|
34
|
+
const os = require('node:os');
|
|
35
|
+
const path = require('node:path');
|
|
36
|
+
const { execFileSync } = require('node:child_process');
|
|
37
|
+
|
|
38
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'stranded-autofix-park-test-'));
|
|
39
|
+
process.env.HOME = tmpHome;
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
isStrandedAutoFixPark,
|
|
43
|
+
isExhaustedAutoFix,
|
|
44
|
+
isGuardParkedWithoutAutoFix,
|
|
45
|
+
isEligibleForNeedsReviewAutoResolve,
|
|
46
|
+
selectExhaustedNeedsReviewTargets,
|
|
47
|
+
applyNeedsReviewAutoResolve,
|
|
48
|
+
computeLooksDone,
|
|
49
|
+
fixSlugFor,
|
|
50
|
+
NEEDS_REVIEW_RESOLVE_CAP,
|
|
51
|
+
} = require('../scheduler.cjs');
|
|
52
|
+
const { resolvePrdWriteDir } = require('../lib/prdLocations.cjs');
|
|
53
|
+
const { bustCwdCache } = require('../lib/queueStore.cjs');
|
|
54
|
+
|
|
55
|
+
const MIN_MS = 60_000;
|
|
56
|
+
const THRESHOLD_MS = 30 * MIN_MS;
|
|
57
|
+
|
|
58
|
+
// findPrdDir (via resolvePrdsDirs -> allProjectCwds) only discovers a PRD
|
|
59
|
+
// dir under a cwd it already knows about — same registration computeLooksDone
|
|
60
|
+
// needs in scheduler-looks-done.test.cjs / scheduler-guard-verdict-autoresolve.test.cjs.
|
|
61
|
+
function registerActiveProject(cwd, slug) {
|
|
62
|
+
const slugDir = path.join(tmpHome, '.claude', 'projects', slug);
|
|
63
|
+
fs.mkdirSync(slugDir, { recursive: true });
|
|
64
|
+
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
65
|
+
bustCwdCache();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Replays the live 1218-fo-01 tuple: needs_review, verifierVerdict
|
|
69
|
+
// transcript_errors, exitCode 0, a landed commit, autoFixAttempted true,
|
|
70
|
+
// autoFixOutcome undefined, statusHistory ending in the exact
|
|
71
|
+
// spawnInvestigation:onExit restore reason.
|
|
72
|
+
function strandedJob(overrides = {}) {
|
|
73
|
+
return {
|
|
74
|
+
slug: '1218-fo-01-move-scripts-lib-into-src-main-lib',
|
|
75
|
+
cwd: '/home/user/project',
|
|
76
|
+
status: 'needs_review',
|
|
77
|
+
exitCode: 0,
|
|
78
|
+
verifierVerdict: 'transcript_errors',
|
|
79
|
+
landedCommit: 'd1edf15babc1234',
|
|
80
|
+
autoFixAttempted: true,
|
|
81
|
+
statusHistory: [
|
|
82
|
+
{ to: 'needs_review', at: new Date(Date.now() - 45 * MIN_MS).toISOString() },
|
|
83
|
+
{ to: 'investigating', at: new Date(Date.now() - 40 * MIN_MS).toISOString(), reason: 'spawning investigation probe' },
|
|
84
|
+
{
|
|
85
|
+
to: 'needs_review',
|
|
86
|
+
at: new Date(Date.now() - 35 * MIN_MS).toISOString(),
|
|
87
|
+
reason: 'investigation probe exited — restoring prior status',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
...overrides,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- isStrandedAutoFixPark ---
|
|
95
|
+
|
|
96
|
+
test('the 1218 tuple (autoFixAttempted true, autoFixOutcome undefined, no child row) is a stranded auto-fix park', () => {
|
|
97
|
+
const job = strandedJob();
|
|
98
|
+
assert.equal(isStrandedAutoFixPark(job, [job]), true);
|
|
99
|
+
assert.equal(isExhaustedAutoFix(job), false, 'never accumulated a retry — must not double-count as exhausted');
|
|
100
|
+
assert.equal(isGuardParkedWithoutAutoFix(job), false, 'autoFixAttempted is true — the guard-parked door requires false');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('autoFixOutcome "error" or "no-plan" with an unspent retry is NOT stranded — selectAutoFixTargets still owns it', () => {
|
|
104
|
+
// Durably stamped (unlike the 1218 tuple's permanently-unset outcome), so
|
|
105
|
+
// its one bounded retry (autoFixRetries < 1) is still selectAutoFixTargets's
|
|
106
|
+
// to spend — pulling it into this ladder instead would race it away from
|
|
107
|
+
// that retry (see scheduler-needs-review-autoresolve.test.cjs's "a
|
|
108
|
+
// non-exhausted needs_review row … is left alone").
|
|
109
|
+
assert.equal(isStrandedAutoFixPark(strandedJob({ autoFixOutcome: 'error' }), []), false);
|
|
110
|
+
assert.equal(isStrandedAutoFixPark(strandedJob({ autoFixOutcome: 'no-plan' }), []), false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('autoFixOutcome "plan" is never a stranded park — that is isPlanUnqueued/isFixPlanDead territory', () => {
|
|
114
|
+
assert.equal(isStrandedAutoFixPark(strandedJob({ autoFixOutcome: 'plan' }), []), false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('a row that never attempted auto-fix at all is not a stranded park', () => {
|
|
118
|
+
assert.equal(isStrandedAutoFixPark(strandedJob({ autoFixAttempted: undefined }), []), false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('a row not in needs_review is never a stranded park, whatever its autoFix* fields say', () => {
|
|
122
|
+
assert.equal(isStrandedAutoFixPark(strandedJob({ status: 'failed' }), []), false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('a live fix-plan child (running) means there IS something in flight — not stranded', () => {
|
|
126
|
+
const job = strandedJob();
|
|
127
|
+
const child = { slug: fixSlugFor(job), status: 'running' };
|
|
128
|
+
assert.equal(isStrandedAutoFixPark(job, [job, child]), false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('a row with a live investigating status (the fix-plan child mid-probe) is NOT selected', () => {
|
|
132
|
+
const job = strandedJob();
|
|
133
|
+
const child = { slug: fixSlugFor(job), status: 'investigating' };
|
|
134
|
+
assert.equal(isStrandedAutoFixPark(job, [job, child]), false);
|
|
135
|
+
assert.equal(selectExhaustedNeedsReviewTargets([job, child], Date.now(), THRESHOLD_MS).length, 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('a queued (pending) fix-plan child also blocks the stranded-park door', () => {
|
|
139
|
+
const job = strandedJob();
|
|
140
|
+
const child = { slug: fixSlugFor(job), status: 'pending' };
|
|
141
|
+
assert.equal(isStrandedAutoFixPark(job, [job, child]), false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a DEAD fix-plan child (needs_review/failed/quarantined/skipped) does not block it — nothing left to wait on', () => {
|
|
145
|
+
const job = strandedJob();
|
|
146
|
+
for (const deadStatus of ['needs_review', 'failed', 'quarantined', 'skipped']) {
|
|
147
|
+
const child = { slug: fixSlugFor(job), status: deadStatus };
|
|
148
|
+
assert.equal(isStrandedAutoFixPark(job, [job, child]), true, `child status ${deadStatus} must not block`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('a COMPLETED fix-plan child does not block it either', () => {
|
|
153
|
+
const job = strandedJob();
|
|
154
|
+
const child = { slug: fixSlugFor(job), status: 'completed' };
|
|
155
|
+
assert.equal(isStrandedAutoFixPark(job, [job, child]), true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('isEligibleForNeedsReviewAutoResolve admits the stranded-park door alongside the other two', () => {
|
|
159
|
+
const job = strandedJob();
|
|
160
|
+
assert.equal(isEligibleForNeedsReviewAutoResolve(job, [job]), true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// --- selectExhaustedNeedsReviewTargets: age-gated selection ---
|
|
164
|
+
|
|
165
|
+
test('a fresh stranded park (under threshold) is not selected', () => {
|
|
166
|
+
const job = strandedJob({
|
|
167
|
+
statusHistory: [
|
|
168
|
+
{ to: 'needs_review', at: new Date(Date.now() - 2 * MIN_MS).toISOString(), reason: 'investigation probe exited — restoring prior status' },
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
assert.equal(selectExhaustedNeedsReviewTargets([job], Date.now(), THRESHOLD_MS).length, 0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('a stranded park past NEEDS_REVIEW_RESOLVE_MS, under the cap, IS selected', () => {
|
|
175
|
+
const job = strandedJob();
|
|
176
|
+
const found = selectExhaustedNeedsReviewTargets([job], Date.now(), THRESHOLD_MS);
|
|
177
|
+
assert.equal(found.length, 1);
|
|
178
|
+
assert.equal(found[0].slug, job.slug);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// --- applyNeedsReviewAutoResolve: same bounded ladder, entered via the new door ---
|
|
182
|
+
|
|
183
|
+
test('computeLooksDone evidence completes the stranded row via the existing looksDone branch', async () => {
|
|
184
|
+
const projectCwd = fs.mkdtempSync(path.join(tmpHome, 'proj-stranded-'));
|
|
185
|
+
fs.mkdirSync(projectCwd, { recursive: true });
|
|
186
|
+
const git = (args) => execFileSync('git', args, { cwd: projectCwd, encoding: 'utf8' });
|
|
187
|
+
git(['init', '-q']);
|
|
188
|
+
git(['config', 'user.email', 'test@example.com']);
|
|
189
|
+
git(['config', 'user.name', 'Test']);
|
|
190
|
+
fs.writeFileSync(path.join(projectCwd, 'README.md'), 'hello\n');
|
|
191
|
+
git(['add', '-A']);
|
|
192
|
+
git(['commit', '-q', '-m', 'initial']);
|
|
193
|
+
registerActiveProject(projectCwd, path.basename(projectCwd));
|
|
194
|
+
|
|
195
|
+
const dir = resolvePrdWriteDir(projectCwd);
|
|
196
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
197
|
+
const slug = '1218-fo-01-move-scripts-lib-into-src-main-lib';
|
|
198
|
+
fs.writeFileSync(
|
|
199
|
+
path.join(dir, `${slug}.md`),
|
|
200
|
+
`---\ntitle: Test PRD\ncwd: ${projectCwd}\nestimateMinutes: 30\n---\n# Implementation notes\nEdit \`src/thing.js\`.\n`,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const startedAt = new Date(Date.now() - 60 * MIN_MS).toISOString();
|
|
204
|
+
await new Promise((r) => setTimeout(r, 1100)); // git --since has 1s resolution
|
|
205
|
+
fs.mkdirSync(path.join(projectCwd, 'src'), { recursive: true });
|
|
206
|
+
fs.writeFileSync(path.join(projectCwd, 'src', 'thing.js'), 'module.exports = {};\n');
|
|
207
|
+
git(['add', '-A']);
|
|
208
|
+
git(['commit', '-q', '-m', `the actual fix for ${slug} landed`]);
|
|
209
|
+
|
|
210
|
+
const job = strandedJob({ slug, cwd: projectCwd, startedAt });
|
|
211
|
+
assert.equal(isStrandedAutoFixPark(job, [job]), true);
|
|
212
|
+
|
|
213
|
+
const looksDone = await computeLooksDone(job);
|
|
214
|
+
assert.ok(looksDone, 'expected computeLooksDone to find the attributable commit');
|
|
215
|
+
job.looksDone = looksDone;
|
|
216
|
+
|
|
217
|
+
const outcome = applyNeedsReviewAutoResolve(job, [job]);
|
|
218
|
+
assert.equal(outcome, 'completed');
|
|
219
|
+
assert.equal(job.status, 'completed');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('with no completion evidence yet, the stranded row gets one bounded requeue, same cap/logic as the other doors', () => {
|
|
223
|
+
const job = strandedJob({ exhaustedResolveAttempts: 0 });
|
|
224
|
+
assert.equal(job.looksDone, undefined);
|
|
225
|
+
const outcome = applyNeedsReviewAutoResolve(job, [job]);
|
|
226
|
+
assert.equal(outcome, 'requeued');
|
|
227
|
+
assert.equal(job.status, 'pending');
|
|
228
|
+
assert.equal(job.exhaustedResolveAttempts, 1);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('once the cap is spent, the stranded row auto-skips like every other door', () => {
|
|
232
|
+
const job = strandedJob({ exhaustedResolveAttempts: NEEDS_REVIEW_RESOLVE_CAP });
|
|
233
|
+
const outcome = applyNeedsReviewAutoResolve(job, [job]);
|
|
234
|
+
assert.equal(outcome, 'skipped');
|
|
235
|
+
assert.equal(job.status, 'skipped');
|
|
236
|
+
assert.equal(job.needsReviewAutoResolvedSkip, true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('a live fix-plan child also blocks applyNeedsReviewAutoResolve, not just selection', () => {
|
|
240
|
+
const job = strandedJob();
|
|
241
|
+
const child = { slug: fixSlugFor(job), status: 'running' };
|
|
242
|
+
const outcome = applyNeedsReviewAutoResolve(job, [job, child]);
|
|
243
|
+
assert.equal(outcome, null, 'nothing to resolve while the fix-plan child is still live');
|
|
244
|
+
assert.equal(job.status, 'needs_review', 'must stay parked while the child is in flight');
|
|
245
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-supervisor-record.test.cjs — executeJob writes
|
|
3
|
+
* <runDir>/<slug>.supervisor.json synchronously at the `spawned pid=` point,
|
|
4
|
+
* i.e. before the child's first output and before onPid's mutate runs.
|
|
5
|
+
*
|
|
6
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-supervisor-record.test.cjs
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
let tmpHome;
|
|
16
|
+
let originalHome;
|
|
17
|
+
let executeJob;
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
originalHome = process.env.HOME;
|
|
21
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-supervisor-rec-'));
|
|
22
|
+
process.env.HOME = tmpHome;
|
|
23
|
+
({ executeJob } = require('../scheduler.cjs'));
|
|
24
|
+
});
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
process.env.HOME = originalHome;
|
|
27
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => { delete process.env.SM_CLAUDE_BIN; });
|
|
30
|
+
|
|
31
|
+
function writeSlowStub() {
|
|
32
|
+
const stubPath = path.join(os.tmpdir(), `sm-sup-stub-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
|
|
33
|
+
const body = `
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}, 500);
|
|
38
|
+
`;
|
|
39
|
+
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
40
|
+
return stubPath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('the supervisor record exists by the time onPid fires, before the child prints anything', async () => {
|
|
44
|
+
const mainCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-sup-main-'));
|
|
45
|
+
const worktreeCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-sup-wt-'));
|
|
46
|
+
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
47
|
+
const runDir = fs.mkdtempSync(path.join(tmpHome, '.claude', 'sm-sup-run-'));
|
|
48
|
+
try {
|
|
49
|
+
const slug = `sup-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
50
|
+
const prdsDir = path.join(mainCwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
51
|
+
fs.mkdirSync(prdsDir, { recursive: true });
|
|
52
|
+
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'body', 'utf8');
|
|
53
|
+
process.env.SM_CLAUDE_BIN = writeSlowStub();
|
|
54
|
+
|
|
55
|
+
const seen = {};
|
|
56
|
+
const result = await executeJob({ slug, cwd: mainCwd }, runDir, mainCwd, (pid) => {
|
|
57
|
+
const file = path.join(runDir, `${slug}.supervisor.json`);
|
|
58
|
+
seen.exists = fs.existsSync(file);
|
|
59
|
+
seen.record = seen.exists ? JSON.parse(fs.readFileSync(file, 'utf8')) : null;
|
|
60
|
+
seen.pid = pid;
|
|
61
|
+
seen.logHasOutput = fs.readFileSync(path.join(runDir, `${slug}.log`), 'utf8').includes('"type":"result"');
|
|
62
|
+
seen.metaExists = fs.existsSync(path.join(runDir, `${slug}.meta.json`));
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}, worktreeCwd);
|
|
65
|
+
|
|
66
|
+
expect(result.exitCode).toBe(0);
|
|
67
|
+
expect(seen.exists).toBe(true);
|
|
68
|
+
expect(seen.logHasOutput).toBe(false);
|
|
69
|
+
expect(seen.metaExists).toBe(false);
|
|
70
|
+
expect(seen.record).toMatchObject({
|
|
71
|
+
kind: 'job', slug, cwd: mainCwd, runId: path.basename(runDir), pid: seen.pid, pgid: seen.pid,
|
|
72
|
+
execCwd: worktreeCwd, worktreeDir: worktreeCwd, worktreeBranch: `sm-job/${slug}`, schedulerPid: process.pid,
|
|
73
|
+
});
|
|
74
|
+
expect(typeof seen.record.startedAt).toBe('number');
|
|
75
|
+
expect(seen.record.idleKillMs).toBeGreaterThan(0);
|
|
76
|
+
} finally {
|
|
77
|
+
fs.rmSync(mainCwd, { recursive: true, force: true });
|
|
78
|
+
fs.rmSync(worktreeCwd, { recursive: true, force: true });
|
|
79
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* subsequent tickQueue() call short-circuited on the stale flag forever,
|
|
9
9
|
* even though queue.json's `paused` field was correctly null.
|
|
10
10
|
*
|
|
11
|
-
* Run: timeout
|
|
11
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/scheduler-tick-cancel-token.test.cjs
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import { test } from 'vitest';
|
|
17
17
|
const assert = require('node:assert/strict');
|
|
18
18
|
const { applyPauseCleared, pickNextBatch } = require('../scheduler.cjs');
|
|
19
19
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-tick-wedge.test.cjs — tickQueue's serialized chain (tickTail) must
|
|
3
|
+
* survive a tick body that never settles: the body is bounded by a watchdog,
|
|
4
|
+
* the chain resets, and a late-resuming stale body is fenced off (no spawn, no
|
|
5
|
+
* mutate). mutateTail is never reset.
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-tick-wedge.test.cjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
|
|
17
|
+
let tmpHome;
|
|
18
|
+
let originalHome;
|
|
19
|
+
let originalClaudeBin;
|
|
20
|
+
let originalAutofix;
|
|
21
|
+
let originalWatchdog;
|
|
22
|
+
let originalClaudeVersion;
|
|
23
|
+
let scheduler;
|
|
24
|
+
let queueStore;
|
|
25
|
+
let auditLog;
|
|
26
|
+
let spawnMarker;
|
|
27
|
+
|
|
28
|
+
function auditKinds() {
|
|
29
|
+
try {
|
|
30
|
+
return fs.readFileSync(auditLog.auditLogPath(), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l).kind);
|
|
31
|
+
} catch { return []; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
originalHome = process.env.HOME;
|
|
36
|
+
originalClaudeBin = process.env.SM_CLAUDE_BIN;
|
|
37
|
+
originalAutofix = process.env.SM_AUTOFIX_DISABLE;
|
|
38
|
+
originalWatchdog = process.env.SM_TICK_WATCHDOG_MS;
|
|
39
|
+
// This test's spawn marker is written by the stub on ANY invocation, including the
|
|
40
|
+
// `claude --version` probe — so it needs the real probe, not the sandbox's pinned version.
|
|
41
|
+
originalClaudeVersion = process.env.SM_CLAUDE_VERSION;
|
|
42
|
+
delete process.env.SM_CLAUDE_VERSION;
|
|
43
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-tick-wedge-'));
|
|
44
|
+
process.env.HOME = tmpHome;
|
|
45
|
+
process.env.SM_AUTOFIX_DISABLE = '1';
|
|
46
|
+
// Any spawn of the "claude" binary drops a marker file: proof spawnJob ran.
|
|
47
|
+
spawnMarker = path.join(tmpHome, 'claude-spawned.marker');
|
|
48
|
+
const stubPath = path.join(tmpHome, 'claude-stub.cjs');
|
|
49
|
+
const body = `require('node:fs').appendFileSync(${JSON.stringify(spawnMarker)}, 'x');\n`
|
|
50
|
+
+ `process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok\\nSCHEDULER_VERDICT: PASS' }) + '\\n');\n`
|
|
51
|
+
+ 'process.exit(0);\n';
|
|
52
|
+
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}`, { mode: 0o755 });
|
|
53
|
+
process.env.SM_CLAUDE_BIN = stubPath;
|
|
54
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
|
|
55
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true });
|
|
56
|
+
scheduler = require('../scheduler.cjs');
|
|
57
|
+
queueStore = require('../lib/queueStore.cjs');
|
|
58
|
+
auditLog = require('../lib/auditLog.cjs');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterAll(() => {
|
|
62
|
+
process.env.HOME = originalHome;
|
|
63
|
+
for (const [k, v] of [['SM_CLAUDE_BIN', originalClaudeBin], ['SM_AUTOFIX_DISABLE', originalAutofix], ['SM_TICK_WATCHDOG_MS', originalWatchdog], ['SM_CLAUDE_VERSION', originalClaudeVersion]]) {
|
|
64
|
+
if (v === undefined) delete process.env[k]; else process.env[k] = v;
|
|
65
|
+
}
|
|
66
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('a never-settling tick body is declared wedged, the chain resets, the stale body is fenced, audit fires once', async () => {
|
|
70
|
+
process.env.SM_TICK_WATCHDOG_MS = '150';
|
|
71
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-tick-wedge-project-'));
|
|
72
|
+
const projDir = fs.mkdtempSync(path.join(tmpHome, '.claude', 'projects', 'sm-tick-wedge-fake-'));
|
|
73
|
+
fs.writeFileSync(path.join(projDir, 'session.jsonl'), `${JSON.stringify({ cwd })}\n`, 'utf8');
|
|
74
|
+
const slug = `9003-tick-wedge-${process.pid}`;
|
|
75
|
+
await scheduler.writeQueue({
|
|
76
|
+
jobs: [{ slug, title: 'x', cwd, status: 'pending', dependsOn: [] }],
|
|
77
|
+
config: {},
|
|
78
|
+
paused: null,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The first readMerged (the tick body's first await) hangs until released;
|
|
82
|
+
// every later call delegates to the real reader.
|
|
83
|
+
const realRead = queueStore.readMerged;
|
|
84
|
+
let release;
|
|
85
|
+
const hung = new Promise((r) => { release = r; });
|
|
86
|
+
let calls = 0;
|
|
87
|
+
queueStore.readMerged = function patched(...args) {
|
|
88
|
+
calls += 1;
|
|
89
|
+
if (calls === 1) return hung;
|
|
90
|
+
return realRead.apply(this, args);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const wedgedResult = await scheduler.tickQueue();
|
|
95
|
+
expect(wedgedResult).toMatchObject({ fired: false, reason: 'wedged' });
|
|
96
|
+
expect(auditKinds().filter((k) => k === 'tick_wedged')).toHaveLength(1);
|
|
97
|
+
|
|
98
|
+
// A second wedge in the same episode (no normal tick between) must not re-audit.
|
|
99
|
+
calls = 0;
|
|
100
|
+
const hung2 = new Promise(() => {});
|
|
101
|
+
queueStore.readMerged = function patched2(...args) {
|
|
102
|
+
calls += 1;
|
|
103
|
+
if (calls === 1) return hung2;
|
|
104
|
+
return realRead.apply(this, args);
|
|
105
|
+
};
|
|
106
|
+
const again = await scheduler.tickQueue();
|
|
107
|
+
expect(again).toMatchObject({ reason: 'wedged' });
|
|
108
|
+
expect(auditKinds().filter((k) => k === 'tick_wedged')).toHaveLength(1);
|
|
109
|
+
|
|
110
|
+
// Next tick runs on a fresh chain (real reader, generous budget).
|
|
111
|
+
queueStore.readMerged = realRead;
|
|
112
|
+
process.env.SM_TICK_WATCHDOG_MS = '30000';
|
|
113
|
+
// Launch gates this case controls, so the dispatch assertion below does not
|
|
114
|
+
// depend on host state: the CPU load gate (loadavg1/cores > 0.85 -> 'load-deferred')
|
|
115
|
+
// via tickQueue's bypassLoadGate, and the memory gate ('memory-deferred', reads
|
|
116
|
+
// MemAvailable from /proc/meminfo) via a stubbed fs.readFileSync for that one path.
|
|
117
|
+
const realReadFileSync = fs.readFileSync;
|
|
118
|
+
fs.readFileSync = function patchedReadFileSync(file, ...rest) {
|
|
119
|
+
if (file === '/proc/meminfo') return 'MemAvailable: 67108864 kB\n';
|
|
120
|
+
return realReadFileSync.call(this, file, ...rest);
|
|
121
|
+
};
|
|
122
|
+
let fresh;
|
|
123
|
+
try {
|
|
124
|
+
fresh = await scheduler.tickQueue({ bypassLoadGate: true });
|
|
125
|
+
} finally {
|
|
126
|
+
fs.readFileSync = realReadFileSync;
|
|
127
|
+
}
|
|
128
|
+
expect(fresh.reason).not.toBe('wedged');
|
|
129
|
+
|
|
130
|
+
// Let the fresh tick's own spawn (if any) finish, then clear the marker.
|
|
131
|
+
// The fresh tick must have dispatched: wait for the stub's marker, then for
|
|
132
|
+
// the row to leave running/pending (spawnJob is fire-and-forget).
|
|
133
|
+
const deadline = Date.now() + 10_000;
|
|
134
|
+
while (Date.now() < deadline) {
|
|
135
|
+
const row = queueStore.readMergedSync().jobs.find((j) => j.slug === slug);
|
|
136
|
+
if (fs.existsSync(spawnMarker) && (!row || (row.status !== 'running' && row.status !== 'pending'))) break;
|
|
137
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
138
|
+
}
|
|
139
|
+
expect(fs.existsSync(spawnMarker)).toBe(true);
|
|
140
|
+
fs.rmSync(spawnMarker, { force: true });
|
|
141
|
+
|
|
142
|
+
// Late-resume the stale body with a state that WOULD dispatch a pending job.
|
|
143
|
+
const before = queueStore.readMergedSync();
|
|
144
|
+
const staleState = {
|
|
145
|
+
...before,
|
|
146
|
+
jobs: [{ slug: `9004-tick-wedge-stale-${process.pid}`, title: 'y', cwd, status: 'pending', dependsOn: [] }],
|
|
147
|
+
config: { ...(before.config || {}) },
|
|
148
|
+
};
|
|
149
|
+
release(staleState);
|
|
150
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
151
|
+
expect(fs.existsSync(spawnMarker)).toBe(false);
|
|
152
|
+
} finally {
|
|
153
|
+
queueStore.readMerged = realRead;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('a slow-but-healthy tick under the budget is not flagged as wedged', async () => {
|
|
158
|
+
process.env.SM_TICK_WATCHDOG_MS = '2000';
|
|
159
|
+
const realRead = queueStore.readMerged;
|
|
160
|
+
const before = auditKinds().filter((k) => k === 'tick_wedged').length;
|
|
161
|
+
queueStore.readMerged = async function slow(...args) {
|
|
162
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
163
|
+
return realRead.apply(this, args);
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
const r = await scheduler.tickQueue();
|
|
167
|
+
expect(r?.reason).not.toBe('wedged');
|
|
168
|
+
expect(auditKinds().filter((k) => k === 'tick_wedged').length).toBe(before);
|
|
169
|
+
} finally {
|
|
170
|
+
queueStore.readMerged = realRead;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* requeue instead of burning a whole run + an auto-fix investigation on an
|
|
5
5
|
* outage that has nothing to do with the code.
|
|
6
6
|
*
|
|
7
|
-
* Run: timeout
|
|
7
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/scheduler-transient-failure.test.cjs
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import { test } from 'vitest';
|
|
13
13
|
const assert = require('node:assert/strict');
|
|
14
14
|
const fs = require('node:fs');
|
|
15
15
|
const os = require('node:os');
|
|
@@ -18,6 +18,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
18
18
|
const fs = require('node:fs');
|
|
19
19
|
const os = require('node:os');
|
|
20
20
|
const path = require('node:path');
|
|
21
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
21
22
|
const { execFileSync } = require('node:child_process');
|
|
22
23
|
|
|
23
24
|
let tmpHome;
|
|
@@ -30,6 +31,24 @@ function git(args, cwd) {
|
|
|
30
31
|
return execFileSync('git', args, { cwd, encoding: 'utf8' });
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// A completed job's PRD is archived immediately (archiveCompletedPrd, called
|
|
35
|
+
// for EVERY job that reaches effectiveStatus:'completed' — not just a
|
|
36
|
+
// retention-aged one), and the very next reconcile() pass (spawnJob's own
|
|
37
|
+
// broadcast({flush:true}) right after finishing) then finds the row's PRD
|
|
38
|
+
// gone and backfills+drops it from queue.json into history.jsonl in that
|
|
39
|
+
// SAME pass — see scheduler-reconcile-history-backfill.test.cjs. So a job
|
|
40
|
+
// that actually completed is found in EITHER queue.json OR history.jsonl,
|
|
41
|
+
// never guaranteed to still be the former.
|
|
42
|
+
function readTerminalRow(queuePath, projectCwd, slug) {
|
|
43
|
+
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
44
|
+
const row = jobs.find((j) => j.slug === slug);
|
|
45
|
+
if (row) return row;
|
|
46
|
+
const historyPath = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state', 'history.jsonl');
|
|
47
|
+
if (!fs.existsSync(historyPath)) return undefined;
|
|
48
|
+
const lines = fs.readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
49
|
+
return lines.map((l) => JSON.parse(l)).reverse().find((j) => j.slug === slug);
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
function initRepo(dir) {
|
|
34
53
|
fs.mkdirSync(dir, { recursive: true });
|
|
35
54
|
git(['init', '-q'], dir);
|
|
@@ -61,8 +80,9 @@ function writeProjectQueue(cwd, jobs) {
|
|
|
61
80
|
// test can prove whether — and where — it was actually spawned, then emits a
|
|
62
81
|
// clean stream-json success result.
|
|
63
82
|
function writeClaudeStub() {
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
return claudeStub.writeClaudeStub({
|
|
84
|
+
allowCommitUnder: os.tmpdir(),
|
|
85
|
+
body: `
|
|
66
86
|
const fs = require('fs');
|
|
67
87
|
const path = require('path');
|
|
68
88
|
const { execFileSync } = require('child_process');
|
|
@@ -73,14 +93,13 @@ function writeClaudeStub() {
|
|
|
73
93
|
// fallback case), where the run parks in needs_review instead, which is
|
|
74
94
|
// fine — this test only cares whether it ran in place, not its verdict.
|
|
75
95
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
runGit(['add', '-A']);
|
|
97
|
+
runGit(['commit', '-q', '-m', 'stub commit']);
|
|
78
98
|
} catch { /* not a git repo — best-effort only */ }
|
|
79
99
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok\\nSCHEDULER_VERDICT: PASS' }) + '\\n');
|
|
80
100
|
process.exit(0);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return stubPath;
|
|
101
|
+
`,
|
|
102
|
+
});
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
beforeAll(() => {
|
|
@@ -111,7 +130,13 @@ test('a job held behind the worktree cap stays pending with a heldReason, is nev
|
|
|
111
130
|
registerActiveProject(projectCwd);
|
|
112
131
|
|
|
113
132
|
const slug = `1112-test-held-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
114
|
-
|
|
133
|
+
// Epic-scoped, not the legacy flat prds/ dir: the flat dir is retired and
|
|
134
|
+
// swept into prds-archived/ by consolidateFlatPrds on every reconcile()
|
|
135
|
+
// pass once a row is no longer 'pending'/'running' — which would strand
|
|
136
|
+
// this test's row the moment it completes (reconcile()'s terminal-job
|
|
137
|
+
// "PRD gone -> archived on purpose" drop, see
|
|
138
|
+
// scheduler-reconcile-history-backfill.test.cjs).
|
|
139
|
+
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
115
140
|
fs.mkdirSync(prdsDir, { recursive: true });
|
|
116
141
|
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Held behind the worktree cap.', 'utf8');
|
|
117
142
|
const queuePath = writeProjectQueue(projectCwd, [{ slug, status: 'pending', cwd: projectCwd }]);
|
|
@@ -147,8 +172,8 @@ test('a job held behind the worktree cap stays pending with a heldReason, is nev
|
|
|
147
172
|
|
|
148
173
|
await spawnJob({ slug, cwd: projectCwd }, `${runId}-2`, runDir, projectCwd);
|
|
149
174
|
|
|
150
|
-
|
|
151
|
-
row
|
|
175
|
+
row = readTerminalRow(queuePath, projectCwd, slug);
|
|
176
|
+
expect(row).toBeTruthy();
|
|
152
177
|
expect(row.status).toBe('completed');
|
|
153
178
|
expect(row.heldReason).toBeUndefined();
|
|
154
179
|
} finally {
|
|
@@ -163,7 +188,8 @@ test('a non-git cwd still falls back to running in place (unchanged behaviour
|
|
|
163
188
|
registerActiveProject(projectCwd);
|
|
164
189
|
|
|
165
190
|
const slug = `1112-test-nongit-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
166
|
-
|
|
191
|
+
// Epic-scoped — see the sibling test above for why not the flat prds/ dir.
|
|
192
|
+
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
167
193
|
fs.mkdirSync(prdsDir, { recursive: true });
|
|
168
194
|
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Non-git cwd, must run in place.', 'utf8');
|
|
169
195
|
const queuePath = writeProjectQueue(projectCwd, [{ slug, status: 'pending', cwd: projectCwd }]);
|
|
@@ -23,6 +23,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
23
23
|
const fs = require('node:fs');
|
|
24
24
|
const os = require('node:os');
|
|
25
25
|
const path = require('node:path');
|
|
26
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
26
27
|
|
|
27
28
|
let tmpHome;
|
|
28
29
|
let originalHome;
|
|
@@ -48,16 +49,13 @@ afterEach(() => {
|
|
|
48
49
|
// where it actually ran), captures the '-p' prompt, then emits a stream-json
|
|
49
50
|
// success result and exits 0.
|
|
50
51
|
function writeClaudeStub() {
|
|
51
|
-
|
|
52
|
-
const body = `
|
|
52
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
53
53
|
const fs = require('fs');
|
|
54
54
|
const path = require('path');
|
|
55
55
|
fs.writeFileSync(path.join(process.cwd(), 'ran-here.marker'), 'yes', 'utf8');
|
|
56
56
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
|
|
57
57
|
process.exit(0);
|
|
58
|
-
|
|
59
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
60
|
-
return stubPath;
|
|
58
|
+
` });
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
test('executeJob spawns the child in execCwd (worktree) while resolving the PRD from job.cwd (main tree)', async () => {
|