claude-code-session-manager 0.97.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-D93FoaOm.js → DataModel-HdR0AxFq.js} +1 -1
- package/dist/assets/{History-CXxxqNP1.js → History-Dr4Jr_5z.js} +2 -2
- package/dist/assets/{Hooks-CzeYFQjq.js → Hooks-NhrvH3JP.js} +3 -3
- package/dist/assets/{Library-D2zPkwOC.js → Library-Dv_EtIMr.js} +1 -1
- package/dist/assets/{MarkdownEditor-DbinP78A.js → MarkdownEditor-DTKDWV_n.js} +1 -1
- package/dist/assets/{McpServers-Bu_sVGLr.js → McpServers-M034G9B3.js} +2 -2
- package/dist/assets/{Memory-BgiI6LZ_.js → Memory-DzF1B4gx.js} +6 -6
- package/dist/assets/{Permissions-C1MMzZRm.js → Permissions-DrgbSGWl.js} +3 -3
- package/dist/assets/{Plugins-Dhu6wAZi.js → Plugins-Bd3ef35w.js} +2 -2
- package/dist/assets/{ProvenanceBadge-C3AcWKMG.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
- package/dist/assets/{SaveBar-plwYm0iO.js → SaveBar-rimJem-Y.js} +1 -1
- package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
- package/dist/assets/{ScopeSwitcher-DFy0vv_o.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
- package/dist/assets/Settings-Dc1F_sSg.js +3 -0
- package/dist/assets/{SkillReferenceGraph-DsI7hRPB.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
- package/dist/assets/{Skills-3E748UfB.js → Skills-DRXd18gu.js} +2 -2
- package/dist/assets/{SystemPrompt-BqmfUJOd.js → SystemPrompt-ahy5-Fwe.js} +1 -1
- package/dist/assets/{TagLibrary-CPz6fO8Q.js → TagLibrary-C_tToDeL.js} +1 -1
- package/dist/assets/{TiptapBody-D9EoBQ1P.js → TiptapBody-DvdefS6K.js} +1 -1
- package/dist/assets/{Toggle-BHAmZNDn.js → Toggle-6Jl5Njc6.js} +1 -1
- package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
- package/dist/assets/{index-tn5JIVPj.js → index-vx8O73l8.js} +497 -502
- package/dist/assets/{settingsSchema-Crqy_aNz.js → settingsSchema-D6iRguNV.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +26 -29
- package/plugins/CLAUDE.md +6 -6
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +23 -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 +153 -19
- 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 +33 -30
- package/src/main/lib/workTypeLibrary.cjs +7 -1
- package/src/main/promptSessionEvents.cjs +37 -13
- package/src/main/queueOps.cjs +17 -6
- package/src/main/scheduler.cjs +975 -203
- 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-nESrTRIg.js +0 -1
- package/dist/assets/Scheduler-BVYTeh39.js +0 -16
- package/dist/assets/Settings-DIhGgdfs.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__/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 -154
- 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-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 -375
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
- package/src/main/lib/kgExchangePairing.cjs +0 -75
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* rateLimitPollerStreak.test.cjs — covers the fix for the silent
|
|
3
|
-
* consecutiveFailures streak on the usage/rate-limit poller (scheduler.cjs
|
|
4
|
-
* pollLoop): the 'meter_rate_limited' branch used to never back off and
|
|
5
|
-
* never call persistSchedulerState(), letting scheduler-state.json freeze
|
|
6
|
-
* stale while the loop kept failing every POLL_INTERVAL_MS underneath it.
|
|
7
|
-
* This suite covers the pure pieces of the fix: the shared backoff
|
|
8
|
-
* curve/cap (nextBackoffMs), the once-per-streak WARN gate
|
|
9
|
-
* (shouldWarnFailureStreak), and the periodic escalation ladder that fires
|
|
10
|
-
* every FAILURE_STREAK_ESCALATION_MS while a streak PERSISTS past the
|
|
11
|
-
* initial WARN, re-arming once the streak clears (shouldEscalateFailureStreak).
|
|
12
|
-
*
|
|
13
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/rateLimitPollerStreak.test.cjs
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
import { test, expect } from 'vitest';
|
|
19
|
-
const {
|
|
20
|
-
nextBackoffMs,
|
|
21
|
-
shouldWarnFailureStreak,
|
|
22
|
-
shouldEscalateFailureStreak,
|
|
23
|
-
restoreFailureStreakWarnedAt,
|
|
24
|
-
persistedStreakMinutes,
|
|
25
|
-
BACKOFF_MAX_MS,
|
|
26
|
-
FAILURE_STREAK_WARN_THRESHOLD,
|
|
27
|
-
FAILURE_STREAK_ESCALATION_MS,
|
|
28
|
-
} = require('../scheduler.cjs');
|
|
29
|
-
|
|
30
|
-
test('nextBackoffMs starts at 30s from a zero/falsy backoff', () => {
|
|
31
|
-
expect(nextBackoffMs(0)).toBe(30_000);
|
|
32
|
-
expect(nextBackoffMs(null)).toBe(30_000);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('nextBackoffMs doubles each call', () => {
|
|
36
|
-
let ms = 0;
|
|
37
|
-
ms = nextBackoffMs(ms); // 30_000
|
|
38
|
-
ms = nextBackoffMs(ms); // 60_000
|
|
39
|
-
ms = nextBackoffMs(ms); // 120_000
|
|
40
|
-
expect(ms).toBe(120_000);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('nextBackoffMs caps at BACKOFF_MAX_MS (8 minutes) and never exceeds it', () => {
|
|
44
|
-
let ms = 0;
|
|
45
|
-
for (let i = 0; i < 20; i++) ms = nextBackoffMs(ms);
|
|
46
|
-
expect(ms).toBe(BACKOFF_MAX_MS);
|
|
47
|
-
expect(BACKOFF_MAX_MS).toBe(480_000);
|
|
48
|
-
// One more doubling from an already-capped value must still be capped, not climb further.
|
|
49
|
-
expect(nextBackoffMs(ms)).toBe(BACKOFF_MAX_MS);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('shouldWarnFailureStreak is false below the threshold', () => {
|
|
53
|
-
expect(shouldWarnFailureStreak(1, false)).toBe(false);
|
|
54
|
-
expect(shouldWarnFailureStreak(FAILURE_STREAK_WARN_THRESHOLD - 1, false)).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('shouldWarnFailureStreak fires exactly once when the threshold is crossed', () => {
|
|
58
|
-
expect(shouldWarnFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, false)).toBe(true);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('shouldWarnFailureStreak does not spam on every subsequent failure in the same streak', () => {
|
|
62
|
-
// Simulate a 57-failure streak: warn fires once, then `alreadyWarned` stays
|
|
63
|
-
// true for every remaining failure — this is the no-repeat property.
|
|
64
|
-
let warned = false;
|
|
65
|
-
let warnCount = 0;
|
|
66
|
-
for (let failures = 1; failures <= 57; failures++) {
|
|
67
|
-
if (shouldWarnFailureStreak(failures, warned)) {
|
|
68
|
-
warned = true;
|
|
69
|
-
warnCount++;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
expect(warnCount).toBe(1);
|
|
73
|
-
expect(warned).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('shouldWarnFailureStreak re-arms after a streak resets (alreadyWarned back to false)', () => {
|
|
77
|
-
expect(shouldWarnFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, false)).toBe(true);
|
|
78
|
-
// A later, independent streak crossing the threshold again must warn again.
|
|
79
|
-
expect(shouldWarnFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, false)).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('shouldWarnFailureStreak respects a custom threshold', () => {
|
|
83
|
-
expect(shouldWarnFailureStreak(3, false, 3)).toBe(true);
|
|
84
|
-
expect(shouldWarnFailureStreak(2, false, 3)).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('shouldEscalateFailureStreak: false below the WARN threshold, regardless of elapsed time', () => {
|
|
88
|
-
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD - 1, null, Date.now())).toBe(false);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('shouldEscalateFailureStreak: fires immediately the first time (lastEscalatedAtMs null) once past the WARN threshold', () => {
|
|
92
|
-
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, null, Date.now())).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('shouldEscalateFailureStreak: does not re-fire before FAILURE_STREAK_ESCALATION_MS has elapsed since the last one', () => {
|
|
96
|
-
const lastEscalatedAtMs = 1_000_000;
|
|
97
|
-
const tooSoon = lastEscalatedAtMs + FAILURE_STREAK_ESCALATION_MS - 1;
|
|
98
|
-
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, lastEscalatedAtMs, tooSoon)).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('shouldEscalateFailureStreak: fires again once FAILURE_STREAK_ESCALATION_MS has elapsed — the persisting-streak ladder', () => {
|
|
102
|
-
const lastEscalatedAtMs = 1_000_000;
|
|
103
|
-
const dueNow = lastEscalatedAtMs + FAILURE_STREAK_ESCALATION_MS;
|
|
104
|
-
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, lastEscalatedAtMs, dueNow)).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('shouldEscalateFailureStreak: re-arms after a streak clears (lastEscalatedAtMs reset to null) — a later independent streak escalates again', () => {
|
|
108
|
-
const firstStreakNow = 1_000_000;
|
|
109
|
-
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, null, firstStreakNow)).toBe(true);
|
|
110
|
-
// Streak clears (a success resets both failureStreakWarned and the
|
|
111
|
-
// escalation timestamp) — a LATER, independent streak crossing the WARN
|
|
112
|
-
// threshold again must escalate again immediately, not wait out a stale
|
|
113
|
-
// 30-minute window from the previous incident.
|
|
114
|
-
const laterStreakNow = firstStreakNow + 5 * 60_000; // well under 30 min later
|
|
115
|
-
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, null, laterStreakNow)).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('restoreFailureStreakWarnedAt backfills now when warned=true has no numeric timestamp', () => {
|
|
119
|
-
expect(restoreFailureStreakWarnedAt(true, null, 5000)).toBe(5000);
|
|
120
|
-
expect(restoreFailureStreakWarnedAt(true, 1000, 5000)).toBe(1000);
|
|
121
|
-
expect(restoreFailureStreakWarnedAt(false, null, 5000)).toBeNull();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('persistedStreakMinutes is always a number, never null', () => {
|
|
125
|
-
expect(persistedStreakMinutes(null, 5000)).toBe(0);
|
|
126
|
-
expect(persistedStreakMinutes(0, 120_000)).toBe(2);
|
|
127
|
-
expect(persistedStreakMinutes(restoreFailureStreakWarnedAt(true, undefined, 5000), 5000)).toBe(0);
|
|
128
|
-
});
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* rcaReport.test.cjs — unit tests for src/main/lib/rcaReport.cjs.
|
|
3
|
-
*
|
|
4
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/rcaReport.test.cjs
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use strict';
|
|
8
|
-
|
|
9
|
-
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
-
const fs = require('node:fs');
|
|
11
|
-
const os = require('node:os');
|
|
12
|
-
const path = require('node:path');
|
|
13
|
-
|
|
14
|
-
let tmpHome;
|
|
15
|
-
let realHome;
|
|
16
|
-
let rcaReport;
|
|
17
|
-
|
|
18
|
-
// config.cjs bakes os.homedir() into top-level consts at first require
|
|
19
|
-
// (allowedRoots/WRITE_PREFIXES). Node's require cache must be purged per test
|
|
20
|
-
// so each test's tmpHome takes effect — vi.resetModules() only resets vitest's
|
|
21
|
-
// ESM graph, not require().
|
|
22
|
-
const MODULES_TO_RELOAD = ['../lib/rcaReport.cjs', '../config.cjs'];
|
|
23
|
-
|
|
24
|
-
function purgeRequireCache() {
|
|
25
|
-
for (const m of MODULES_TO_RELOAD) {
|
|
26
|
-
try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
realHome = process.env.HOME;
|
|
32
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'rca-report-test-'));
|
|
33
|
-
process.env.HOME = tmpHome;
|
|
34
|
-
delete process.env.SM_RCA_DISABLE;
|
|
35
|
-
purgeRequireCache();
|
|
36
|
-
rcaReport = require('../lib/rcaReport.cjs');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
process.env.HOME = realHome;
|
|
41
|
-
delete process.env.SM_RCA_DISABLE;
|
|
42
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
43
|
-
purgeRequireCache();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
function makeProject(name = 'proj') {
|
|
49
|
-
const cwd = path.join(tmpHome, name);
|
|
50
|
-
fs.mkdirSync(cwd, { recursive: true });
|
|
51
|
-
return cwd;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function writeRun(runDir, slug, { logLines = ['line1', 'line2'], exitCode = 1, durationMs = 1234 } = {}) {
|
|
55
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
56
|
-
fs.writeFileSync(path.join(runDir, `${slug}.log`), logLines.join('\n') + '\n');
|
|
57
|
-
fs.writeFileSync(path.join(runDir, `${slug}.meta.json`), JSON.stringify({ exitCode, durationMs }));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function writePrd(cwd, slug, { acBody = '- [ ] `timeout 10 echo ok` passes.' } = {}) {
|
|
61
|
-
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
62
|
-
fs.mkdirSync(prdsDir, { recursive: true });
|
|
63
|
-
const body = ['---', `title: ${slug}`, '---', '', '# Goal', '', 'Do the thing.', '', '# Acceptance criteria', '', acBody, '', '# Out of scope', '', '- N/A'].join('\n');
|
|
64
|
-
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), body);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function baseJob(overrides = {}) {
|
|
68
|
-
return { slug: 'testslug', runId: '2026-07-24T00-00-00-000Z', exitCode: 1, error: 'something broke', ...overrides };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function readSessions(cwd) {
|
|
72
|
-
const p = path.join(cwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json');
|
|
73
|
-
if (!fs.existsSync(p)) return {};
|
|
74
|
-
return JSON.parse(fs.readFileSync(p, 'utf8')).sessions ?? {};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ─── report writing ──────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
test('writeRcaReport: writes root-cause-<slug>.md into the run directory', async () => {
|
|
80
|
-
const cwd = makeProject();
|
|
81
|
-
const runDir = path.join(tmpHome, 'run1');
|
|
82
|
-
writeRun(runDir, 'testslug');
|
|
83
|
-
writePrd(cwd, 'testslug');
|
|
84
|
-
|
|
85
|
-
const res = await rcaReport.writeRcaReport({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
|
|
86
|
-
|
|
87
|
-
expect(res.filed).toBe(true);
|
|
88
|
-
expect(res.path).toBe(path.join(runDir, 'root-cause-testslug.md'));
|
|
89
|
-
const body = fs.readFileSync(res.path, 'utf8');
|
|
90
|
-
expect(body).toContain('prdSlug: testslug');
|
|
91
|
-
expect(body).toContain('# What happened');
|
|
92
|
-
expect(body).toContain('# The PRD\'s acceptance criteria');
|
|
93
|
-
expect(body).toContain('timeout 10 echo ok');
|
|
94
|
-
expect(res.summary).toContain('testslug');
|
|
95
|
-
expect(res.summary.split('\n')).toHaveLength(1);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('writeRcaReport: creates no Epic — a parked job never opens work on its own', async () => {
|
|
99
|
-
const cwd = makeProject();
|
|
100
|
-
const runDir = path.join(tmpHome, 'run1');
|
|
101
|
-
writeRun(runDir, 'testslug');
|
|
102
|
-
writePrd(cwd, 'testslug');
|
|
103
|
-
|
|
104
|
-
await rcaReport.writeRcaReport({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
|
|
105
|
-
|
|
106
|
-
expect(readSessions(cwd)).toEqual({});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('writeRcaReport: a second call for the same run overwrites in place, never duplicates', async () => {
|
|
110
|
-
const cwd = makeProject();
|
|
111
|
-
const runDir = path.join(tmpHome, 'run1');
|
|
112
|
-
writeRun(runDir, 'testslug');
|
|
113
|
-
writePrd(cwd, 'testslug');
|
|
114
|
-
const job = baseJob({ cwd });
|
|
115
|
-
|
|
116
|
-
const first = await rcaReport.writeRcaReport({ job, runDir, verdict: 'transcript_errors' });
|
|
117
|
-
const second = await rcaReport.writeRcaReport({ job, runDir, verdict: 'transcript_errors' });
|
|
118
|
-
|
|
119
|
-
expect(second.path).toBe(first.path);
|
|
120
|
-
expect(fs.readdirSync(runDir).filter((f) => f.startsWith('root-cause-'))).toHaveLength(1);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('writeRcaReport: investigationText enriches the report in place', async () => {
|
|
124
|
-
const cwd = makeProject();
|
|
125
|
-
const runDir = path.join(tmpHome, 'run1');
|
|
126
|
-
writeRun(runDir, 'testslug');
|
|
127
|
-
writePrd(cwd, 'testslug');
|
|
128
|
-
const job = baseJob({ cwd });
|
|
129
|
-
|
|
130
|
-
const first = await rcaReport.writeRcaReport({ job, runDir, verdict: 'transcript_errors' });
|
|
131
|
-
expect(fs.readFileSync(first.path, 'utf8')).not.toContain('## Investigation analysis');
|
|
132
|
-
|
|
133
|
-
const second = await rcaReport.writeRcaReport({
|
|
134
|
-
job, runDir, verdict: 'transcript_errors', investigationText: 'Root cause: forgot to bound the poll loop.',
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
expect(second.path).toBe(first.path);
|
|
138
|
-
const enriched = fs.readFileSync(second.path, 'utf8');
|
|
139
|
-
expect(enriched).toContain('## Investigation analysis');
|
|
140
|
-
expect(enriched).toContain('forgot to bound the poll loop');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test('writeRcaReport: works for a job whose PRD cannot be read (no acceptance criteria on record)', async () => {
|
|
144
|
-
const cwd = makeProject('bare-proj');
|
|
145
|
-
const runDir = path.join(tmpHome, 'run1');
|
|
146
|
-
writeRun(runDir, 'testslug');
|
|
147
|
-
|
|
148
|
-
const res = await rcaReport.writeRcaReport({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
|
|
149
|
-
|
|
150
|
-
expect(res.filed).toBe(true);
|
|
151
|
-
expect(fs.readFileSync(res.path, 'utf8')).toContain('acceptance criteria not found');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// ─── skip paths ──────────────────────────────────────────────────────────────
|
|
155
|
-
|
|
156
|
-
test('writeRcaReport: rejects a slug containing path-traversal segments', async () => {
|
|
157
|
-
const cwd = makeProject();
|
|
158
|
-
const runDir = path.join(tmpHome, 'run1');
|
|
159
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
160
|
-
|
|
161
|
-
const res = await rcaReport.writeRcaReport({ job: baseJob({ cwd, slug: '../../etc/passwd' }), runDir, verdict: 'transcript_errors' });
|
|
162
|
-
|
|
163
|
-
expect(res.filed).toBe(false);
|
|
164
|
-
expect(res.reason).toBe('unsafe-slug');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('writeRcaReport: skips when there is no run directory to write into', async () => {
|
|
168
|
-
const cwd = makeProject();
|
|
169
|
-
|
|
170
|
-
const res = await rcaReport.writeRcaReport({ job: baseJob({ cwd }), verdict: 'transcript_errors' });
|
|
171
|
-
|
|
172
|
-
expect(res.filed).toBe(false);
|
|
173
|
-
expect(res.reason).toBe('no-run-dir');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// ─── recovery actions ────────────────────────────────────────────────────────
|
|
177
|
-
|
|
178
|
-
test('RECOVERY_ACTIONS: every FAILURE_CLASSES member has an entry from the closed set', () => {
|
|
179
|
-
const CLOSED_SET = new Set(['archive', 'resume-and-commit', 'verify-and-close', 'investigate']);
|
|
180
|
-
for (const failureClass of Object.values(rcaReport.FAILURE_CLASSES)) {
|
|
181
|
-
expect(rcaReport.RECOVERY_ACTIONS).toHaveProperty(failureClass);
|
|
182
|
-
expect(CLOSED_SET.has(rcaReport.RECOVERY_ACTIONS[failureClass])).toBe(true);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('recoveryActionFor: maps each known failure class to its expected action', () => {
|
|
187
|
-
const { FAILURE_CLASSES, recoveryActionFor } = rcaReport;
|
|
188
|
-
expect(recoveryActionFor(FAILURE_CLASSES.ALREADY_SHIPPED)).toBe('archive');
|
|
189
|
-
expect(recoveryActionFor(FAILURE_CLASSES.UNCOMMITTED)).toBe('resume-and-commit');
|
|
190
|
-
expect(recoveryActionFor(FAILURE_CLASSES.NO_SENTINEL)).toBe('verify-and-close');
|
|
191
|
-
expect(recoveryActionFor(FAILURE_CLASSES.SELF_QUEUE)).toBe('investigate');
|
|
192
|
-
expect(recoveryActionFor(FAILURE_CLASSES.STUCK_LOOP)).toBe('investigate');
|
|
193
|
-
expect(recoveryActionFor(FAILURE_CLASSES.POST_AC_OVERRUN)).toBe('investigate');
|
|
194
|
-
expect(recoveryActionFor(FAILURE_CLASSES.TRANSCRIPT_ERRORS)).toBe('investigate');
|
|
195
|
-
expect(recoveryActionFor(FAILURE_CLASSES.UNKNOWN)).toBe('investigate');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test('recoveryActionFor: defaults to investigate for an unrecognised class', () => {
|
|
199
|
-
expect(rcaReport.recoveryActionFor('totally-made-up-class')).toBe('investigate');
|
|
200
|
-
expect(rcaReport.recoveryActionFor(undefined)).toBe('investigate');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('writeRcaReport: markdown carries a machine-readable recovery-action line matching recoveryActionFor, for each failure class', async () => {
|
|
204
|
-
const cwd = makeProject();
|
|
205
|
-
const runDir = path.join(tmpHome, 'run1');
|
|
206
|
-
writeRun(runDir, 'testslug');
|
|
207
|
-
writePrd(cwd, 'testslug');
|
|
208
|
-
|
|
209
|
-
const cases = [
|
|
210
|
-
{ verdict: 'pass_no_commit', logTail: 'The work was already implemented in a prior commit.', expectedClass: rcaReport.FAILURE_CLASSES.ALREADY_SHIPPED },
|
|
211
|
-
{ verdict: 'transcript_errors', logTail: 'Launching skill: session-manager-dev:develop', expectedClass: rcaReport.FAILURE_CLASSES.SELF_QUEUE },
|
|
212
|
-
{ verdict: 'transcript_errors', logTail: 'while true; do sleep 1; done', expectedClass: rcaReport.FAILURE_CLASSES.STUCK_LOOP },
|
|
213
|
-
{ verdict: 'uncommitted_changes', logTail: 'plain log tail', expectedClass: rcaReport.FAILURE_CLASSES.UNCOMMITTED },
|
|
214
|
-
{ verdict: 'no_verdict_sentinel', logTail: 'plain log tail', expectedClass: rcaReport.FAILURE_CLASSES.NO_SENTINEL },
|
|
215
|
-
{ verdict: 'transcript_errors', logTail: 'plain log tail', expectedClass: rcaReport.FAILURE_CLASSES.TRANSCRIPT_ERRORS },
|
|
216
|
-
{ verdict: 'some_other_verdict', logTail: 'plain log tail', expectedClass: rcaReport.FAILURE_CLASSES.UNKNOWN },
|
|
217
|
-
];
|
|
218
|
-
|
|
219
|
-
for (const { verdict, logTail, expectedClass } of cases) {
|
|
220
|
-
writeRun(runDir, 'testslug', { logLines: logTail.split('\n') });
|
|
221
|
-
const res = await rcaReport.writeRcaReport({ job: baseJob({ cwd }), runDir, verdict });
|
|
222
|
-
expect(res.failureClass).toBe(expectedClass);
|
|
223
|
-
const expectedAction = rcaReport.recoveryActionFor(expectedClass);
|
|
224
|
-
expect(res.recoveryAction).toBe(expectedAction);
|
|
225
|
-
const body = fs.readFileSync(res.path, 'utf8');
|
|
226
|
-
expect(body).toContain(`recovery-action: ${expectedAction}`);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('writeRcaReport: SM_RCA_DISABLE=1 skips without writing', async () => {
|
|
231
|
-
process.env.SM_RCA_DISABLE = '1';
|
|
232
|
-
const cwd = makeProject();
|
|
233
|
-
const runDir = path.join(tmpHome, 'run1');
|
|
234
|
-
writeRun(runDir, 'testslug');
|
|
235
|
-
writePrd(cwd, 'testslug');
|
|
236
|
-
|
|
237
|
-
const res = await rcaReport.writeRcaReport({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
|
|
238
|
-
|
|
239
|
-
expect(res.filed).toBe(false);
|
|
240
|
-
expect(res.reason).toBe('disabled');
|
|
241
|
-
expect(fs.readdirSync(runDir).filter((f) => f.startsWith('root-cause-'))).toHaveLength(0);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test('classifyFailure: blocked_by_foreign_wip_streak verdict classifies as BLOCKED_BY_FOREIGN_WIP, never UNKNOWN', () => {
|
|
245
|
-
const failureClass = rcaReport.classifyFailure({ verdict: 'blocked_by_foreign_wip_streak', logTail: 'anything' });
|
|
246
|
-
expect(failureClass).toBe(rcaReport.FAILURE_CLASSES.BLOCKED_BY_FOREIGN_WIP);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test('writeRcaReport: a blocked_by_foreign_wip_streak verdict points at the sibling job, not "re-run the gate"', async () => {
|
|
250
|
-
const cwd = makeProject();
|
|
251
|
-
const runDir = path.join(tmpHome, 'run-blocked');
|
|
252
|
-
writeRun(runDir, 'blockedslug');
|
|
253
|
-
writePrd(cwd, 'blockedslug');
|
|
254
|
-
|
|
255
|
-
const res = await rcaReport.writeRcaReport({
|
|
256
|
-
job: baseJob({ cwd, slug: 'blockedslug' }),
|
|
257
|
-
runDir,
|
|
258
|
-
verdict: 'blocked_by_foreign_wip_streak',
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
expect(res.filed).toBe(true);
|
|
262
|
-
expect(res.failureClass).toBe(rcaReport.FAILURE_CLASSES.BLOCKED_BY_FOREIGN_WIP);
|
|
263
|
-
const body = fs.readFileSync(res.path, 'utf8');
|
|
264
|
-
expect(body).toMatch(/sibling job/i);
|
|
265
|
-
expect(body).not.toMatch(/Re-run the acceptance criteria gate/);
|
|
266
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* reconcileFlatPrdSweep.test.cjs — end-to-end proof that `reconcile()` itself
|
|
3
|
-
* (not just `consolidateAllFlatPrds()` in isolation) never turns a
|
|
4
|
-
* hand-written flat-dir PRD into a queue job, regardless of which of
|
|
5
|
-
* reconcile's several callers (tickQueue's poll, job completion, the
|
|
6
|
-
* schedule:state/schedule:rescan IPC handlers, rescheduleTimer) triggered
|
|
7
|
-
* the pass.
|
|
8
|
-
*
|
|
9
|
-
* A code-review pass on this PRD flagged that gating only `tickQueue()` left
|
|
10
|
-
* every OTHER caller of `reconcile()` free to still turn a freshly
|
|
11
|
-
* hand-written flat-dir PRD into a `pending` job — the fix moved the sweep
|
|
12
|
-
* inside `reconcile()` itself instead. This test exercises the real
|
|
13
|
-
* `reconcile()` (not a stand-in) against a real on-disk flat-dir PRD with no
|
|
14
|
-
* pre-existing queue row (exactly the state of a file dropped there moments
|
|
15
|
-
* ago) and asserts it never appears in `state.jobs` afterward.
|
|
16
|
-
*
|
|
17
|
-
* HOME-isolated (mirrors scheduler-reconcile-invalid-repair.test.cjs): stub
|
|
18
|
-
* HOME to a mkdtemp dir before requiring scheduler.cjs, and register the
|
|
19
|
-
* fixture project as "active" via a fake ~/.claude/projects transcript —
|
|
20
|
-
* reconcile's PRD discovery (prdLocations.cjs) only scans cwds it can find
|
|
21
|
-
* that way.
|
|
22
|
-
*
|
|
23
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/reconcileFlatPrdSweep.test.cjs
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
'use strict';
|
|
27
|
-
|
|
28
|
-
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
29
|
-
const fs = require('node:fs');
|
|
30
|
-
const os = require('node:os');
|
|
31
|
-
const path = require('node:path');
|
|
32
|
-
|
|
33
|
-
let tmpHome;
|
|
34
|
-
let originalHome;
|
|
35
|
-
let scheduler;
|
|
36
|
-
let bustCwdCache;
|
|
37
|
-
|
|
38
|
-
beforeAll(() => {
|
|
39
|
-
originalHome = process.env.HOME;
|
|
40
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-reconcile-flatsweep-home-'));
|
|
41
|
-
process.env.HOME = tmpHome;
|
|
42
|
-
|
|
43
|
-
scheduler = require('../scheduler.cjs');
|
|
44
|
-
({ bustCwdCache } = require('../lib/queueStore.cjs'));
|
|
45
|
-
|
|
46
|
-
if (!scheduler.PRDS_DIR.startsWith(tmpHome)) {
|
|
47
|
-
throw new Error(`refusing to run: PRDS_DIR (${scheduler.PRDS_DIR}) is not under the temp HOME (${tmpHome})`);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
afterAll(() => {
|
|
52
|
-
process.env.HOME = originalHome;
|
|
53
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
function registerActiveProject(cwd) {
|
|
57
|
-
const projectsDir = path.join(tmpHome, '.claude', 'projects');
|
|
58
|
-
const slugDir = path.join(projectsDir, `fake-project-slug-${path.basename(cwd)}`);
|
|
59
|
-
fs.mkdirSync(slugDir, { recursive: true });
|
|
60
|
-
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
61
|
-
// Each test registers a NEW project mid-file; without busting queueStore's
|
|
62
|
-
// stateCwds cache (CACHE_MS 120s), a reconcile() call from an EARLIER test
|
|
63
|
-
// in this file can leave a stale cwd list cached that this test's freshly
|
|
64
|
-
// registered project is invisible to — reconcile() then never discovers
|
|
65
|
-
// its PRD at all (confirmed: this is what made
|
|
66
|
-
// 'reconcile() still onboards a PRD written to the canonical Epic-scoped
|
|
67
|
-
// dir' intermittently fail under full-suite load, PRD 1414).
|
|
68
|
-
bustCwdCache();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function makeFixtureProject(prefix) {
|
|
72
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
73
|
-
registerActiveProject(cwd);
|
|
74
|
-
const opsRoot = path.join(cwd, 'session-manager-operations');
|
|
75
|
-
const flatPrdsDir = path.join(opsRoot, 'scheduler', 'prds');
|
|
76
|
-
const stateDir = path.join(opsRoot, 'scheduler', 'state');
|
|
77
|
-
fs.mkdirSync(flatPrdsDir, { recursive: true });
|
|
78
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
79
|
-
return { cwd, flatPrdsDir, stateDir, archiveDir: path.join(opsRoot, 'scheduler', 'prds-archived') };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
test('reconcile() never turns a hand-written flat-dir PRD into a job, no matter which caller triggers it', async () => {
|
|
83
|
-
const { cwd, flatPrdsDir, archiveDir } = makeFixtureProject('sm-reconcile-flatsweep-proj-');
|
|
84
|
-
const filename = '1500-hand-written-bypass.md';
|
|
85
|
-
fs.writeFileSync(
|
|
86
|
-
path.join(flatPrdsDir, filename),
|
|
87
|
-
`---\ntitle: Hand-written bypass\ncwd: ${cwd}\nestimateMinutes: 10\n---\n\n# Goal\n\nSomething.\n`,
|
|
88
|
-
'utf8',
|
|
89
|
-
);
|
|
90
|
-
// No queue.json at all — the exact state of a PRD dropped on disk moments
|
|
91
|
-
// ago, before any prior reconcile() pass has scanned it into a job.
|
|
92
|
-
|
|
93
|
-
const state = { jobs: [], invalidJobs: [], paused: null };
|
|
94
|
-
await scheduler.reconcile(state);
|
|
95
|
-
|
|
96
|
-
const found = state.jobs.find((j) => j.slug === '1500-hand-written-bypass');
|
|
97
|
-
expect(found).toBeUndefined();
|
|
98
|
-
expect(fs.existsSync(path.join(flatPrdsDir, filename))).toBe(false);
|
|
99
|
-
expect(fs.existsSync(path.join(archiveDir, filename))).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('reconcile() still onboards a PRD written to the canonical Epic-scoped dir (the sweep only touches the flat dir)', async () => {
|
|
103
|
-
const { cwd } = makeFixtureProject('sm-reconcile-flatsweep-epic-proj-');
|
|
104
|
-
const epicId = 'epic-abc';
|
|
105
|
-
const epicPrdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId, 'prds');
|
|
106
|
-
fs.mkdirSync(epicPrdsDir, { recursive: true });
|
|
107
|
-
fs.writeFileSync(
|
|
108
|
-
path.join(epicPrdsDir, '1501-canonical.md'),
|
|
109
|
-
`---\ntitle: Canonical\ncwd: ${cwd}\nestimateMinutes: 10\nsourcePromptId: ${epicId}\ncreatedVia: scheduler-api\nissuedAt: 2026-08-07T00:00:00.000Z\n---\n\n# Goal\n\nSomething.\n`,
|
|
110
|
-
'utf8',
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const state = { jobs: [], invalidJobs: [], paused: null };
|
|
114
|
-
await scheduler.reconcile(state);
|
|
115
|
-
|
|
116
|
-
const found = state.jobs.find((j) => j.slug === '1501-canonical');
|
|
117
|
-
expect(found).toBeDefined();
|
|
118
|
-
expect(found.status).toBe('pending');
|
|
119
|
-
});
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* reconcileTiming.test.cjs — proves reconcile() (src/main/scheduler.cjs)
|
|
3
|
-
* records a per-phase timing breakdown and emits exactly one warn-level
|
|
4
|
-
* `logs.writeLine` when a pass exceeds RECONCILE_SLOW_PASS_MS
|
|
5
|
-
* (src/main/lib/schedulerConfig.cjs), and stays silent on a normal-speed
|
|
6
|
-
* pass. Before this, diagnosing a slow reconcile pass (the
|
|
7
|
-
* "schedule.state timed out after 5000ms" toast) required hand-timing the
|
|
8
|
-
* filesystem scans from a throwaway script.
|
|
9
|
-
*
|
|
10
|
-
* HOME-isolated (mirrors reconcileFlatPrdSweep.test.cjs): stub HOME to a
|
|
11
|
-
* mkdtemp dir before requiring scheduler.cjs, and register the fixture
|
|
12
|
-
* project as "active" via a fake ~/.claude/projects transcript — reconcile's
|
|
13
|
-
* PRD discovery (prdLocations.cjs) only scans cwds it can find that way.
|
|
14
|
-
*
|
|
15
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/reconcileTiming.test.cjs
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
'use strict';
|
|
19
|
-
|
|
20
|
-
import { test, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
|
|
21
|
-
const fs = require('node:fs');
|
|
22
|
-
const os = require('node:os');
|
|
23
|
-
const path = require('node:path');
|
|
24
|
-
|
|
25
|
-
let tmpHome;
|
|
26
|
-
let originalHome;
|
|
27
|
-
let scheduler;
|
|
28
|
-
let queueHistory;
|
|
29
|
-
let logs;
|
|
30
|
-
let RECONCILE_SLOW_PASS_MS;
|
|
31
|
-
|
|
32
|
-
beforeAll(() => {
|
|
33
|
-
originalHome = process.env.HOME;
|
|
34
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-reconcile-timing-home-'));
|
|
35
|
-
process.env.HOME = tmpHome;
|
|
36
|
-
|
|
37
|
-
scheduler = require('../scheduler.cjs');
|
|
38
|
-
queueHistory = require('../lib/queueHistory.cjs');
|
|
39
|
-
logs = require('../logs.cjs');
|
|
40
|
-
({ RECONCILE_SLOW_PASS_MS } = require('../lib/schedulerConfig.cjs'));
|
|
41
|
-
|
|
42
|
-
if (!scheduler.PRDS_DIR.startsWith(tmpHome)) {
|
|
43
|
-
throw new Error(`refusing to run: PRDS_DIR (${scheduler.PRDS_DIR}) is not under the temp HOME (${tmpHome})`);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
afterAll(() => {
|
|
48
|
-
process.env.HOME = originalHome;
|
|
49
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
afterEach(() => {
|
|
53
|
-
vi.restoreAllMocks();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
function registerActiveProject(cwd) {
|
|
57
|
-
const projectsDir = path.join(tmpHome, '.claude', 'projects');
|
|
58
|
-
const slugDir = path.join(projectsDir, `fake-project-slug-${path.basename(cwd)}`);
|
|
59
|
-
fs.mkdirSync(slugDir, { recursive: true });
|
|
60
|
-
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function makeFixtureProject(prefix) {
|
|
64
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
65
|
-
registerActiveProject(cwd);
|
|
66
|
-
return cwd;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function writeCanonicalPrd(cwd, epicId, slug) {
|
|
70
|
-
const epicPrdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId, 'prds');
|
|
71
|
-
fs.mkdirSync(epicPrdsDir, { recursive: true });
|
|
72
|
-
fs.writeFileSync(
|
|
73
|
-
path.join(epicPrdsDir, `${slug}.md`),
|
|
74
|
-
`---\ntitle: Timing fixture\ncwd: ${cwd}\nestimateMinutes: 10\nsourcePromptId: ${epicId}\ncreatedVia: scheduler-api\nissuedAt: 2026-08-07T00:00:00.000Z\n---\n\n# Goal\n\nSomething.\n`,
|
|
75
|
-
'utf8',
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function warnCallsMatching(spy) {
|
|
80
|
-
return spy.mock.calls
|
|
81
|
-
.map(([payload]) => payload)
|
|
82
|
-
.filter((payload) => payload?.level === 'warn' && payload?.scope === 'scheduler' && /reconcile\(\) pass took/.test(payload?.message ?? ''));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
test('a normal-speed reconcile() pass logs nothing', async () => {
|
|
86
|
-
const cwd = makeFixtureProject('sm-reconcile-timing-fast-proj-');
|
|
87
|
-
writeCanonicalPrd(cwd, 'epic-fast', '1600-fast');
|
|
88
|
-
|
|
89
|
-
const writeLineSpy = vi.spyOn(logs, 'writeLine');
|
|
90
|
-
const state = { jobs: [], invalidJobs: [], paused: null };
|
|
91
|
-
await scheduler.reconcile(state);
|
|
92
|
-
|
|
93
|
-
expect(warnCallsMatching(writeLineSpy)).toHaveLength(0);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('a slow reconcile() pass emits exactly one warn with the per-phase breakdown', async () => {
|
|
97
|
-
const cwd = makeFixtureProject('sm-reconcile-timing-slow-proj-');
|
|
98
|
-
writeCanonicalPrd(cwd, 'epic-slow', '1601-slow');
|
|
99
|
-
|
|
100
|
-
const realHistoryTerminalBySlug = queueHistory.historyTerminalBySlug.bind(queueHistory);
|
|
101
|
-
vi.spyOn(queueHistory, 'historyTerminalBySlug').mockImplementation(async (...args) => {
|
|
102
|
-
await new Promise((resolve) => { setTimeout(resolve, RECONCILE_SLOW_PASS_MS + 200); });
|
|
103
|
-
return realHistoryTerminalBySlug(...args);
|
|
104
|
-
});
|
|
105
|
-
const writeLineSpy = vi.spyOn(logs, 'writeLine');
|
|
106
|
-
|
|
107
|
-
const state = { jobs: [], invalidJobs: [], paused: null };
|
|
108
|
-
await scheduler.reconcile(state);
|
|
109
|
-
|
|
110
|
-
const warnCalls = warnCallsMatching(writeLineSpy);
|
|
111
|
-
expect(warnCalls).toHaveLength(1);
|
|
112
|
-
const { meta } = warnCalls[0];
|
|
113
|
-
expect(meta.totalMs).toBeGreaterThan(RECONCILE_SLOW_PASS_MS);
|
|
114
|
-
expect(meta.phaseMs).toHaveProperty('flatPrdSweep');
|
|
115
|
-
expect(meta.phaseMs).toHaveProperty('prdDirResolve');
|
|
116
|
-
expect(meta.phaseMs).toHaveProperty('parseLoop');
|
|
117
|
-
expect(meta.phaseMs).toHaveProperty('historyLookup');
|
|
118
|
-
expect(meta.phaseMs).toHaveProperty('queueWrite');
|
|
119
|
-
expect(meta.phaseMs.historyLookup).toBeGreaterThanOrEqual(RECONCILE_SLOW_PASS_MS);
|
|
120
|
-
expect(typeof meta.prdFileCount).toBe('number');
|
|
121
|
-
expect(typeof meta.resolvedDirCount).toBe('number');
|
|
122
|
-
expect(meta.prdFileCount).toBeGreaterThanOrEqual(1);
|
|
123
|
-
}, 15_000);
|
|
124
|
-
|
|
125
|
-
test('reconcile() surfaces its own error and does not swallow it while timing', async () => {
|
|
126
|
-
vi.spyOn(queueHistory, 'partitionJobs').mockImplementation(() => {
|
|
127
|
-
throw new Error('boom: injected failure for timing-swallow test');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const cwd = makeFixtureProject('sm-reconcile-timing-throw-proj-');
|
|
131
|
-
writeCanonicalPrd(cwd, 'epic-throw', '1602-throw');
|
|
132
|
-
|
|
133
|
-
const state = { jobs: [], invalidJobs: [], paused: null };
|
|
134
|
-
await expect(scheduler.reconcile(state)).rejects.toThrow('boom: injected failure for timing-swallow test');
|
|
135
|
-
});
|