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,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scheduler-quiet-machine-lease.test.cjs — PRD 1107: the exclusive
|
|
3
|
-
* quiet-machine lease must be released on EVERY spawnJob exit path,
|
|
4
|
-
* including a killed job (SIGKILL/exit 137), not just a clean exit — the
|
|
5
|
-
* same finally block that already releases the sessionSlots token. Drives
|
|
6
|
-
* spawnJob end-to-end against a real git fixture repo with a stub `claude`
|
|
7
|
-
* binary that exits 137 mid-run (same pattern as
|
|
8
|
-
* scheduler-inplace-salvage.test.cjs), then asserts:
|
|
9
|
-
* 1. the lease is released once the job settles, and
|
|
10
|
-
* 2. an ordinary pending job — held behind the lease while it was held —
|
|
11
|
-
* is now eligible to dispatch.
|
|
12
|
-
*
|
|
13
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-quiet-machine-lease.test.cjs
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
19
|
-
const fs = require('node:fs');
|
|
20
|
-
const os = require('node:os');
|
|
21
|
-
const path = require('node:path');
|
|
22
|
-
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
23
|
-
const { execFileSync } = require('node:child_process');
|
|
24
|
-
|
|
25
|
-
let tmpHome;
|
|
26
|
-
let originalHome;
|
|
27
|
-
let originalDisable;
|
|
28
|
-
let spawnJob;
|
|
29
|
-
let quietMachineLease;
|
|
30
|
-
let pickNextBatch;
|
|
31
|
-
|
|
32
|
-
function git(args, cwd) {
|
|
33
|
-
return execFileSync('git', args, { cwd, encoding: 'utf8' });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function initRepo(dir) {
|
|
37
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
38
|
-
git(['init', '-q'], dir);
|
|
39
|
-
git(['config', 'user.email', 'test@example.com'], dir);
|
|
40
|
-
git(['config', 'user.name', 'Test'], dir);
|
|
41
|
-
fs.writeFileSync(path.join(dir, 'README.md'), 'hello\n', 'utf8');
|
|
42
|
-
git(['add', '-A'], dir);
|
|
43
|
-
git(['commit', '-q', '-m', 'initial'], dir);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// See scheduler-inplace-salvage.test.cjs's identical helper: readQueue()'s
|
|
47
|
-
// per-project discovery walks ~/.claude/projects/*/*.jsonl transcripts for a
|
|
48
|
-
// `cwd` field to find known project queue.json shards.
|
|
49
|
-
function registerActiveProject(cwd) {
|
|
50
|
-
const projectsDir = path.join(tmpHome, '.claude', 'projects');
|
|
51
|
-
const slugDir = path.join(projectsDir, `fake-project-slug-${path.basename(cwd)}`);
|
|
52
|
-
fs.mkdirSync(slugDir, { recursive: true });
|
|
53
|
-
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Every real scheduler row is born from a real PRD .md file — reconcile()'s
|
|
57
|
-
// auto-archive-drop path (a terminal job whose slug has no on-disk PRD
|
|
58
|
-
// anywhere is treated as "archived on purpose": dropped from queue.json and
|
|
59
|
-
// backfilled to history.jsonl, see scheduler-reconcile-history-backfill.
|
|
60
|
-
// test.cjs) fires the moment a completion path reconciles with
|
|
61
|
-
// broadcast({flush:true}). Without a matching PRD source, a row this test
|
|
62
|
-
// drives to a terminal status vanishes from queue.json before the assertions
|
|
63
|
-
// below ever read it back. Epic-scoped (not the legacy flat prds/ dir) so
|
|
64
|
-
// it's never swept by consolidateFlatPrds either.
|
|
65
|
-
// Some tests author their own real PRD (e.g. with `quietMachine: true`
|
|
66
|
-
// frontmatter) before calling writeProjectQueue — never shadow that with a
|
|
67
|
-
// blank fixture, which would silently overwrite its parsed frontmatter on
|
|
68
|
-
// the next reconcile() refresh (onDisk.set(slug, ...) keeps whichever file a
|
|
69
|
-
// later scan pass parses).
|
|
70
|
-
function prdAlreadyExists(cwd, slug) {
|
|
71
|
-
const flatPath = path.join(cwd, 'session-manager-operations', 'scheduler', 'prds', `${slug}.md`);
|
|
72
|
-
if (fs.existsSync(flatPath)) return true;
|
|
73
|
-
const epicsRoot = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics');
|
|
74
|
-
if (!fs.existsSync(epicsRoot)) return false;
|
|
75
|
-
return fs.readdirSync(epicsRoot).some((epicId) => fs.existsSync(path.join(epicsRoot, epicId, 'prds', `${slug}.md`)));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function writeFixturePrd(cwd, slug) {
|
|
79
|
-
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
80
|
-
fs.mkdirSync(prdsDir, { recursive: true });
|
|
81
|
-
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Test fixture PRD.', 'utf8');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function writeProjectQueue(cwd, jobs) {
|
|
85
|
-
const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
|
|
86
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
87
|
-
fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({ jobs }, null, 2));
|
|
88
|
-
for (const job of jobs) {
|
|
89
|
-
if (job && job.slug && !prdAlreadyExists(job.cwd || cwd, job.slug)) writeFixturePrd(job.cwd || cwd, job.slug);
|
|
90
|
-
}
|
|
91
|
-
return path.join(stateDir, 'queue.json');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Stub `claude` binary that exits 137 (SIGKILL) with no result:success event,
|
|
95
|
-
// simulating a leased job killed mid-run before it could finish and release
|
|
96
|
-
// naturally through its own clean-exit path. Dirties the tree first (like
|
|
97
|
-
// scheduler-inplace-salvage.test.cjs's stub) so the scheduler classifies this
|
|
98
|
-
// as a genuine kill-with-partial-work, not an auto-retryable transient blip
|
|
99
|
-
// with no evidence of real execution.
|
|
100
|
-
function writeKilledClaudeStub() {
|
|
101
|
-
return claudeStub.writeClaudeStub({ body: `
|
|
102
|
-
const fs = require('fs');
|
|
103
|
-
const path = require('path');
|
|
104
|
-
fs.writeFileSync(path.join(process.cwd(), 'job-output.txt'), 'work in progress when killed\\n', 'utf8');
|
|
105
|
-
process.exit(137);
|
|
106
|
-
` });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let reapDeadRunningJobs;
|
|
110
|
-
|
|
111
|
-
beforeAll(() => {
|
|
112
|
-
originalHome = process.env.HOME;
|
|
113
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-quiet-lease-'));
|
|
114
|
-
process.env.HOME = tmpHome;
|
|
115
|
-
originalDisable = process.env.SM_JOB_WORKTREE_DISABLE;
|
|
116
|
-
process.env.SM_JOB_WORKTREE_DISABLE = '1';
|
|
117
|
-
({ spawnJob, reapDeadRunningJobs } = require('../scheduler.cjs'));
|
|
118
|
-
({ pickNextBatch } = require('../lib/schedulerBatch.cjs'));
|
|
119
|
-
quietMachineLease = require('../lib/quietMachineLease.cjs');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
afterAll(() => {
|
|
123
|
-
process.env.HOME = originalHome;
|
|
124
|
-
if (originalDisable === undefined) delete process.env.SM_JOB_WORKTREE_DISABLE;
|
|
125
|
-
else process.env.SM_JOB_WORKTREE_DISABLE = originalDisable;
|
|
126
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
afterEach(() => {
|
|
130
|
-
delete process.env.SM_CLAUDE_BIN;
|
|
131
|
-
quietMachineLease.__resetForTests();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test('a killed quietMachine job releases the exclusive lease, and an ordinary job dispatches on the next pick', async () => {
|
|
135
|
-
const projectCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-quiet-lease-project-'));
|
|
136
|
-
initRepo(projectCwd);
|
|
137
|
-
registerActiveProject(projectCwd);
|
|
138
|
-
|
|
139
|
-
const quietSlug = `1107-test-quiet-killed-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
140
|
-
const ordinarySlug = `1107-test-ordinary-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
141
|
-
// Epic-scoped, not the legacy flat prds/ dir: the flat dir is retired and
|
|
142
|
-
// swept into prds-archived/ by consolidateFlatPrds the moment a job is no
|
|
143
|
-
// longer pending/running, which would silently strip this PRD's
|
|
144
|
-
// `quietMachine: true` frontmatter from the row on the very next
|
|
145
|
-
// reconcile() refresh (or drop the row entirely once terminal — see
|
|
146
|
-
// scheduler-reconcile-history-backfill.test.cjs).
|
|
147
|
-
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
148
|
-
fs.mkdirSync(prdsDir, { recursive: true });
|
|
149
|
-
fs.writeFileSync(path.join(prdsDir, `${quietSlug}.md`), '---\nquietMachine: true\n---\nMeasure timing without contention, then get killed.', 'utf8');
|
|
150
|
-
fs.writeFileSync(path.join(prdsDir, `${ordinarySlug}.md`), 'An ordinary PRD queued behind the quiet job.', 'utf8');
|
|
151
|
-
|
|
152
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
153
|
-
{ slug: quietSlug, status: 'pending', cwd: projectCwd, quietMachine: true },
|
|
154
|
-
{ slug: ordinarySlug, status: 'pending', cwd: projectCwd, createdAt: new Date().toISOString() },
|
|
155
|
-
]);
|
|
156
|
-
|
|
157
|
-
process.env.SM_CLAUDE_BIN = writeKilledClaudeStub();
|
|
158
|
-
|
|
159
|
-
const runId = `run-${quietSlug}`;
|
|
160
|
-
const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
|
|
161
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
// Before spawnJob ever runs, demonstrate that a held lease (as it would
|
|
165
|
-
// be while quietSlug is genuinely running) blocks EVERY dispatch,
|
|
166
|
-
// including the ordinary sibling job sitting right behind it in the same
|
|
167
|
-
// project. Uses a standalone dummy holder so it doesn't interact with
|
|
168
|
-
// spawnJob's own acquire/release of the real lease below.
|
|
169
|
-
expect(quietMachineLease.acquire('dummy-holder-simulating-in-flight-run')).toBe(true);
|
|
170
|
-
const heldPick = pickNextBatch(
|
|
171
|
-
[
|
|
172
|
-
{ slug: quietSlug, status: 'running', cwd: projectCwd, quietMachine: true },
|
|
173
|
-
{ slug: ordinarySlug, status: 'pending', cwd: projectCwd, createdAt: new Date().toISOString() },
|
|
174
|
-
],
|
|
175
|
-
new Set([quietSlug]),
|
|
176
|
-
5,
|
|
177
|
-
{ leaseHeld: quietMachineLease.isHeld(), machineInUse: 1, now: Date.now() },
|
|
178
|
-
);
|
|
179
|
-
expect(heldPick.batch).toEqual([]);
|
|
180
|
-
quietMachineLease.release('dummy-holder-simulating-in-flight-run');
|
|
181
|
-
|
|
182
|
-
// Now the real thing: spawnJob acquires the lease itself for quietSlug,
|
|
183
|
-
// runs the killed stub, and must release it on this exit path.
|
|
184
|
-
await spawnJob({ slug: quietSlug, cwd: projectCwd, quietMachine: true }, runId, runDir, projectCwd);
|
|
185
|
-
|
|
186
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
187
|
-
const quietRow = jobs.find((j) => j.slug === quietSlug);
|
|
188
|
-
expect(quietRow).toBeTruthy();
|
|
189
|
-
expect(quietRow.exitCode).toBe(137);
|
|
190
|
-
expect(quietRow.quietMachine).toBe(true);
|
|
191
|
-
|
|
192
|
-
// The lease was released on this killed-job exit path — spawnJob's own
|
|
193
|
-
// finally block, the same one that releases the sessionSlots token.
|
|
194
|
-
expect(quietMachineLease.isHeld()).toBe(false);
|
|
195
|
-
|
|
196
|
-
// With the lease free again, the ordinary sibling job is now eligible.
|
|
197
|
-
const nextPick = pickNextBatch(
|
|
198
|
-
[
|
|
199
|
-
{ slug: quietSlug, status: 'failed', cwd: projectCwd, quietMachine: true },
|
|
200
|
-
{ slug: ordinarySlug, status: 'pending', cwd: projectCwd, createdAt: new Date().toISOString() },
|
|
201
|
-
],
|
|
202
|
-
new Set(),
|
|
203
|
-
5,
|
|
204
|
-
{ leaseHeld: quietMachineLease.isHeld(), machineInUse: 0, now: Date.now() },
|
|
205
|
-
);
|
|
206
|
-
expect(nextPick.batch.map((j) => j.slug)).toEqual([ordinarySlug]);
|
|
207
|
-
} finally {
|
|
208
|
-
fs.rmSync(projectCwd, { recursive: true, force: true });
|
|
209
|
-
fs.rmSync(runDir, { recursive: true, force: true });
|
|
210
|
-
}
|
|
211
|
-
}, 30_000);
|
|
212
|
-
|
|
213
|
-
test('reapDeadRunningJobs releases a quietMachine job\'s lease too — the process vanished without spawnJob\'s own finally ever running', async () => {
|
|
214
|
-
// This reaper exists exactly for the case spawnJob's own finally never
|
|
215
|
-
// runs (the process dies without the completion mutate() finishing) — see
|
|
216
|
-
// reapDeadRunningJobs's header comment. A quietMachine row reaped through
|
|
217
|
-
// THIS path must release the lease itself, or it stays wedged forever.
|
|
218
|
-
const projectCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-quiet-lease-reap-project-'));
|
|
219
|
-
registerActiveProject(projectCwd);
|
|
220
|
-
|
|
221
|
-
const slug = `1107-test-reaped-quiet-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
222
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
223
|
-
{
|
|
224
|
-
slug,
|
|
225
|
-
status: 'running',
|
|
226
|
-
cwd: projectCwd,
|
|
227
|
-
quietMachine: true,
|
|
228
|
-
runId: `run-${slug}`,
|
|
229
|
-
runtime: { pid: 999999 }, // guaranteed-dead pid (see reaperHelpers tests)
|
|
230
|
-
},
|
|
231
|
-
]);
|
|
232
|
-
const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', `run-${slug}`);
|
|
233
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
234
|
-
fs.writeFileSync(
|
|
235
|
-
path.join(runDir, `${slug}.log`),
|
|
236
|
-
'{"type":"result","subtype":"success","result":"done","is_error":false}\n',
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
// Simulate the lease as held by this row, exactly as it would be if
|
|
240
|
-
// spawnJob's own acquire ran but its finally never got the chance to.
|
|
241
|
-
expect(quietMachineLease.acquire(slug)).toBe(true);
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
await reapDeadRunningJobs();
|
|
245
|
-
|
|
246
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
247
|
-
const row = jobs.find((j) => j.slug === slug);
|
|
248
|
-
expect(row).toBeTruthy();
|
|
249
|
-
expect(row.status).toBe('completed');
|
|
250
|
-
|
|
251
|
-
// The lease must be released here too, not just in spawnJob's finally.
|
|
252
|
-
expect(quietMachineLease.isHeld()).toBe(false);
|
|
253
|
-
} finally {
|
|
254
|
-
fs.rmSync(projectCwd, { recursive: true, force: true });
|
|
255
|
-
fs.rmSync(runDir, { recursive: true, force: true });
|
|
256
|
-
}
|
|
257
|
-
});
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scheduler-rate-limit-cooldown-freshness.test.cjs — PRD 1119.
|
|
3
|
-
*
|
|
4
|
-
* setPaused()'s manual-override cooldown used to suppress WRITING any pause
|
|
5
|
-
* for 5 minutes after a human Resume/Run now, unconditionally — even when
|
|
6
|
-
* the triggering rate-limit observation came from a run that started AFTER
|
|
7
|
-
* the manual clear (i.e. brand-new evidence, not the stale auto-detection
|
|
8
|
-
* the cooldown exists to ignore). Combined with the halt-reset that resets a
|
|
9
|
-
* rate-limited job straight back to 'pending', this produced a hot dispatch
|
|
10
|
-
* loop: dispatch -> 429 in seconds -> reset to pending -> pause suppressed
|
|
11
|
-
* -> dispatch again, on a ~12s cadence, until the cooldown itself expired.
|
|
12
|
-
*
|
|
13
|
-
* isCooldownSuppressed() is the pure predicate this PRD extracts so both
|
|
14
|
-
* directions of the fix (fresh observation bypasses the cooldown; stale one
|
|
15
|
-
* stays suppressed) are directly unit-testable without touching queue.json,
|
|
16
|
-
* mutate(), or any fs state. nextRapidRateLimitCount() is the pure reducer
|
|
17
|
-
* behind the independent rapid-repeat hard-pause cap (a second circuit
|
|
18
|
-
* breaker for when the computed resumeAt is itself stale/wrong and would
|
|
19
|
-
* otherwise let the resume timer keep re-clearing the pause every ~30s,
|
|
20
|
-
* with every subsequent dispatch genuinely "fresh").
|
|
21
|
-
*
|
|
22
|
-
* Run: timeout 60 npx vitest run src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
'use strict';
|
|
26
|
-
|
|
27
|
-
import { test, expect } from 'vitest';
|
|
28
|
-
const {
|
|
29
|
-
isCooldownSuppressed,
|
|
30
|
-
nextRapidRateLimitCount,
|
|
31
|
-
CONSECUTIVE_RAPID_RATE_LIMIT_THRESHOLD,
|
|
32
|
-
RAPID_RATE_LIMIT_WINDOW_MS,
|
|
33
|
-
MANUAL_PAUSE_COOLDOWN_MS,
|
|
34
|
-
} = require('../scheduler.cjs');
|
|
35
|
-
|
|
36
|
-
const clearedAt = 1_000_000;
|
|
37
|
-
|
|
38
|
-
test('a rate_limit pause triggered by a run that STARTED AFTER the manual clear engages regardless of the cooldown', () => {
|
|
39
|
-
const now = clearedAt + 60_000; // well inside the 5-minute cooldown window
|
|
40
|
-
const observedAt = clearedAt + 5_000; // the triggering run started after the clear
|
|
41
|
-
const suppressed = isCooldownSuppressed({ pauseClearedManuallyAt: clearedAt, now, observedAt, force: false });
|
|
42
|
-
expect(suppressed).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('a rate_limit pause triggered by an observation OLDER than the manual clear stays suppressed', () => {
|
|
46
|
-
const now = clearedAt + 60_000;
|
|
47
|
-
const observedAt = clearedAt - 5_000; // stale: this run started before the human cleared the pause
|
|
48
|
-
const suppressed = isCooldownSuppressed({ pauseClearedManuallyAt: clearedAt, now, observedAt, force: false });
|
|
49
|
-
expect(suppressed).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('no observedAt at all (unrelated pause reasons, e.g. auth/network) is treated as stale — unchanged prior behavior', () => {
|
|
53
|
-
const now = clearedAt + 60_000;
|
|
54
|
-
const suppressed = isCooldownSuppressed({ pauseClearedManuallyAt: clearedAt, now, observedAt: null, force: false });
|
|
55
|
-
expect(suppressed).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('once the cooldown window itself has elapsed, suppression lifts regardless of freshness', () => {
|
|
59
|
-
const now = clearedAt + MANUAL_PAUSE_COOLDOWN_MS + 1;
|
|
60
|
-
const suppressed = isCooldownSuppressed({ pauseClearedManuallyAt: clearedAt, now, observedAt: null, force: false });
|
|
61
|
-
expect(suppressed).toBe(false);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('no manual clear on record never suppresses', () => {
|
|
65
|
-
const suppressed = isCooldownSuppressed({ pauseClearedManuallyAt: null, now: Date.now(), observedAt: null, force: false });
|
|
66
|
-
expect(suppressed).toBe(false);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('force always bypasses the cooldown, fresh or not', () => {
|
|
70
|
-
const now = clearedAt + 1_000;
|
|
71
|
-
const staleObservedAt = clearedAt - 5_000;
|
|
72
|
-
const suppressed = isCooldownSuppressed({ pauseClearedManuallyAt: clearedAt, now, observedAt: staleObservedAt, force: true });
|
|
73
|
-
expect(suppressed).toBe(false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// ---------- rapid-repeat consecutive counter ----------
|
|
77
|
-
|
|
78
|
-
test('a rate-limited run under the rapid window increments the counter', () => {
|
|
79
|
-
expect(nextRapidRateLimitCount(0, { rateLimited: true, durationMs: 4_000 })).toBe(1);
|
|
80
|
-
expect(nextRapidRateLimitCount(1, { rateLimited: true, durationMs: 4_000 })).toBe(2);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('a rate-limited run that ran for >= the rapid window does not increment the counter', () => {
|
|
84
|
-
expect(nextRapidRateLimitCount(1, { rateLimited: true, durationMs: RAPID_RATE_LIMIT_WINDOW_MS })).toBe(1);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('any non-rate-limited outcome resets the counter to 0', () => {
|
|
88
|
-
expect(nextRapidRateLimitCount(2, { rateLimited: false, durationMs: 999_999 })).toBe(0);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('driving the counter to the documented threshold engages the hard-pause condition', () => {
|
|
92
|
-
let count = 0;
|
|
93
|
-
for (let i = 0; i < CONSECUTIVE_RAPID_RATE_LIMIT_THRESHOLD; i += 1) {
|
|
94
|
-
count = nextRapidRateLimitCount(count, { rateLimited: true, durationMs: 3_000 });
|
|
95
|
-
}
|
|
96
|
-
expect(count).toBe(CONSECUTIVE_RAPID_RATE_LIMIT_THRESHOLD);
|
|
97
|
-
expect(count >= CONSECUTIVE_RAPID_RATE_LIMIT_THRESHOLD).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('reaching the threshold forces the pause through even against a STALE observation still inside the cooldown window', () => {
|
|
101
|
-
// This is the scenario the hard cap exists for: freshness alone is not
|
|
102
|
-
// enough when the computed resumeAt is itself stale/wrong and the resume
|
|
103
|
-
// timer keeps re-clearing the pause, so a LATER dispatch's observation can
|
|
104
|
-
// legitimately look "stale" relative to the most recent clear. The
|
|
105
|
-
// rapid-repeat counter is an independent, unconditional circuit breaker.
|
|
106
|
-
let count = 0;
|
|
107
|
-
for (let i = 0; i < CONSECUTIVE_RAPID_RATE_LIMIT_THRESHOLD; i += 1) {
|
|
108
|
-
count = nextRapidRateLimitCount(count, { rateLimited: true, durationMs: 3_000 });
|
|
109
|
-
}
|
|
110
|
-
const force = count >= CONSECUTIVE_RAPID_RATE_LIMIT_THRESHOLD;
|
|
111
|
-
const staleObservedAt = clearedAt - 999_999;
|
|
112
|
-
const suppressed = isCooldownSuppressed({ pauseClearedManuallyAt: clearedAt, now: clearedAt + 1_000, observedAt: staleObservedAt, force });
|
|
113
|
-
expect(suppressed).toBe(false);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('one non-rate-limited run in between resets the streak so the hard pause never engages', () => {
|
|
117
|
-
let count = 0;
|
|
118
|
-
count = nextRapidRateLimitCount(count, { rateLimited: true, durationMs: 3_000 });
|
|
119
|
-
count = nextRapidRateLimitCount(count, { rateLimited: true, durationMs: 3_000 });
|
|
120
|
-
count = nextRapidRateLimitCount(count, { rateLimited: false, durationMs: 60_000 }); // job actually ran fine
|
|
121
|
-
count = nextRapidRateLimitCount(count, { rateLimited: true, durationMs: 3_000 });
|
|
122
|
-
expect(count).toBeLessThan(CONSECUTIVE_RAPID_RATE_LIMIT_THRESHOLD);
|
|
123
|
-
});
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scheduler-rate-limit-pause.test.cjs — PRD 1118: pause against the BINDING
|
|
3
|
-
* rate-limit window read off a run's own log, not refreshNextReset()'s
|
|
4
|
-
* five_hour-only assumption, and never leave an indefinite pause with no
|
|
5
|
-
* resume timer when both the log and the billing endpoint come up empty
|
|
6
|
-
* (the live failure mode: the usage endpoint itself 429s).
|
|
7
|
-
*
|
|
8
|
-
* Also (usage-meter circuit PRD): neither function may arm a PAST reset —
|
|
9
|
-
* resolveRateLimitPauseReset/computeEffectiveResumeAt now reject a stale ISO
|
|
10
|
-
* via usageCircuit.isResetFresh and fall back to the bounded 30-MINUTE pause
|
|
11
|
-
* instead. A stale reset used to survive straight into computeResumeDelay's
|
|
12
|
-
* `Math.max(30_000, <negative>)`, arming a 30-SECOND nap that spun the queue
|
|
13
|
-
* right back into the same still-active rate limit.
|
|
14
|
-
*
|
|
15
|
-
* These test the PURE decision functions scheduler.cjs's setPaused() and the
|
|
16
|
-
* spawnJob() rateLimited branch delegate to — never the real setPaused/mutate
|
|
17
|
-
* path, which writes the machine's live scheduler queue.json at a fixed
|
|
18
|
-
* homedir path (~/.claude/session-manager/scheduled-plans) and must not be
|
|
19
|
-
* touched by a test run.
|
|
20
|
-
*
|
|
21
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-rate-limit-pause.test.cjs
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
'use strict';
|
|
25
|
-
|
|
26
|
-
import { test } from 'vitest';
|
|
27
|
-
const assert = require('node:assert/strict');
|
|
28
|
-
const path = require('node:path');
|
|
29
|
-
const {
|
|
30
|
-
resolveRateLimitPauseReset,
|
|
31
|
-
billingResetForPause,
|
|
32
|
-
computeEffectiveResumeAt,
|
|
33
|
-
computeResumeDelay,
|
|
34
|
-
computeDegradedBudget,
|
|
35
|
-
} = require('../scheduler.cjs');
|
|
36
|
-
const billing = require('../usage.cjs');
|
|
37
|
-
|
|
38
|
-
// Repo-tracked, byte-for-byte copy of the real 2026-09-05 starry-night-ships
|
|
39
|
-
// incident log (see src/main/lib/__tests__/rateLimitWindow.test.cjs for the
|
|
40
|
-
// live-path parity check) — kept hermetic rather than pointed at the
|
|
41
|
-
// machine-local live path directly.
|
|
42
|
-
const FIXTURE = path.join(__dirname, '..', 'lib', '__tests__', 'fixtures', '204-mercury-steam-horse.log.txt');
|
|
43
|
-
const FIVE_HOUR_ISO = new Date(1788643800 * 1000).toISOString(); // what refreshNextReset() would have returned
|
|
44
|
-
const SEVEN_DAY_ISO = new Date(1789059600 * 1000).toISOString(); // the actually-binding window
|
|
45
|
-
// Fixed "now" contemporaneous with the fixture's own timestamps (BEFORE both
|
|
46
|
-
// resets above), so freshness checks read the fixture the way it looked at
|
|
47
|
-
// incident time — not relative to whenever this suite happens to run.
|
|
48
|
-
const FIXTURE_NOW = Date.parse('2026-09-05T16:32:00.000Z');
|
|
49
|
-
|
|
50
|
-
test('resolveRateLimitPauseReset: prefers the log-derived seven-day reset over the billing five_hour reset', () => {
|
|
51
|
-
const resetIso = resolveRateLimitPauseReset(FIXTURE, FIVE_HOUR_ISO, FIXTURE_NOW);
|
|
52
|
-
assert.strictEqual(resetIso, SEVEN_DAY_ISO);
|
|
53
|
-
assert.notStrictEqual(resetIso, FIVE_HOUR_ISO);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('resolveRateLimitPauseReset: falls back to the billing reset when the log yields none', () => {
|
|
57
|
-
const resetIso = resolveRateLimitPauseReset('/nonexistent/path/job.log', FIVE_HOUR_ISO, FIXTURE_NOW);
|
|
58
|
-
assert.strictEqual(resetIso, FIVE_HOUR_ISO);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('resolveRateLimitPauseReset: null when BOTH the log and the billing endpoint are unavailable', () => {
|
|
62
|
-
assert.strictEqual(resolveRateLimitPauseReset('/nonexistent/path/job.log', null, FIXTURE_NOW), null);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('resolveRateLimitPauseReset: a PAST log-derived reset is rejected, not returned stale', () => {
|
|
66
|
-
// "now" is AFTER both fixture resets have already elapsed.
|
|
67
|
-
const longAfter = Date.parse('2026-09-15T00:00:00.000Z');
|
|
68
|
-
const resetIso = resolveRateLimitPauseReset(FIXTURE, FIVE_HOUR_ISO, longAfter);
|
|
69
|
-
assert.strictEqual(resetIso, null);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('resolveRateLimitPauseReset: log yields nothing and the billing reset has already passed -> null, not the stale value', () => {
|
|
73
|
-
const longAfter = Date.parse('2026-09-15T00:00:00.000Z');
|
|
74
|
-
const resetIso = resolveRateLimitPauseReset('/nonexistent/path/job.log', FIVE_HOUR_ISO, longAfter);
|
|
75
|
-
assert.strictEqual(resetIso, null);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('computeEffectiveResumeAt: rate_limit with no resumeAt gets the same bounded 30-minute fallback as network', () => {
|
|
79
|
-
const now = FIXTURE_NOW;
|
|
80
|
-
const resumeAt = computeEffectiveResumeAt('rate_limit', null, now);
|
|
81
|
-
assert.strictEqual(resumeAt, new Date(now + 30 * 60_000).toISOString());
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('computeEffectiveResumeAt: a FRESH explicit resumeAt (the log-derived reset) is never overridden', () => {
|
|
85
|
-
const resumeAt = computeEffectiveResumeAt('rate_limit', SEVEN_DAY_ISO, FIXTURE_NOW);
|
|
86
|
-
assert.strictEqual(resumeAt, SEVEN_DAY_ISO);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('computeEffectiveResumeAt: a PAST resumeAt gets the 30-minute fallback, not a ~30-second nap', () => {
|
|
90
|
-
// now is well AFTER SEVEN_DAY_ISO has already elapsed — the exact shape of
|
|
91
|
-
// the bug: Math.max(30_000, <negative>) used to arm a 30-second resume.
|
|
92
|
-
const longAfter = Date.parse('2026-09-15T00:00:00.000Z');
|
|
93
|
-
const resumeAt = computeEffectiveResumeAt('rate_limit', SEVEN_DAY_ISO, longAfter);
|
|
94
|
-
assert.strictEqual(resumeAt, new Date(longAfter + 30 * 60_000).toISOString());
|
|
95
|
-
assert.notStrictEqual(resumeAt, SEVEN_DAY_ISO);
|
|
96
|
-
const { delayMs } = computeResumeDelay(resumeAt, longAfter);
|
|
97
|
-
assert.ok(delayMs >= 30 * 60_000, `expected >= 30 minutes, got ${delayMs}ms`);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('computeEffectiveResumeAt: other reasons (e.g. auth) get no fallback — stays indefinite', () => {
|
|
101
|
-
assert.strictEqual(computeEffectiveResumeAt('auth', null, Date.now()), null);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('billingResetForPause: skips refreshNextReset() (no billing.fetchUsage call) while the shared circuit is OPEN', async () => {
|
|
105
|
-
const originalFetchUsage = billing.fetchUsage;
|
|
106
|
-
let fetchCalls = 0;
|
|
107
|
-
billing.fetchUsage = async () => { fetchCalls++; return { kind: 'ok', data: { usage: {} } }; };
|
|
108
|
-
try {
|
|
109
|
-
// Force the shared circuit open (3 consecutive failures), same threshold usageCircuit.cjs uses.
|
|
110
|
-
billing.usageCircuit.recordFailure('a');
|
|
111
|
-
billing.usageCircuit.recordFailure('a');
|
|
112
|
-
billing.usageCircuit.recordFailure('a');
|
|
113
|
-
assert.strictEqual(billing.usageCircuit.state(), 'open');
|
|
114
|
-
|
|
115
|
-
await billingResetForPause();
|
|
116
|
-
assert.strictEqual(fetchCalls, 0, 'refreshNextReset() must not call billing.fetchUsage() while the circuit is open');
|
|
117
|
-
} finally {
|
|
118
|
-
billing.usageCircuit.recordSuccess({}); // close the shared circuit back up for later tests/files
|
|
119
|
-
billing.fetchUsage = originalFetchUsage;
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('computeResumeDelay: a seven-day-out reset (~5 days from now) is armed, not treated as too-far', () => {
|
|
124
|
-
const now = Date.parse('2026-09-05T16:32:00.000Z');
|
|
125
|
-
// ~5 days out — comfortably under the 0x7fffffff (~24.8 day) setTimeout cap.
|
|
126
|
-
const fiveDaysOut = new Date(now + 5 * 24 * 60 * 60_000).toISOString();
|
|
127
|
-
const { delayMs, tooFar } = computeResumeDelay(fiveDaysOut, now);
|
|
128
|
-
assert.strictEqual(tooFar, false);
|
|
129
|
-
assert.ok(delayMs < 0x7fffffff, `expected delay under setTimeout cap, got ${delayMs}`);
|
|
130
|
-
assert.ok(delayMs > 400_000_000, `expected ~5 days in ms, got ${delayMs}`);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('computeResumeDelay: a reset far enough out DOES trip the overflow guard', () => {
|
|
134
|
-
const now = Date.parse('2026-09-05T16:32:00.000Z');
|
|
135
|
-
const thirtyDaysOut = new Date(now + 30 * 24 * 60 * 60_000).toISOString();
|
|
136
|
-
const { tooFar } = computeResumeDelay(thirtyDaysOut, now);
|
|
137
|
-
assert.strictEqual(tooFar, true);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('refreshNextReset (via billingResetForPause) seeds lastGoodUsagePayload: a following degraded cycle yields the fetched percent, not 100', async () => {
|
|
141
|
-
const originalFetchUsage = billing.fetchUsage;
|
|
142
|
-
billing.fetchUsage = async () => ({
|
|
143
|
-
kind: 'ok',
|
|
144
|
-
data: { usage: { limits: [{ kind: 'weekly_all', percent: 64, resets_at: '2026-09-24T17:00:00Z', scope: null, is_active: true }] } },
|
|
145
|
-
});
|
|
146
|
-
try {
|
|
147
|
-
await billingResetForPause();
|
|
148
|
-
assert.strictEqual(computeDegradedBudget().utilization, 64);
|
|
149
|
-
} finally {
|
|
150
|
-
billing.fetchUsage = originalFetchUsage;
|
|
151
|
-
}
|
|
152
|
-
});
|