claude-code-session-manager 0.98.0 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/cli.cjs +16 -4
- package/bin/node-floor.cjs +41 -0
- package/dist/assets/{DataModel-DCEgMTjH.js → DataModel-HdR0AxFq.js} +1 -1
- package/dist/assets/{History-Bfdoywlh.js → History-Dr4Jr_5z.js} +2 -2
- package/dist/assets/{Hooks-CKDJjTJV.js → Hooks-NhrvH3JP.js} +3 -3
- package/dist/assets/{Library-7akfgy67.js → Library-Dv_EtIMr.js} +1 -1
- package/dist/assets/{MarkdownEditor-DDesiqet.js → MarkdownEditor-DTKDWV_n.js} +1 -1
- package/dist/assets/{McpServers-D4lZtXof.js → McpServers-M034G9B3.js} +2 -2
- package/dist/assets/{Memory-CeI8G1w5.js → Memory-DzF1B4gx.js} +6 -6
- package/dist/assets/{Permissions-BGleLja4.js → Permissions-DrgbSGWl.js} +3 -3
- package/dist/assets/{Plugins-P4eCXy_S.js → Plugins-Bd3ef35w.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BFczzI1B.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
- package/dist/assets/{SaveBar-l9jnEMES.js → SaveBar-rimJem-Y.js} +1 -1
- package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
- package/dist/assets/{ScopeSwitcher-CjJi8ST5.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
- package/dist/assets/Settings-Dc1F_sSg.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CjbO01Sg.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
- package/dist/assets/{Skills-BxZ9_fCR.js → Skills-DRXd18gu.js} +2 -2
- package/dist/assets/{SystemPrompt-D8vfFb8b.js → SystemPrompt-ahy5-Fwe.js} +1 -1
- package/dist/assets/{TagLibrary-2vzUo7YL.js → TagLibrary-C_tToDeL.js} +1 -1
- package/dist/assets/{TiptapBody-B8ZfbK90.js → TiptapBody-DvdefS6K.js} +1 -1
- package/dist/assets/{Toggle-BbW4nGGf.js → Toggle-6Jl5Njc6.js} +1 -1
- package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
- package/dist/assets/{index-Cd98Q1lD.js → index-vx8O73l8.js} +497 -502
- package/dist/assets/{settingsSchema-IhUfucCF.js → settingsSchema-D6iRguNV.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +25 -29
- package/plugins/CLAUDE.md +6 -6
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +21 -19
- package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
- package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
- package/scripts/README.md +3 -11
- package/scripts/audit-ops-hygiene.cjs +3 -3
- package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
- package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
- package/scripts/mint-epic.cjs +2 -3
- package/scripts/scheduler-mcp-server.cjs +36 -7
- package/src/main/agentLibrary.cjs +22 -2
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +41 -38
- package/src/main/docEdit.cjs +4 -1
- package/src/main/files.cjs +2 -5
- package/src/main/health.cjs +1 -1
- package/src/main/index.cjs +23 -10
- package/src/main/ipcSchemas.cjs +28 -35
- package/src/main/lib/agentPersonaSchema.cjs +13 -2
- package/src/main/lib/atomicFs.cjs +116 -0
- package/src/main/lib/branchSweep.cjs +13 -12
- package/src/main/lib/buildTarget.cjs +3 -3
- package/src/main/lib/claudeCliCaps.cjs +129 -0
- package/src/main/lib/credentials.cjs +2 -4
- package/src/main/lib/crossProjectFeedback.cjs +3 -3
- package/src/main/lib/cwdClassify.cjs +15 -3
- package/src/main/lib/definitionOfDone.cjs +207 -125
- package/src/main/lib/dodDrainHook.cjs +11 -5
- package/src/main/lib/epicMint.cjs +2 -4
- package/src/main/lib/epicStatusMirror.cjs +8 -10
- package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
- package/src/main/lib/gateAuthority.cjs +96 -0
- package/src/main/lib/gitExec.cjs +69 -0
- package/src/main/lib/gitWorktree.cjs +656 -92
- package/src/main/lib/instanceLock.cjs +5 -8
- package/src/main/lib/launchFailure.cjs +2 -3
- package/src/main/lib/macroLibrary.cjs +386 -0
- package/src/main/lib/mcpToolCatalog.cjs +26 -17
- package/src/main/lib/needsReviewLedger.cjs +1 -1
- package/src/main/lib/opsErrorLog.cjs +11 -1
- package/src/main/lib/opsOwnership.cjs +0 -5
- package/src/main/lib/pidAlive.cjs +32 -0
- package/src/main/lib/prdCreate.cjs +121 -12
- package/src/main/lib/prdDisposition.cjs +2 -2
- package/src/main/lib/prdGateFiles.cjs +284 -0
- package/src/main/lib/prdLocations.cjs +61 -0
- package/src/main/lib/prdMigration.cjs +2 -3
- package/src/main/lib/prdSizing.cjs +2 -1
- package/src/main/lib/promptSessionSchema.cjs +10 -2
- package/src/main/lib/rcaReport.cjs +4 -6
- package/src/main/lib/reaperHelpers.cjs +8 -1
- package/src/main/lib/reservationExpiry.cjs +6 -1
- package/src/main/lib/reviewNotice.cjs +263 -0
- package/src/main/lib/runClaudeP.cjs +4 -1
- package/src/main/lib/schedulerPaths.cjs +22 -2
- package/src/main/lib/sessionSlots.cjs +2 -4
- package/src/main/lib/shippedPersonaSeeds.cjs +179 -0
- package/src/main/lib/timeoutShim.cjs +123 -0
- package/src/main/lib/timeoutShimScript.cjs +275 -0
- package/src/main/lib/upgradeDrain.cjs +4 -10
- package/src/main/lib/watchdogHelpers.cjs +12 -25
- package/src/main/lib/workTypeLibrary.cjs +7 -1
- package/src/main/promptSessionEvents.cjs +37 -13
- package/src/main/scheduler.cjs +910 -198
- package/src/main/seedAgentPersonas.cjs +278 -23
- package/src/main/supervisor.cjs +4 -1
- package/src/main/templates/PRD_AUTHORING.md +126 -406
- package/src/preload/__tests__/preload-surface.test.cjs +68 -0
- package/src/preload/api.d.ts +41 -94
- package/src/preload/index.cjs +14 -9
- package/src/seed/agents/architect.md +1 -0
- package/src/seed/agents/dev-lead.md +17 -18
- package/src/seed/agents/project-home-builder.md +1 -0
- package/src/seed/agents/validator.md +10 -4
- package/dist/assets/HostBilko-CDVHE0hT.js +0 -1
- package/dist/assets/Scheduler-C_phpeUS.js +0 -16
- package/dist/assets/Settings-BIwZ3FGQ.js +0 -3
- package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
- package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
- package/src/main/__tests__/agentLibrary.test.cjs +0 -276
- package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
- package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
- package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
- package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
- package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
- package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
- package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
- package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
- package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
- package/src/main/__tests__/chat-queue.test.cjs +0 -97
- package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
- package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
- package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
- package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
- package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
- package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
- package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
- package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
- package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
- package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
- package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
- package/src/main/__tests__/docEdit.test.cjs +0 -244
- package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
- package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
- package/src/main/__tests__/dod-report.test.cjs +0 -304
- package/src/main/__tests__/dod-reverify.test.cjs +0 -285
- package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
- package/src/main/__tests__/epicMint.test.cjs +0 -332
- package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
- package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
- package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
- package/src/main/__tests__/exchanges.test.cjs +0 -122
- package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
- package/src/main/__tests__/extractJson.test.cjs +0 -51
- package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
- package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
- package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
- package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
- package/src/main/__tests__/health-credentials.test.cjs +0 -81
- package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
- package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
- package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
- package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
- package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
- package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
- package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
- package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
- package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
- package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
- package/src/main/__tests__/historyDashboard.test.cjs +0 -163
- package/src/main/__tests__/historyRollup.test.cjs +0 -333
- package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
- package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
- package/src/main/__tests__/kg-augment.test.cjs +0 -195
- package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
- package/src/main/__tests__/machineProfile.test.cjs +0 -152
- package/src/main/__tests__/mcpStatus.test.cjs +0 -61
- package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
- package/src/main/__tests__/memoryStale.test.cjs +0 -88
- package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
- package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
- package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
- package/src/main/__tests__/personaMerge.test.cjs +0 -169
- package/src/main/__tests__/planValidator.test.cjs +0 -125
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
- package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
- package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
- package/src/main/__tests__/prdAgentType.test.cjs +0 -103
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
- package/src/main/__tests__/prdCreate.test.cjs +0 -979
- package/src/main/__tests__/prdCreateAdoption.test.cjs +0 -205
- package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
- package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
- package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
- package/src/main/__tests__/prdLocations.test.cjs +0 -195
- package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
- package/src/main/__tests__/prdMigration.test.cjs +0 -349
- package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
- package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
- package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
- package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
- package/src/main/__tests__/prdSizing.test.cjs +0 -106
- package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
- package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
- package/src/main/__tests__/proc-role-env.test.cjs +0 -125
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
- package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
- package/src/main/__tests__/projectPages.test.cjs +0 -137
- package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
- package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
- package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
- package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
- package/src/main/__tests__/pty-write-result.test.cjs +0 -46
- package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
- package/src/main/__tests__/queueHistory.test.cjs +0 -355
- package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
- package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
- package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -211
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
- package/src/main/__tests__/rcaReport.test.cjs +0 -266
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
- package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
- package/src/main/__tests__/runLogRetention.test.cjs +0 -489
- package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
- package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
- package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
- package/src/main/__tests__/runVerify.test.cjs +0 -1784
- package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
- package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
- package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
- package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
- package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
- package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
- package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
- package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
- package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
- package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
- package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
- package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
- package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
- package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
- package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
- package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
- package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
- package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
- package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
- package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
- package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
- package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
- package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
- package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
- package/src/main/__tests__/scheduler-mutate-reentrancy.test.cjs +0 -117
- package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
- package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
- package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
- package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
- package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
- package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
- package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
- package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
- package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
- package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
- package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
- package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
- package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
- package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
- package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
- package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
- package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
- package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
- package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
- package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
- package/src/main/__tests__/seedStatus.test.cjs +0 -100
- package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
- package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
- package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
- package/src/main/__tests__/telemetryContract.test.cjs +0 -930
- package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
- package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
- package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
- package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
- package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
- package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
- package/src/main/__tests__/validationSentinels.test.cjs +0 -84
- package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
- package/src/main/bilkoHost.cjs +0 -314
- package/src/main/bilkoHostCore.cjs +0 -89
- package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
- package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
- package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
- package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
- package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
- package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
- package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
- package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
- package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
- package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
- package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
- package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
- package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
- package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
- package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
- package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
- package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
- package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
- package/src/main/lib/__tests__/gateFixtures.json +0 -20
- package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
- package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
- package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
- package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
- package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
- package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
- package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
- package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
- package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
- package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
- package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
- package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
- package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
- package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
- package/src/main/lib/__tests__/procName.test.cjs +0 -92
- package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
- package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
- package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
- package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
- package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
- package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
- package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
- package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
- package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
- package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
- package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -441
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
- package/src/main/lib/kgExchangePairing.cjs +0 -75
|
@@ -1,577 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* reaperHelpers.test.cjs — selectReapableJobs: the pure predicate behind
|
|
3
|
-
* reapDeadRunningJobs()'s pidless-zombie fix.
|
|
4
|
-
*
|
|
5
|
-
* Prior behaviour: a 'running' row with no runtime.pid was skipped forever,
|
|
6
|
-
* unconditionally ("spawn may be mid-flight; give it a cycle") — no age
|
|
7
|
-
* bound. Repro 2026-09-01: a row sat pidless for 464 minutes against a
|
|
8
|
-
* 24-minute estimate, empty run dir, no claude process anywhere — nothing
|
|
9
|
-
* pid-bound (deadman, idle watchdog, supervisor) could ever see it.
|
|
10
|
-
*
|
|
11
|
-
* Run: timeout 120 npx vitest run src/main/lib/__tests__/reaperHelpers.test.cjs
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
'use strict';
|
|
15
|
-
|
|
16
|
-
// vitest, NOT node:test — this repo's suite is vitest-only (CLAUDE.md).
|
|
17
|
-
import { test } from 'vitest';
|
|
18
|
-
const assert = require('node:assert/strict');
|
|
19
|
-
const fs = require('node:fs');
|
|
20
|
-
const os = require('node:os');
|
|
21
|
-
const path = require('node:path');
|
|
22
|
-
const { spawn } = require('node:child_process');
|
|
23
|
-
const {
|
|
24
|
-
selectReapableJobs, mapOutcomeToGateOutcome, classifyRunOutcome,
|
|
25
|
-
findLiveProcessForJob, logHasOutput, resolvePidlessGateOutcome,
|
|
26
|
-
isAlreadySatisfiedOnMain, resolvePidlessFailureOverride,
|
|
27
|
-
readSpawnedPidFromLog, isLogFresh,
|
|
28
|
-
} = require('../reaperHelpers.cjs');
|
|
29
|
-
const { detectRateLimitInLog } = require('../rateLimitDetect.cjs');
|
|
30
|
-
|
|
31
|
-
const NOW = Date.parse('2026-09-01T12:00:00.000Z');
|
|
32
|
-
const agoMin = (m) => new Date(NOW - m * 60_000).toISOString();
|
|
33
|
-
const GRACE = 10 * 60_000;
|
|
34
|
-
const alwaysDead = () => false;
|
|
35
|
-
const alwaysAlive = () => true;
|
|
36
|
-
|
|
37
|
-
test('pidless + older than grace → reaped, with a named reason', () => {
|
|
38
|
-
const jobs = [{ slug: 'zombie', status: 'running', startedAt: agoMin(464) }];
|
|
39
|
-
const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
40
|
-
assert.strictEqual(warnings.length, 0);
|
|
41
|
-
assert.strictEqual(reapable.length, 1);
|
|
42
|
-
assert.strictEqual(reapable[0].slug, 'zombie');
|
|
43
|
-
assert.strictEqual(reapable[0].pid, null);
|
|
44
|
-
assert.strictEqual(reapable[0].pidless, true);
|
|
45
|
-
assert.match(reapable[0].reason, /no runtime\.pid recorded after 10m/);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('pidless + within grace → skipped (the genuine mid-flight-spawn case)', () => {
|
|
49
|
-
const jobs = [{ slug: 'fresh', status: 'running', startedAt: agoMin(2) }];
|
|
50
|
-
const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
51
|
-
assert.deepStrictEqual(reapable, []);
|
|
52
|
-
assert.deepStrictEqual(warnings, []);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('pidless + exactly at grace boundary → reaped (>= not >)', () => {
|
|
56
|
-
const jobs = [{ slug: 'boundary', status: 'running', startedAt: agoMin(10) }];
|
|
57
|
-
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
58
|
-
assert.strictEqual(reapable.length, 1);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('live pid → skipped regardless of age', () => {
|
|
62
|
-
const jobs = [{ slug: 'live', status: 'running', runtime: { pid: 4242 }, startedAt: agoMin(500) }];
|
|
63
|
-
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
64
|
-
assert.deepStrictEqual(reapable, []);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// ---------- dispatchPhase breadcrumb (PRD: dispatch-region diagnostic) ----------
|
|
68
|
-
|
|
69
|
-
test('pidless + dispatchPhase present → reason names the phase and timestamp', () => {
|
|
70
|
-
const jobs = [{
|
|
71
|
-
slug: 'zombie-phase',
|
|
72
|
-
status: 'running',
|
|
73
|
-
startedAt: agoMin(464),
|
|
74
|
-
dispatchPhase: 'baseline-persisted',
|
|
75
|
-
dispatchPhaseAt: '2026-09-12T02:46:10.000Z',
|
|
76
|
-
}];
|
|
77
|
-
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
78
|
-
assert.strictEqual(reapable.length, 1);
|
|
79
|
-
assert.strictEqual(
|
|
80
|
-
reapable[0].reason,
|
|
81
|
-
'reaped: no runtime.pid recorded after 10m — spawn never completed '
|
|
82
|
-
+ '(last dispatch phase: baseline-persisted at 2026-09-12T02:46:10.000Z)',
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('pidless + no dispatchPhase (older-build row) → reason byte-identical to the pre-breadcrumb message', () => {
|
|
87
|
-
const jobs = [{ slug: 'zombie-noph', status: 'running', startedAt: agoMin(464) }];
|
|
88
|
-
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
89
|
-
assert.strictEqual(reapable.length, 1);
|
|
90
|
-
assert.strictEqual(
|
|
91
|
-
reapable[0].reason,
|
|
92
|
-
'reaped: no runtime.pid recorded after 10m — spawn never completed',
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('pidless + dispatchPhase present but dispatchPhaseAt missing/unparseable → phase named, no "at" clause, never throws', () => {
|
|
97
|
-
const jobs = [
|
|
98
|
-
{ slug: 'zombie-noat', status: 'running', startedAt: agoMin(464), dispatchPhase: 'exec-entered' },
|
|
99
|
-
{ slug: 'zombie-badat', status: 'running', startedAt: agoMin(464), dispatchPhase: 'exec-entered', dispatchPhaseAt: 'not-a-date' },
|
|
100
|
-
];
|
|
101
|
-
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
102
|
-
assert.strictEqual(reapable.length, 2);
|
|
103
|
-
for (const r of reapable) {
|
|
104
|
-
assert.strictEqual(
|
|
105
|
-
r.reason,
|
|
106
|
-
'reaped: no runtime.pid recorded after 10m — spawn never completed (last dispatch phase: exec-entered)',
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('pidless + dispatchPhase === "spawned" with no pid → still reapable, breadcrumb changes message only', () => {
|
|
112
|
-
const jobs = [{
|
|
113
|
-
slug: 'torn-write',
|
|
114
|
-
status: 'running',
|
|
115
|
-
startedAt: agoMin(464),
|
|
116
|
-
dispatchPhase: 'spawned',
|
|
117
|
-
dispatchPhaseAt: agoMin(465),
|
|
118
|
-
}];
|
|
119
|
-
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
120
|
-
assert.strictEqual(reapable.length, 1);
|
|
121
|
-
assert.strictEqual(reapable[0].pidless, true);
|
|
122
|
-
assert.match(reapable[0].reason, /last dispatch phase: spawned/);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('dead pid → reaped exactly as before (pidless: false, no reason string)', () => {
|
|
126
|
-
const jobs = [{ slug: 'dead', status: 'running', runtime: { pid: 4242 }, startedAt: agoMin(500) }];
|
|
127
|
-
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysDead, grace: GRACE });
|
|
128
|
-
assert.strictEqual(reapable.length, 1);
|
|
129
|
-
assert.strictEqual(reapable[0].slug, 'dead');
|
|
130
|
-
assert.strictEqual(reapable[0].pid, 4242);
|
|
131
|
-
assert.strictEqual(reapable[0].pidless, false);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test('pidless + missing startedAt → skipped and warned, not reaped', () => {
|
|
135
|
-
const jobs = [{ slug: 'no-started-at', status: 'running' }];
|
|
136
|
-
const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
137
|
-
assert.deepStrictEqual(reapable, []);
|
|
138
|
-
assert.strictEqual(warnings.length, 1);
|
|
139
|
-
assert.strictEqual(warnings[0].slug, 'no-started-at');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('pidless + unparseable startedAt → skipped and warned, not reaped', () => {
|
|
143
|
-
const jobs = [{ slug: 'bad-started-at', status: 'running', startedAt: 'not-a-date' }];
|
|
144
|
-
const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
145
|
-
assert.deepStrictEqual(reapable, []);
|
|
146
|
-
assert.strictEqual(warnings.length, 1);
|
|
147
|
-
assert.strictEqual(warnings[0].slug, 'bad-started-at');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('pidless + startedAt in the future (clock skew) → skipped, not reaped', () => {
|
|
151
|
-
const jobs = [{ slug: 'future', status: 'running', startedAt: agoMin(-60) }];
|
|
152
|
-
const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
153
|
-
assert.deepStrictEqual(reapable, []);
|
|
154
|
-
assert.deepStrictEqual(warnings, []);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('non-running rows are never considered, pidless or not', () => {
|
|
158
|
-
const jobs = [
|
|
159
|
-
{ slug: 'a', status: 'pending', startedAt: agoMin(500) },
|
|
160
|
-
{ slug: 'b', status: 'completed', startedAt: agoMin(500) },
|
|
161
|
-
{ slug: 'c', status: 'failed', runtime: { pid: 1 }, startedAt: agoMin(500) },
|
|
162
|
-
];
|
|
163
|
-
const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysDead, grace: GRACE });
|
|
164
|
-
assert.deepStrictEqual(reapable, []);
|
|
165
|
-
assert.deepStrictEqual(warnings, []);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('pidless + past grace + findLiveProcess finds a pid → recovered, never reaped', () => {
|
|
169
|
-
const jobs = [{ slug: 'zombie-alive', status: 'running', startedAt: agoMin(464) }];
|
|
170
|
-
const findLiveProcess = () => 2174739;
|
|
171
|
-
const { reapable, recovered, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE, findLiveProcess });
|
|
172
|
-
assert.deepStrictEqual(reapable, []);
|
|
173
|
-
assert.deepStrictEqual(warnings, []);
|
|
174
|
-
assert.strictEqual(recovered.length, 1);
|
|
175
|
-
assert.strictEqual(recovered[0].slug, 'zombie-alive');
|
|
176
|
-
assert.strictEqual(recovered[0].pid, 2174739);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test('pidless + past grace + findLiveProcess finds nothing → reaped exactly as today', () => {
|
|
180
|
-
const jobs = [{ slug: 'zombie-dead', status: 'running', startedAt: agoMin(464) }];
|
|
181
|
-
const findLiveProcess = () => null;
|
|
182
|
-
const { reapable, recovered } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE, findLiveProcess });
|
|
183
|
-
assert.deepStrictEqual(recovered, []);
|
|
184
|
-
assert.strictEqual(reapable.length, 1);
|
|
185
|
-
assert.strictEqual(reapable[0].pidless, true);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test('pidless + past grace + no findLiveProcess supplied → reaped exactly as before this PRD', () => {
|
|
189
|
-
const jobs = [{ slug: 'zombie', status: 'running', startedAt: agoMin(464) }];
|
|
190
|
-
const { reapable, recovered } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
191
|
-
assert.strictEqual(reapable.length, 1);
|
|
192
|
-
assert.deepStrictEqual(recovered, []);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test('mixed batch: each row classified independently', () => {
|
|
196
|
-
const jobs = [
|
|
197
|
-
{ slug: 'zombie', status: 'running', startedAt: agoMin(464) },
|
|
198
|
-
{ slug: 'fresh', status: 'running', startedAt: agoMin(2) },
|
|
199
|
-
{ slug: 'live', status: 'running', runtime: { pid: 1 }, startedAt: agoMin(500) },
|
|
200
|
-
{ slug: 'dead', status: 'running', runtime: { pid: 2 }, startedAt: agoMin(500) },
|
|
201
|
-
{ slug: 'bad-started-at', status: 'running', startedAt: 'nope' },
|
|
202
|
-
];
|
|
203
|
-
const pidAlive = (pid) => pid === 1;
|
|
204
|
-
const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive, grace: GRACE });
|
|
205
|
-
assert.deepStrictEqual(reapable.map((r) => r.slug).sort(), ['dead', 'zombie']);
|
|
206
|
-
assert.deepStrictEqual(warnings.map((w) => w.slug), ['bad-started-at']);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// mapOutcomeToGateOutcome — classifyRunOutcome's four-way result collapsed
|
|
210
|
-
// onto the persisted `gateOutcome` field (PRD 1109). 'no_result' -> 'never_ran'
|
|
211
|
-
// is the one non-obvious mapping: a missing verdict event means the gate
|
|
212
|
-
// never fired, a distinct fact from 'failed' (a verdict event exists and
|
|
213
|
-
// says error).
|
|
214
|
-
test('mapOutcomeToGateOutcome: success -> passed', () => {
|
|
215
|
-
assert.strictEqual(mapOutcomeToGateOutcome('success'), 'passed');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test('mapOutcomeToGateOutcome: failed -> failed', () => {
|
|
219
|
-
assert.strictEqual(mapOutcomeToGateOutcome('failed'), 'failed');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('mapOutcomeToGateOutcome: no_result -> never_ran', () => {
|
|
223
|
-
assert.strictEqual(mapOutcomeToGateOutcome('no_result'), 'never_ran');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test('mapOutcomeToGateOutcome: unknown -> unknown', () => {
|
|
227
|
-
assert.strictEqual(mapOutcomeToGateOutcome('unknown'), 'unknown');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// detectRateLimitInLog / classifyRunOutcome — PRD 1117: the reaper's
|
|
231
|
-
// rate-limit detection must be the SAME single source of truth spawnJob
|
|
232
|
-
// uses (detectRateLimitInLog, shared via lib/rateLimitDetect.cjs), and a
|
|
233
|
-
// rate-limited death must classify as a NEW, distinct 'rate_limited'
|
|
234
|
-
// outcome — never collapsed into 'failed', and never re-labelling
|
|
235
|
-
// 'success'/'failed'/'no_result' (which keep their prior meanings, per the
|
|
236
|
-
// mapOutcomeToGateOutcome tests above, unmodified).
|
|
237
|
-
|
|
238
|
-
function writeTmpLog(contents) {
|
|
239
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-reaper-rate-limit-test-'));
|
|
240
|
-
const p = path.join(dir, 'run.log');
|
|
241
|
-
fs.writeFileSync(p, contents);
|
|
242
|
-
return p;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
test('detectRateLimitInLog: rateLimitType":"five_hour" variant', () => {
|
|
246
|
-
const p = writeTmpLog('{"type":"result","rate_limit_info":{"rateLimitType":"five_hour"}}\n');
|
|
247
|
-
assert.strictEqual(detectRateLimitInLog(p), true);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('detectRateLimitInLog: rateLimitType":"seven_day" variant (missed pre-PRD-1117)', () => {
|
|
251
|
-
const p = writeTmpLog('{"type":"rate_limit_event","rate_limit_info":{"rateLimitType":"seven_day","unifiedWindows":{"five_hour":{"utilization":0}}}}\n');
|
|
252
|
-
assert.strictEqual(detectRateLimitInLog(p), true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test('detectRateLimitInLog: api_error_status":429 variant', () => {
|
|
256
|
-
const p = writeTmpLog('{"type":"result","is_error":true,"api_error_status":429,"result":"nope"}\n');
|
|
257
|
-
assert.strictEqual(detectRateLimitInLog(p), true);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test('detectRateLimitInLog: "You\'ve reached your <model> limit" variant (missed pre-PRD-1117)', () => {
|
|
261
|
-
const p = writeTmpLog('{"type":"result","result":"You\'ve reached your Fable limit. Switch to another model."}\n');
|
|
262
|
-
assert.strictEqual(detectRateLimitInLog(p), true);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test('detectRateLimitInLog: real evidence log (2026-09-05 204-mercury-steam-horse 429)', () => {
|
|
266
|
-
const evidencePath = '/home/bilko/.claude/session-manager/scheduled-plans/runs/2026-09-05T16-32-18-381Z/204-mercury-steam-horse.log';
|
|
267
|
-
if (!fs.existsSync(evidencePath)) return; // machine-local fixture; skip elsewhere
|
|
268
|
-
assert.strictEqual(detectRateLimitInLog(evidencePath), true);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test('classifyRunOutcome: a 429 log tail classifies as the new distinct rate_limited outcome, not failed', () => {
|
|
272
|
-
const p = writeTmpLog('{"type":"result","subtype":"success","is_error":true,"api_error_status":429,"rateLimitType":"seven_day","result":"You\'ve reached your Fable limit."}\n');
|
|
273
|
-
assert.strictEqual(classifyRunOutcome(p), 'rate_limited');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test('classifyRunOutcome: a genuine error with no rate-limit signal still classifies as failed', () => {
|
|
277
|
-
const p = writeTmpLog('{"type":"result","subtype":"error","is_error":true,"result":"Error: expected 2 but got 3"}\n');
|
|
278
|
-
assert.strictEqual(classifyRunOutcome(p), 'failed');
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// REGRESSION GUARD (2026-09-05). The rate-limit check originally ran as the
|
|
282
|
-
// FIRST statement of classifyRunOutcome, before the result event was parsed.
|
|
283
|
-
// Because the CLI emits an informational rate_limit_event with
|
|
284
|
-
// status:"allowed_warning" on nearly every run once utilization is non-zero,
|
|
285
|
-
// that misclassified genuinely successful runs as 'rate_limited' — which in
|
|
286
|
-
// reapDeadRunningJobs re-queues finished work and spuriously pauses the whole
|
|
287
|
-
// machine. The fixture below is the shape of a REAL successful run log, not a
|
|
288
|
-
// synthetic one-liner: the allowed_warning event that every run carries, then
|
|
289
|
-
// a clean result. If someone moves the check back to the top, this goes red.
|
|
290
|
-
test('classifyRunOutcome: a SUCCESSFUL run whose tail carries an allowed_warning rate_limit_event is still success', () => {
|
|
291
|
-
const p = writeTmpLog([
|
|
292
|
-
'{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","resetsAt":1788643800,"rateLimitType":"five_hour","utilization":0.42,"isUsingOverage":false}}',
|
|
293
|
-
'{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","resetsAt":1789059600,"rateLimitType":"seven_day","utilization":0.59,"isUsingOverage":false}}',
|
|
294
|
-
'{"type":"result","subtype":"success","is_error":false,"num_turns":55,"result":"SCHEDULER_VERDICT: PASS"}',
|
|
295
|
-
].join('\n') + '\n');
|
|
296
|
-
assert.strictEqual(classifyRunOutcome(p), 'success');
|
|
297
|
-
assert.strictEqual(mapOutcomeToGateOutcome(classifyRunOutcome(p)), 'passed');
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// The same tail, but the run actually errored: NOW the rate-limit signal is
|
|
301
|
-
// what it claims to be, and the outcome must be the retryable one.
|
|
302
|
-
test('classifyRunOutcome: the same allowed_warning tail on an ERRORED run classifies as rate_limited', () => {
|
|
303
|
-
const p = writeTmpLog([
|
|
304
|
-
'{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","rateLimitType":"seven_day","utilization":0.59}}',
|
|
305
|
-
'{"type":"result","subtype":"success","is_error":true,"api_error_status":429,"result":"You\'ve reached your Fable limit."}',
|
|
306
|
-
].join('\n') + '\n');
|
|
307
|
-
assert.strictEqual(classifyRunOutcome(p), 'rate_limited');
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test('classifyRunOutcome: a clean success log still classifies as success', () => {
|
|
311
|
-
const p = writeTmpLog('{"type":"result","subtype":"success","is_error":false,"result":"done"}\n');
|
|
312
|
-
assert.strictEqual(classifyRunOutcome(p), 'success');
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// findLiveProcessForJob — the /proc positive liveness scan behind the
|
|
316
|
-
// 2026-09-06 stranded-branch incident (234-uranus-eight-tails-ox: marked
|
|
317
|
-
// failed/never_ran while its process was alive and still writing to its
|
|
318
|
-
// worktree). Linux-only, and a safe no-op everywhere else per its own header.
|
|
319
|
-
|
|
320
|
-
test('findLiveProcessForJob: finds a live process via /proc/<pid>/cwd match', async () => {
|
|
321
|
-
if (process.platform !== 'linux' || !fs.existsSync('/proc')) return;
|
|
322
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-reaper-liveness-'));
|
|
323
|
-
const child = spawn('sleep', ['5'], { cwd: dir, stdio: 'ignore' });
|
|
324
|
-
try {
|
|
325
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
326
|
-
const pid = findLiveProcessForJob({ slug: 'irrelevant-slug' }, { worktreeDir: dir });
|
|
327
|
-
assert.strictEqual(pid, child.pid);
|
|
328
|
-
} finally {
|
|
329
|
-
child.kill('SIGKILL');
|
|
330
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test('findLiveProcessForJob: no live process anywhere → null', () => {
|
|
335
|
-
if (process.platform !== 'linux' || !fs.existsSync('/proc')) return;
|
|
336
|
-
const pid = findLiveProcessForJob(
|
|
337
|
-
{ slug: 'definitely-not-a-real-running-slug-xyz123' },
|
|
338
|
-
{ worktreeDir: '/nonexistent/worktree/dir/xyz123' },
|
|
339
|
-
);
|
|
340
|
-
assert.strictEqual(pid, null);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// findLiveProcessForJob: runCwd — the 2026-09-13 in-place/shared-tree fix.
|
|
344
|
-
// A run with no dedicated worktree (no worktreeDir) and no slug in argv
|
|
345
|
-
// (dispatch never puts it there) is invisible to both existing matches; the
|
|
346
|
-
// runCwd+cmdline-claude combo match exists for exactly this shape. `sleep`
|
|
347
|
-
// is used as the /proc-visible process here (not a real `claude` binary), so
|
|
348
|
-
// this exercises the cwd-plus-cmdline-substring code path directly rather
|
|
349
|
-
// than depending on a real claude binary being installed.
|
|
350
|
-
test('findLiveProcessForJob: runCwd match requires BOTH cwd AND a claude-looking cmdline — bare cwd match is never sufficient', async () => {
|
|
351
|
-
if (process.platform !== 'linux' || !fs.existsSync('/proc')) return;
|
|
352
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-reaper-runcwd-'));
|
|
353
|
-
// Runs in `dir` but its cmdline has no "claude" in it — must NOT match.
|
|
354
|
-
const child = spawn('sleep', ['5'], { cwd: dir, stdio: 'ignore' });
|
|
355
|
-
try {
|
|
356
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
357
|
-
const pid = findLiveProcessForJob({ slug: 'unrelated-slug' }, { runCwd: dir });
|
|
358
|
-
assert.strictEqual(pid, null, 'a bare cwd match without a claude-looking cmdline must never match — shared-tree cross-match hazard');
|
|
359
|
-
} finally {
|
|
360
|
-
child.kill('SIGKILL');
|
|
361
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// logHasOutput — the literal "did the run dir produce any log output" check
|
|
366
|
-
// that gates whether a pidless reap may assert gateOutcome: 'never_ran'.
|
|
367
|
-
|
|
368
|
-
test('logHasOutput: missing file → false', () => {
|
|
369
|
-
assert.strictEqual(logHasOutput('/nonexistent/path/does-not-exist.log'), false);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test('logHasOutput: empty file → false', () => {
|
|
373
|
-
const p = writeTmpLog('');
|
|
374
|
-
assert.strictEqual(logHasOutput(p), false);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
test('logHasOutput: file with content → true', () => {
|
|
378
|
-
const p = writeTmpLog('some real log output\n');
|
|
379
|
-
assert.strictEqual(logHasOutput(p), true);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
test('logHasOutput: null path → false', () => {
|
|
383
|
-
assert.strictEqual(logHasOutput(null), false);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// resolvePidlessGateOutcome — never_ran is asserted ONLY when the run dir has
|
|
387
|
-
// no log output; a run dir WITH output must not claim never_ran even if no
|
|
388
|
-
// clean result event was found in it.
|
|
389
|
-
|
|
390
|
-
test('resolvePidlessGateOutcome: no output → never_ran, regardless of raw outcome (terminalizes as today)', () => {
|
|
391
|
-
assert.strictEqual(resolvePidlessGateOutcome('no_result', false), 'never_ran');
|
|
392
|
-
assert.strictEqual(resolvePidlessGateOutcome('success', false), 'never_ran');
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test('resolvePidlessGateOutcome: output present + no_result → failed, never never_ran', () => {
|
|
396
|
-
assert.strictEqual(resolvePidlessGateOutcome('no_result', true), 'failed');
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test('resolvePidlessGateOutcome: output present + success → passed', () => {
|
|
400
|
-
assert.strictEqual(resolvePidlessGateOutcome('success', true), 'passed');
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
test('resolvePidlessGateOutcome: output present + failed → failed', () => {
|
|
404
|
-
assert.strictEqual(resolvePidlessGateOutcome('failed', true), 'failed');
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// isAlreadySatisfiedOnMain — PRD 1136. Pure decision layer: given the
|
|
408
|
-
// already-git-queried list of satisfying commits, decide whether the
|
|
409
|
-
// commit-guard's 'silent_no_op' verdict should be overridden to 'completed'.
|
|
410
|
-
|
|
411
|
-
test('isAlreadySatisfiedOnMain: a satisfying commit → completed-shaped verdict naming the sha', () => {
|
|
412
|
-
const verdict = isAlreadySatisfiedOnMain(['5de4134abc123', 'a96de98def456']);
|
|
413
|
-
assert.strictEqual(verdict.sha, '5de4134abc123');
|
|
414
|
-
assert.strictEqual(verdict.verdict, 'already_satisfied_on_main');
|
|
415
|
-
assert.match(verdict.reason, /5de4134abc123/);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
test('isAlreadySatisfiedOnMain: no satisfying commits → null — the gate must not be weakened into always-passing', () => {
|
|
419
|
-
assert.strictEqual(isAlreadySatisfiedOnMain([]), null);
|
|
420
|
-
assert.strictEqual(isAlreadySatisfiedOnMain(null), null);
|
|
421
|
-
assert.strictEqual(isAlreadySatisfiedOnMain(undefined), null);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// resolvePidlessFailureOverride — PRD 1173. A pidless reap must never stamp
|
|
425
|
-
// 'failed' on a row that already carries proof (landedCommit) that its work
|
|
426
|
-
// shipped, purely because reproting bookkeeping (runtime.pid) was lost.
|
|
427
|
-
|
|
428
|
-
test('resolvePidlessFailureOverride: row with a landedCommit → override naming the recorded commit sha', () => {
|
|
429
|
-
const override = resolvePidlessFailureOverride({ slug: 'shipped-but-pidless', landedCommit: 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f' });
|
|
430
|
-
assert.ok(override, 'a row with landedCommit must produce a failure override');
|
|
431
|
-
assert.strictEqual(override.verdict, 'pidless_reap_with_landed_commit');
|
|
432
|
-
assert.strictEqual(override.landedCommit, 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f');
|
|
433
|
-
assert.match(override.reason, /f3e35a7f9e49f13a9eb8c338b0d086160e32a87f/, 'the reason string must name the recorded commit sha so a human can go straight to `git show <sha>`');
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
test('resolvePidlessFailureOverride: row with no landedCommit → null, existing failed transition untouched', () => {
|
|
437
|
-
assert.strictEqual(resolvePidlessFailureOverride({ slug: 'genuinely-never-ran' }), null);
|
|
438
|
-
assert.strictEqual(resolvePidlessFailureOverride({ slug: 'genuinely-never-ran', landedCommit: null }), null);
|
|
439
|
-
assert.strictEqual(resolvePidlessFailureOverride({ slug: 'genuinely-never-ran', landedCommit: '' }), null);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// readSpawnedPidFromLog — recovers the pid a pidless row's spawn actually
|
|
443
|
-
// produced from the run's own log (2026-09-13 incident: runId
|
|
444
|
-
// 2026-09-13T16-15-56-818Z spawned pid=3138458, logged it, but the
|
|
445
|
-
// runtime.pid persistence mutate failed silently and the pidless-grace
|
|
446
|
-
// reaper terminalized a live job it could have found here).
|
|
447
|
-
|
|
448
|
-
test('readSpawnedPidFromLog: parses the scheduler\'s own spawned-pid line', () => {
|
|
449
|
-
const p = writeTmpLog('some preamble\n[scheduler] spawned pid=3138458 sessionId=345ca5b1-c7a7-4167-9b55-f4905e3afe01 (process group)\n\nmore output\n');
|
|
450
|
-
assert.strictEqual(readSpawnedPidFromLog(p), 3138458);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
test('readSpawnedPidFromLog: missing file → null', () => {
|
|
454
|
-
assert.strictEqual(readSpawnedPidFromLog('/nonexistent/path/does-not-exist.log'), null);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
test('readSpawnedPidFromLog: empty file → null', () => {
|
|
458
|
-
const p = writeTmpLog('');
|
|
459
|
-
assert.strictEqual(readSpawnedPidFromLog(p), null);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
test('readSpawnedPidFromLog: log with no spawned-pid line → null', () => {
|
|
463
|
-
const p = writeTmpLog('{"type":"result","subtype":"success","is_error":false,"result":"done"}\n');
|
|
464
|
-
assert.strictEqual(readSpawnedPidFromLog(p), null);
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
test('readSpawnedPidFromLog: null path → null', () => {
|
|
468
|
-
assert.strictEqual(readSpawnedPidFromLog(null), null);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// isLogFresh — pure predicate: a log modified within the window is positive
|
|
472
|
-
// proof of life, independent of runtime.pid bookkeeping.
|
|
473
|
-
|
|
474
|
-
test('isLogFresh: mtime within window → true', () => {
|
|
475
|
-
assert.strictEqual(isLogFresh(NOW - 60_000, NOW, 20 * 60_000), true);
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test('isLogFresh: mtime outside window → false', () => {
|
|
479
|
-
assert.strictEqual(isLogFresh(NOW - 30 * 60_000, NOW, 20 * 60_000), false);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
test('isLogFresh: missing/invalid mtime or window → false', () => {
|
|
483
|
-
assert.strictEqual(isLogFresh(null, NOW, 20 * 60_000), false);
|
|
484
|
-
assert.strictEqual(isLogFresh(NaN, NOW, 20 * 60_000), false);
|
|
485
|
-
assert.strictEqual(isLogFresh(NOW - 60_000, NOW, null), false);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
// selectReapableJobs — Part B wiring: a pidless row past grace now consults
|
|
489
|
-
// the run log for a pid, and a fresh log skips the row entirely, BEFORE
|
|
490
|
-
// falling back to findLiveProcess or the existing terminal reap.
|
|
491
|
-
|
|
492
|
-
test('selectReapableJobs: pidless + past grace + log yields a LIVE pid → recovered, never reapable, row stays running', () => {
|
|
493
|
-
const jobs = [{ slug: 'zombie-log-alive', status: 'running', startedAt: agoMin(464) }];
|
|
494
|
-
const pidAlive = (pid) => pid === 3138458;
|
|
495
|
-
const getLogPid = () => 3138458;
|
|
496
|
-
const { reapable, recovered, warnings } = selectReapableJobs(jobs, NOW, {
|
|
497
|
-
pidAlive, grace: GRACE, getLogPid,
|
|
498
|
-
});
|
|
499
|
-
assert.deepStrictEqual(reapable, []);
|
|
500
|
-
assert.deepStrictEqual(warnings, []);
|
|
501
|
-
assert.strictEqual(recovered.length, 1);
|
|
502
|
-
assert.strictEqual(recovered[0].slug, 'zombie-log-alive');
|
|
503
|
-
assert.strictEqual(recovered[0].pid, 3138458);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
test('selectReapableJobs: pidless + past grace + log yields a DEAD pid → still reaps (no regression)', () => {
|
|
507
|
-
const jobs = [{ slug: 'zombie-log-dead', status: 'running', startedAt: agoMin(464) }];
|
|
508
|
-
const getLogPid = () => 3138458;
|
|
509
|
-
const { reapable, recovered } = selectReapableJobs(jobs, NOW, {
|
|
510
|
-
pidAlive: alwaysDead, grace: GRACE, getLogPid,
|
|
511
|
-
});
|
|
512
|
-
assert.deepStrictEqual(recovered, []);
|
|
513
|
-
assert.strictEqual(reapable.length, 1);
|
|
514
|
-
assert.strictEqual(reapable[0].pidless, true);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
test('selectReapableJobs: pidless + past grace + FRESH log mtime → skipped entirely (neither reapable nor recovered)', () => {
|
|
518
|
-
const jobs = [{ slug: 'chatty-pidless', status: 'running', startedAt: agoMin(464) }];
|
|
519
|
-
const getLogMtimeMs = () => NOW - 60_000; // modified 1 minute ago
|
|
520
|
-
const { reapable, recovered, warnings } = selectReapableJobs(jobs, NOW, {
|
|
521
|
-
pidAlive: alwaysAlive, grace: GRACE, getLogMtimeMs, logFreshWindowMs: 20 * 60_000,
|
|
522
|
-
});
|
|
523
|
-
assert.deepStrictEqual(reapable, []);
|
|
524
|
-
assert.deepStrictEqual(recovered, []);
|
|
525
|
-
assert.strictEqual(warnings.length, 1);
|
|
526
|
-
assert.strictEqual(warnings[0].slug, 'chatty-pidless');
|
|
527
|
-
assert.match(warnings[0].reason, /log evidence/);
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
test('selectReapableJobs: pidless + past grace + STALE log mtime + no recoverable pid → reaps with the existing reason string, byte-identical', () => {
|
|
531
|
-
const jobs = [{ slug: 'zombie', status: 'running', startedAt: agoMin(464) }];
|
|
532
|
-
const getLogMtimeMs = () => NOW - 25 * 60_000; // stale relative to a 20m window
|
|
533
|
-
const { reapable } = selectReapableJobs(jobs, NOW, {
|
|
534
|
-
pidAlive: alwaysAlive, grace: GRACE, getLogMtimeMs, logFreshWindowMs: 20 * 60_000,
|
|
535
|
-
});
|
|
536
|
-
assert.strictEqual(reapable.length, 1);
|
|
537
|
-
assert.strictEqual(reapable[0].reason, 'reaped: no runtime.pid recorded after 10m — spawn never completed');
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
test('selectReapableJobs: a row WITH a live runtime.pid is untouched by every new code path', () => {
|
|
541
|
-
const jobs = [{ slug: 'live', status: 'running', runtime: { pid: 1 }, startedAt: agoMin(500) }];
|
|
542
|
-
const getLogPid = () => { throw new Error('must never be called for a live-pid row'); };
|
|
543
|
-
const getLogMtimeMs = () => { throw new Error('must never be called for a live-pid row'); };
|
|
544
|
-
const { reapable, recovered, warnings } = selectReapableJobs(jobs, NOW, {
|
|
545
|
-
pidAlive: alwaysAlive, grace: GRACE, getLogPid, getLogMtimeMs, logFreshWindowMs: 20 * 60_000,
|
|
546
|
-
});
|
|
547
|
-
assert.deepStrictEqual(reapable, []);
|
|
548
|
-
assert.deepStrictEqual(recovered, []);
|
|
549
|
-
assert.deepStrictEqual(warnings, []);
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
test('selectReapableJobs: pidless + past grace + landedCommit on the row → reapable entry carries a failureOverride', () => {
|
|
553
|
-
const jobs = [{
|
|
554
|
-
slug: 'shipped-but-pidless',
|
|
555
|
-
status: 'running',
|
|
556
|
-
startedAt: agoMin(464),
|
|
557
|
-
landedCommit: 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f',
|
|
558
|
-
}];
|
|
559
|
-
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
560
|
-
assert.strictEqual(reapable.length, 1);
|
|
561
|
-
assert.strictEqual(reapable[0].pidless, true);
|
|
562
|
-
assert.ok(reapable[0].failureOverride, 'the reapable entry must carry the evidence-based override');
|
|
563
|
-
assert.strictEqual(reapable[0].failureOverride.landedCommit, 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f');
|
|
564
|
-
// The existing byte-for-byte reason string is untouched by this PRD — the
|
|
565
|
-
// override is carried as a separate field, not folded into `reason`.
|
|
566
|
-
assert.strictEqual(
|
|
567
|
-
reapable[0].reason,
|
|
568
|
-
'reaped: no runtime.pid recorded after 10m — spawn never completed',
|
|
569
|
-
);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
test('selectReapableJobs: pidless + past grace + NO landedCommit on the row → failureOverride is null, unchanged from today', () => {
|
|
573
|
-
const jobs = [{ slug: 'genuinely-never-ran', status: 'running', startedAt: agoMin(464) }];
|
|
574
|
-
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
575
|
-
assert.strictEqual(reapable.length, 1);
|
|
576
|
-
assert.strictEqual(reapable[0].failureOverride, null);
|
|
577
|
-
});
|