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,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scheduler-utilization-hold.test.cjs — the when-available gate holds on the
|
|
3
|
-
* BINDING window and records a machine-readable hold (window/percent/
|
|
4
|
-
* threshold/reset) on the snapshot; under threshold it dispatches.
|
|
5
|
-
* Run: timeout 180 npx vitest run src/main/__tests__/scheduler-utilization-hold.test.cjs
|
|
6
|
-
*/
|
|
7
|
-
'use strict';
|
|
8
|
-
|
|
9
|
-
import { test, expect, beforeAll, afterAll, 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 originalHome;
|
|
16
|
-
let scheduler;
|
|
17
|
-
let billing;
|
|
18
|
-
let originalFetchUsage;
|
|
19
|
-
|
|
20
|
-
beforeAll(() => {
|
|
21
|
-
originalHome = process.env.HOME;
|
|
22
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-utilhold-'));
|
|
23
|
-
process.env.HOME = tmpHome;
|
|
24
|
-
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
|
|
25
|
-
fs.mkdirSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true });
|
|
26
|
-
scheduler = require('../scheduler.cjs');
|
|
27
|
-
billing = require('../usage.cjs');
|
|
28
|
-
originalFetchUsage = billing.fetchUsage;
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterAll(() => {
|
|
32
|
-
billing.fetchUsage = originalFetchUsage;
|
|
33
|
-
process.env.HOME = originalHome;
|
|
34
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
afterEach(() => { billing.fetchUsage = originalFetchUsage; });
|
|
38
|
-
|
|
39
|
-
const RESET = '2026-09-24T17:00:00Z';
|
|
40
|
-
function usageWith(weeklyPct) {
|
|
41
|
-
return {
|
|
42
|
-
kind: 'ok',
|
|
43
|
-
data: {
|
|
44
|
-
usage: {
|
|
45
|
-
five_hour: { utilization: 10, resets_at: '2026-09-21T20:00:00Z' },
|
|
46
|
-
limits: [
|
|
47
|
-
{ kind: 'session', percent: 10, scope: null, resets_at: '2026-09-21T20:00:00Z' },
|
|
48
|
-
{ kind: 'weekly_all', percent: weeklyPct, scope: null, resets_at: RESET },
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// dependsOn never resolves so a tick reaches the picker (stamping
|
|
56
|
-
// lastDispatchAttemptAt) without ever spawning a real job.
|
|
57
|
-
function state() {
|
|
58
|
-
return {
|
|
59
|
-
config: { firePolicy: 'when-available', utilizationThreshold: 90 },
|
|
60
|
-
paused: null,
|
|
61
|
-
jobs: [{ slug: '9002-hold', title: 'x', cwd: tmpHome, status: 'pending', dependsOn: ['never'] }],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
test('holds (no tick) and records the hold when the binding weekly window is over threshold', async () => {
|
|
66
|
-
billing.fetchUsage = async () => usageWith(91);
|
|
67
|
-
await scheduler.refreshNextReset();
|
|
68
|
-
const logs = [];
|
|
69
|
-
const orig = console.log;
|
|
70
|
-
console.log = (...a) => { logs.push(a.join(' ')); };
|
|
71
|
-
try { await scheduler.maybeLaunchWhenAvailable(state()); } finally { console.log = orig; }
|
|
72
|
-
const payload = scheduler.buildScheduleStatePayload(state());
|
|
73
|
-
expect(payload.utilizationHold).toEqual({ window: 'weekly_all', percent: 91, threshold: 90, resetsAt: RESET });
|
|
74
|
-
expect(payload.utilizationWindow).toBe('weekly_all');
|
|
75
|
-
expect(logs.some((l) => l.includes('utilization-held') && l.includes('weekly_all'))).toBe(true);
|
|
76
|
-
expect(logs.some((l) => l.includes('— ticking'))).toBe(false);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('ticks and clears the hold when the binding window is under threshold (weekly_all 64)', async () => {
|
|
80
|
-
billing.fetchUsage = async () => usageWith(64);
|
|
81
|
-
await scheduler.refreshNextReset();
|
|
82
|
-
const logs = [];
|
|
83
|
-
const orig = console.log;
|
|
84
|
-
console.log = (...a) => { logs.push(a.join(' ')); };
|
|
85
|
-
try { await scheduler.maybeLaunchWhenAvailable(state()); } finally { console.log = orig; }
|
|
86
|
-
await scheduler.tickQueue();
|
|
87
|
-
expect(scheduler.buildScheduleStatePayload(state()).utilizationHold).toBeNull();
|
|
88
|
-
expect(logs.some((l) => l.includes('— ticking'))).toBe(true);
|
|
89
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scheduler-verify-prd-path.test.cjs — regression test for PRD 991: the
|
|
3
|
-
* verifier's `prdPath` must resolve to a REAL, existing file for an
|
|
4
|
-
* Epic-scoped PRD, both live and archived.
|
|
5
|
-
*
|
|
6
|
-
* Before this PRD, `verifyRun`'s two call sites (the post-run verify in
|
|
7
|
-
* spawnJob and reverifyNeedsReview) built `prdPath` via `prdPathForJob`,
|
|
8
|
-
* which resolves to the RETIRED flat
|
|
9
|
-
* `<cwd>/session-manager-operations/scheduler/prds/` dir — a dir that holds
|
|
10
|
-
* only zero-byte `.reserved-NNN` stubs for every project that has migrated
|
|
11
|
-
* to Epic-scoped PRDs (i.e. every project today). The verifier therefore
|
|
12
|
-
* silently lost access to the PRD body (dependsOn checks, soak-floor dates,
|
|
13
|
-
* deliverable-path checks, the merge-main PR-number exemption) on every
|
|
14
|
-
* single run.
|
|
15
|
-
*
|
|
16
|
-
* This test exercises actual path resolution against a temp tree — no
|
|
17
|
-
* resolver-stubbing — the same class of gap that let this bug (and PRD
|
|
18
|
-
* 985's sibling notify-path bug) ship green twice.
|
|
19
|
-
*
|
|
20
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-verify-prd-path.test.cjs
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
'use strict';
|
|
24
|
-
|
|
25
|
-
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
26
|
-
const fs = require('node:fs');
|
|
27
|
-
const os = require('node:os');
|
|
28
|
-
const path = require('node:path');
|
|
29
|
-
|
|
30
|
-
let tmpHome;
|
|
31
|
-
let originalHome;
|
|
32
|
-
let resolveVerifyPrdPath;
|
|
33
|
-
let archivedPrdPathForJob;
|
|
34
|
-
let PRDS_DIR;
|
|
35
|
-
|
|
36
|
-
beforeAll(() => {
|
|
37
|
-
originalHome = process.env.HOME;
|
|
38
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-verify-prd-path-'));
|
|
39
|
-
process.env.HOME = tmpHome;
|
|
40
|
-
|
|
41
|
-
// Load AFTER HOME is stubbed — scheduler.cjs snapshots os.homedir() into
|
|
42
|
-
// PRDS_DIR at module load, not lazily (see scheduler-find-prd-dir.test.cjs).
|
|
43
|
-
({ resolveVerifyPrdPath, archivedPrdPathForJob, PRDS_DIR } = require('../scheduler.cjs'));
|
|
44
|
-
|
|
45
|
-
if (!PRDS_DIR.startsWith(tmpHome)) {
|
|
46
|
-
throw new Error(`refusing to run: PRDS_DIR (${PRDS_DIR}) is not under the temp HOME (${tmpHome})`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// allProjectCwds()/activeProjectCwds() discover project cwds by scanning
|
|
50
|
-
// ~/.claude/projects/*/*.jsonl for a `cwd` field — fake one project with a
|
|
51
|
-
// transcript pointing at a real on-disk cwd.
|
|
52
|
-
const projectsDir = path.join(tmpHome, '.claude', 'projects');
|
|
53
|
-
const projDir = path.join(projectsDir, 'fake-project-slug');
|
|
54
|
-
fs.mkdirSync(projDir, { recursive: true });
|
|
55
|
-
fs.writeFileSync(
|
|
56
|
-
path.join(projDir, 'session.jsonl'),
|
|
57
|
-
`${JSON.stringify({ cwd: projectCwd() })}\n`,
|
|
58
|
-
'utf8',
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
afterAll(() => {
|
|
63
|
-
process.env.HOME = originalHome;
|
|
64
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
function projectCwd() {
|
|
68
|
-
return path.join(tmpHome, 'fake-project-cwd');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
test('resolveVerifyPrdPath resolves a LIVE Epic-scoped PRD to a real existing path', async () => {
|
|
72
|
-
const cwd = projectCwd();
|
|
73
|
-
const epicId = 'test-epic-live';
|
|
74
|
-
const slug = `991-test-live-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
75
|
-
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId, 'prds');
|
|
76
|
-
fs.mkdirSync(prdsDir, { recursive: true });
|
|
77
|
-
const prdPath = path.join(prdsDir, `${slug}.md`);
|
|
78
|
-
fs.writeFileSync(prdPath, '---\ntitle: live fixture\n---\n\n# Goal\n\ntest\n', 'utf8');
|
|
79
|
-
|
|
80
|
-
const resolved = await resolveVerifyPrdPath({ slug, cwd });
|
|
81
|
-
expect(resolved).toBe(prdPath);
|
|
82
|
-
expect(fs.existsSync(resolved)).toBe(true);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('resolveVerifyPrdPath returns null for an ARCHIVED-only PRD; archivedPrdPathForJob fills the gap', async () => {
|
|
86
|
-
const cwd = projectCwd();
|
|
87
|
-
const epicId = 'test-epic-archived';
|
|
88
|
-
const slug = `991-test-archived-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
89
|
-
const epicDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId);
|
|
90
|
-
const archiveDir = path.join(epicDir, 'prds-archived');
|
|
91
|
-
fs.mkdirSync(archiveDir, { recursive: true });
|
|
92
|
-
const archivedPath = path.join(archiveDir, `${slug}.md`);
|
|
93
|
-
fs.writeFileSync(archivedPath, '---\ntitle: archived fixture\n---\n\n# Goal\n\ntest\n', 'utf8');
|
|
94
|
-
|
|
95
|
-
const job = { slug, cwd };
|
|
96
|
-
const live = await resolveVerifyPrdPath(job);
|
|
97
|
-
expect(live).toBe(null);
|
|
98
|
-
|
|
99
|
-
const fallback = live ?? archivedPrdPathForJob(job);
|
|
100
|
-
expect(fallback).toBe(archivedPath);
|
|
101
|
-
expect(fs.existsSync(fallback)).toBe(true);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('resolveVerifyPrdPath returns null for a slug that exists nowhere (never falls back to the retired flat dir)', async () => {
|
|
105
|
-
const cwd = projectCwd();
|
|
106
|
-
const slug = `991-test-nonexistent-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
107
|
-
const resolved = await resolveVerifyPrdPath({ slug, cwd });
|
|
108
|
-
expect(resolved).toBe(null);
|
|
109
|
-
});
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scheduler-worktree-cap-defer.test.cjs — PRD 1112: a job that cannot get
|
|
3
|
-
* worktree isolation because the concurrency cap is currently exhausted must
|
|
4
|
-
* be DEFERRED (left 'pending', retried on the next dispatch pass), never
|
|
5
|
-
* spawned in a shared working tree the way every OTHER worktree.ok===false
|
|
6
|
-
* reason (e.g. "not a git repository") still falls back to running in place.
|
|
7
|
-
*
|
|
8
|
-
* Drives spawnJob end-to-end against a real git fixture repo, forcing the
|
|
9
|
-
* cap via SM_JOB_WORKTREE_MAX=1 plus a real occupying worktree (so the cap
|
|
10
|
-
* check itself, not a mock, is what defers the job).
|
|
11
|
-
*
|
|
12
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-worktree-cap-defer.test.cjs
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
'use strict';
|
|
16
|
-
|
|
17
|
-
import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
18
|
-
const fs = require('node:fs');
|
|
19
|
-
const os = require('node:os');
|
|
20
|
-
const path = require('node:path');
|
|
21
|
-
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
22
|
-
const { execFileSync } = require('node:child_process');
|
|
23
|
-
|
|
24
|
-
let tmpHome;
|
|
25
|
-
let originalHome;
|
|
26
|
-
let originalMax;
|
|
27
|
-
let spawnJob;
|
|
28
|
-
let gitWorktree;
|
|
29
|
-
|
|
30
|
-
function git(args, cwd) {
|
|
31
|
-
return execFileSync('git', args, { cwd, encoding: 'utf8' });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// A completed job's PRD is archived immediately (archiveCompletedPrd, called
|
|
35
|
-
// for EVERY job that reaches effectiveStatus:'completed' — not just a
|
|
36
|
-
// retention-aged one), and the very next reconcile() pass (spawnJob's own
|
|
37
|
-
// broadcast({flush:true}) right after finishing) then finds the row's PRD
|
|
38
|
-
// gone and backfills+drops it from queue.json into history.jsonl in that
|
|
39
|
-
// SAME pass — see scheduler-reconcile-history-backfill.test.cjs. So a job
|
|
40
|
-
// that actually completed is found in EITHER queue.json OR history.jsonl,
|
|
41
|
-
// never guaranteed to still be the former.
|
|
42
|
-
function readTerminalRow(queuePath, projectCwd, slug) {
|
|
43
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
44
|
-
const row = jobs.find((j) => j.slug === slug);
|
|
45
|
-
if (row) return row;
|
|
46
|
-
const historyPath = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state', 'history.jsonl');
|
|
47
|
-
if (!fs.existsSync(historyPath)) return undefined;
|
|
48
|
-
const lines = fs.readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
49
|
-
return lines.map((l) => JSON.parse(l)).reverse().find((j) => j.slug === slug);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function initRepo(dir) {
|
|
53
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
-
git(['init', '-q'], dir);
|
|
55
|
-
git(['config', 'user.email', 'test@example.com'], dir);
|
|
56
|
-
git(['config', 'user.name', 'Test'], dir);
|
|
57
|
-
fs.writeFileSync(path.join(dir, 'README.md'), 'hello\n', 'utf8');
|
|
58
|
-
git(['add', '-A'], dir);
|
|
59
|
-
git(['commit', '-q', '-m', 'initial'], dir);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// See scheduler-quiet-machine-lease.test.cjs's identical helper: readQueue()'s
|
|
63
|
-
// per-project discovery walks ~/.claude/projects/*/*.jsonl transcripts for a
|
|
64
|
-
// `cwd` field to find known project queue.json shards.
|
|
65
|
-
function registerActiveProject(cwd) {
|
|
66
|
-
const projectsDir = path.join(tmpHome, '.claude', 'projects');
|
|
67
|
-
const slugDir = path.join(projectsDir, `fake-project-slug-${path.basename(cwd)}`);
|
|
68
|
-
fs.mkdirSync(slugDir, { recursive: true });
|
|
69
|
-
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function writeProjectQueue(cwd, jobs) {
|
|
73
|
-
const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
|
|
74
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
75
|
-
fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({ jobs }, null, 2));
|
|
76
|
-
return path.join(stateDir, 'queue.json');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Stub `claude` binary: writes a marker file into its OWN process.cwd() so a
|
|
80
|
-
// test can prove whether — and where — it was actually spawned, then emits a
|
|
81
|
-
// clean stream-json success result.
|
|
82
|
-
function writeClaudeStub() {
|
|
83
|
-
return claudeStub.writeClaudeStub({
|
|
84
|
-
allowCommitUnder: os.tmpdir(),
|
|
85
|
-
body: `
|
|
86
|
-
const fs = require('fs');
|
|
87
|
-
const path = require('path');
|
|
88
|
-
const { execFileSync } = require('child_process');
|
|
89
|
-
fs.writeFileSync(path.join(process.cwd(), 'ran-here.marker'), 'yes', 'utf8');
|
|
90
|
-
// Land a real commit + finish-protocol sentinel when possible, so a run
|
|
91
|
-
// that actually reaches the finish protocol lands 'completed' — a
|
|
92
|
-
// best-effort no-op when the cwd isn't a git repo (e.g. the non-git
|
|
93
|
-
// fallback case), where the run parks in needs_review instead, which is
|
|
94
|
-
// fine — this test only cares whether it ran in place, not its verdict.
|
|
95
|
-
try {
|
|
96
|
-
runGit(['add', '-A']);
|
|
97
|
-
runGit(['commit', '-q', '-m', 'stub commit']);
|
|
98
|
-
} catch { /* not a git repo — best-effort only */ }
|
|
99
|
-
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok\\nSCHEDULER_VERDICT: PASS' }) + '\\n');
|
|
100
|
-
process.exit(0);
|
|
101
|
-
`,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
beforeAll(() => {
|
|
106
|
-
originalHome = process.env.HOME;
|
|
107
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-cap-defer-'));
|
|
108
|
-
process.env.HOME = tmpHome;
|
|
109
|
-
originalMax = process.env.SM_JOB_WORKTREE_MAX;
|
|
110
|
-
({ spawnJob } = require('../scheduler.cjs'));
|
|
111
|
-
gitWorktree = require('../lib/gitWorktree.cjs');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
afterAll(() => {
|
|
115
|
-
process.env.HOME = originalHome;
|
|
116
|
-
if (originalMax === undefined) delete process.env.SM_JOB_WORKTREE_MAX;
|
|
117
|
-
else process.env.SM_JOB_WORKTREE_MAX = originalMax;
|
|
118
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
afterEach(() => {
|
|
122
|
-
delete process.env.SM_CLAUDE_BIN;
|
|
123
|
-
delete process.env.SM_JOB_WORKTREE_MAX;
|
|
124
|
-
gitWorktree._resetActiveWorktreeCountForTests('job', 0);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('a job held behind the worktree cap stays pending with a heldReason, is never spawned in place, and dispatches once the cap frees up', async () => {
|
|
128
|
-
const projectCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-cap-defer-project-'));
|
|
129
|
-
initRepo(projectCwd);
|
|
130
|
-
registerActiveProject(projectCwd);
|
|
131
|
-
|
|
132
|
-
const slug = `1112-test-held-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
133
|
-
// Epic-scoped, not the legacy flat prds/ dir: the flat dir is retired and
|
|
134
|
-
// swept into prds-archived/ by consolidateFlatPrds on every reconcile()
|
|
135
|
-
// pass once a row is no longer 'pending'/'running' — which would strand
|
|
136
|
-
// this test's row the moment it completes (reconcile()'s terminal-job
|
|
137
|
-
// "PRD gone -> archived on purpose" drop, see
|
|
138
|
-
// scheduler-reconcile-history-backfill.test.cjs).
|
|
139
|
-
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
140
|
-
fs.mkdirSync(prdsDir, { recursive: true });
|
|
141
|
-
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Held behind the worktree cap.', 'utf8');
|
|
142
|
-
const queuePath = writeProjectQueue(projectCwd, [{ slug, status: 'pending', cwd: projectCwd }]);
|
|
143
|
-
|
|
144
|
-
process.env.SM_JOB_WORKTREE_MAX = '1';
|
|
145
|
-
process.env.SM_CLAUDE_BIN = writeClaudeStub();
|
|
146
|
-
|
|
147
|
-
// Occupy the single worktree slot with a REAL checkout, so the cap check
|
|
148
|
-
// spawnJob relies on is exercising the actual gitWorktree plumbing, not a
|
|
149
|
-
// mock.
|
|
150
|
-
const occupier = await gitWorktree.createJobWorktree({ cwd: projectCwd, slug: 'occupier' });
|
|
151
|
-
expect(occupier.ok).toBe(true);
|
|
152
|
-
|
|
153
|
-
const runId = `run-${slug}`;
|
|
154
|
-
const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
|
|
155
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
await spawnJob({ slug, cwd: projectCwd }, runId, runDir, projectCwd);
|
|
159
|
-
|
|
160
|
-
// Held: still pending, never spawned, heldReason visible on the row.
|
|
161
|
-
let jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
162
|
-
let row = jobs.find((j) => j.slug === slug);
|
|
163
|
-
expect(row.status).toBe('pending');
|
|
164
|
-
expect(row.heldReason).toMatch(/worktree cap reached/);
|
|
165
|
-
expect(fs.existsSync(path.join(projectCwd, 'ran-here.marker'))).toBe(false);
|
|
166
|
-
expect(fs.existsSync(path.join(occupier.dir, 'ran-here.marker'))).toBe(false);
|
|
167
|
-
|
|
168
|
-
// Free the cap (mirrors cleanupJobWorktree releasing the counter), then
|
|
169
|
-
// retry — the held job dispatches on this next pass, exactly like a
|
|
170
|
-
// sessionSlots-blocked job retries once a slot frees up.
|
|
171
|
-
await gitWorktree.cleanupJobWorktree({ cwd: projectCwd, dir: occupier.dir, branch: occupier.branch });
|
|
172
|
-
|
|
173
|
-
await spawnJob({ slug, cwd: projectCwd }, `${runId}-2`, runDir, projectCwd);
|
|
174
|
-
|
|
175
|
-
row = readTerminalRow(queuePath, projectCwd, slug);
|
|
176
|
-
expect(row).toBeTruthy();
|
|
177
|
-
expect(row.status).toBe('completed');
|
|
178
|
-
expect(row.heldReason).toBeUndefined();
|
|
179
|
-
} finally {
|
|
180
|
-
fs.rmSync(projectCwd, { recursive: true, force: true });
|
|
181
|
-
fs.rmSync(runDir, { recursive: true, force: true });
|
|
182
|
-
}
|
|
183
|
-
}, 30_000);
|
|
184
|
-
|
|
185
|
-
test('a non-git cwd still falls back to running in place (unchanged behaviour — only cap-reached is a deferral)', async () => {
|
|
186
|
-
const projectCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-cap-defer-nongit-'));
|
|
187
|
-
fs.mkdirSync(projectCwd, { recursive: true }); // deliberately NOT a git repo
|
|
188
|
-
registerActiveProject(projectCwd);
|
|
189
|
-
|
|
190
|
-
const slug = `1112-test-nongit-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
191
|
-
// Epic-scoped — see the sibling test above for why not the flat prds/ dir.
|
|
192
|
-
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
193
|
-
fs.mkdirSync(prdsDir, { recursive: true });
|
|
194
|
-
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Non-git cwd, must run in place.', 'utf8');
|
|
195
|
-
const queuePath = writeProjectQueue(projectCwd, [{ slug, status: 'pending', cwd: projectCwd }]);
|
|
196
|
-
|
|
197
|
-
process.env.SM_CLAUDE_BIN = writeClaudeStub();
|
|
198
|
-
|
|
199
|
-
const runId = `run-${slug}`;
|
|
200
|
-
const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
|
|
201
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
await spawnJob({ slug, cwd: projectCwd }, runId, runDir, projectCwd);
|
|
205
|
-
|
|
206
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
207
|
-
const row = jobs.find((j) => j.slug === slug);
|
|
208
|
-
// Not held/deferred — it actually ran (a non-git cwd can't commit, so it
|
|
209
|
-
// parks in needs_review rather than completed; this test only cares that
|
|
210
|
-
// it was dispatched in place, not its exact verdict).
|
|
211
|
-
expect(row.status).not.toBe('pending');
|
|
212
|
-
expect(row.heldReason).toBeUndefined();
|
|
213
|
-
expect(row.worktreeFallbackReason).toMatch(/not a git repository/);
|
|
214
|
-
// Ran IN PLACE — the marker landed directly in projectCwd, not a worktree.
|
|
215
|
-
expect(fs.existsSync(path.join(projectCwd, 'ran-here.marker'))).toBe(true);
|
|
216
|
-
|
|
217
|
-
// A job losing worktree isolation must never be a silent downgrade
|
|
218
|
-
// discoverable only by reading queue.json afterwards — it must also land
|
|
219
|
-
// as a warn line in the durable ops error log, naming the slug, cwd, and
|
|
220
|
-
// reason.
|
|
221
|
-
const { todayFile } = require('../lib/opsErrorLog.cjs');
|
|
222
|
-
const errorLogPath = todayFile(projectCwd);
|
|
223
|
-
expect(fs.existsSync(errorLogPath)).toBe(true);
|
|
224
|
-
const logLines = fs.readFileSync(errorLogPath, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
225
|
-
const fallbackLine = logLines.find((l) => l.scope === 'scheduler' && l.level === 'warn' && l.message.includes(slug));
|
|
226
|
-
expect(fallbackLine).toBeTruthy();
|
|
227
|
-
expect(fallbackLine.message).toMatch(/SHARED working tree/);
|
|
228
|
-
expect(fallbackLine.message).toMatch(/not a git repository/);
|
|
229
|
-
expect(fallbackLine.meta).toMatchObject({ slug, cwd: projectCwd, reason: expect.stringMatching(/not a git repository/) });
|
|
230
|
-
} finally {
|
|
231
|
-
fs.rmSync(projectCwd, { recursive: true, force: true });
|
|
232
|
-
fs.rmSync(runDir, { recursive: true, force: true });
|
|
233
|
-
}
|
|
234
|
-
}, 30_000);
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scheduler-worktree-exec-cwd.test.cjs — PRD 994, LOAD-BEARING acceptance
|
|
3
|
-
* criterion: when a job is isolated into a worktree, its PRD file, queue
|
|
4
|
-
* state, and run logs must still resolve to the MAIN tree, never the
|
|
5
|
-
* worktree — `session-manager-operations/` is resolved from a `cwd` param
|
|
6
|
-
* (lib/prdLocations.cjs), so a naive cwd swap would give the job a
|
|
7
|
-
* different, empty ops root and orphan its own PRD.
|
|
8
|
-
*
|
|
9
|
-
* Drives executeJob's real `execCwd` parameter (the 5th arg scheduler.cjs
|
|
10
|
-
* passes when a worktree was created) with a stub `claude` binary, proving:
|
|
11
|
-
* 1. the child process's OWN cwd is the worktree dir (execCwd) — verified
|
|
12
|
-
* by having the stub write a marker file to `process.cwd()`;
|
|
13
|
-
* 2. the PRD source is still found via job.cwd (the main tree) even though
|
|
14
|
-
* the worktree dir has no PRD file in it at all;
|
|
15
|
-
* 3. the run's meta.json still records job.cwd, not the worktree dir.
|
|
16
|
-
*
|
|
17
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
'use strict';
|
|
21
|
-
|
|
22
|
-
import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
23
|
-
const fs = require('node:fs');
|
|
24
|
-
const os = require('node:os');
|
|
25
|
-
const path = require('node:path');
|
|
26
|
-
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
27
|
-
|
|
28
|
-
let tmpHome;
|
|
29
|
-
let originalHome;
|
|
30
|
-
let executeJob;
|
|
31
|
-
|
|
32
|
-
beforeAll(() => {
|
|
33
|
-
originalHome = process.env.HOME;
|
|
34
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-execcwd-'));
|
|
35
|
-
process.env.HOME = tmpHome;
|
|
36
|
-
({ executeJob } = require('../scheduler.cjs'));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterAll(() => {
|
|
40
|
-
process.env.HOME = originalHome;
|
|
41
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
delete process.env.SM_CLAUDE_BIN;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Stub `claude` binary: writes a marker file into its OWN process.cwd() (proving
|
|
49
|
-
// where it actually ran), captures the '-p' prompt, then emits a stream-json
|
|
50
|
-
// success result and exits 0.
|
|
51
|
-
function writeClaudeStub() {
|
|
52
|
-
return claudeStub.writeClaudeStub({ body: `
|
|
53
|
-
const fs = require('fs');
|
|
54
|
-
const path = require('path');
|
|
55
|
-
fs.writeFileSync(path.join(process.cwd(), 'ran-here.marker'), 'yes', 'utf8');
|
|
56
|
-
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
|
|
57
|
-
process.exit(0);
|
|
58
|
-
` });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
test('executeJob spawns the child in execCwd (worktree) while resolving the PRD from job.cwd (main tree)', async () => {
|
|
62
|
-
const mainCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-main-'));
|
|
63
|
-
const worktreeCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-exec-'));
|
|
64
|
-
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
65
|
-
const runDir = fs.mkdtempSync(path.join(tmpHome, '.claude', 'sm-worktree-run-'));
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const slug = `994-test-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
69
|
-
const prdsDir = path.join(mainCwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
70
|
-
fs.mkdirSync(prdsDir, { recursive: true });
|
|
71
|
-
const bodyText = 'Do the isolated-worktree thing.';
|
|
72
|
-
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), bodyText, 'utf8');
|
|
73
|
-
|
|
74
|
-
process.env.SM_CLAUDE_BIN = writeClaudeStub();
|
|
75
|
-
|
|
76
|
-
const job = { slug, cwd: mainCwd };
|
|
77
|
-
const result = await executeJob(job, runDir, mainCwd, () => Promise.resolve(), worktreeCwd);
|
|
78
|
-
|
|
79
|
-
expect(result.exitCode).toBe(0);
|
|
80
|
-
|
|
81
|
-
// 1. The child process really ran with cwd = worktreeCwd, not mainCwd.
|
|
82
|
-
expect(fs.existsSync(path.join(worktreeCwd, 'ran-here.marker'))).toBe(true);
|
|
83
|
-
expect(fs.existsSync(path.join(mainCwd, 'ran-here.marker'))).toBe(false);
|
|
84
|
-
|
|
85
|
-
// 2. The PRD source itself is untouched — resolved via job.cwd, not execCwd.
|
|
86
|
-
const onDiskPrd = fs.readFileSync(path.join(prdsDir, `${slug}.md`), 'utf8');
|
|
87
|
-
expect(onDiskPrd).toBe(bodyText);
|
|
88
|
-
|
|
89
|
-
// 3. The run's meta.json records job.cwd (the main tree), not the worktree.
|
|
90
|
-
const metaPath = path.join(runDir, `${slug}.meta.json`);
|
|
91
|
-
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
92
|
-
expect(meta.cwd).toBe(mainCwd);
|
|
93
|
-
} finally {
|
|
94
|
-
fs.rmSync(mainCwd, { recursive: true, force: true });
|
|
95
|
-
fs.rmSync(worktreeCwd, { recursive: true, force: true });
|
|
96
|
-
fs.rmSync(runDir, { recursive: true, force: true });
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('executeJob falls back to job.cwd for the spawn cwd when no execCwd is supplied (unchanged behaviour)', async () => {
|
|
101
|
-
const mainCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-fallback-'));
|
|
102
|
-
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
103
|
-
const runDir = fs.mkdtempSync(path.join(tmpHome, '.claude', 'sm-worktree-run2-'));
|
|
104
|
-
try {
|
|
105
|
-
const slug = `994-test-fallback-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
106
|
-
const prdsDir = path.join(mainCwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
107
|
-
fs.mkdirSync(prdsDir, { recursive: true });
|
|
108
|
-
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'no worktree here', 'utf8');
|
|
109
|
-
process.env.SM_CLAUDE_BIN = writeClaudeStub();
|
|
110
|
-
|
|
111
|
-
const job = { slug, cwd: mainCwd };
|
|
112
|
-
const result = await executeJob(job, runDir, mainCwd, () => Promise.resolve());
|
|
113
|
-
|
|
114
|
-
expect(result.exitCode).toBe(0);
|
|
115
|
-
expect(fs.existsSync(path.join(mainCwd, 'ran-here.marker'))).toBe(true);
|
|
116
|
-
} finally {
|
|
117
|
-
fs.rmSync(mainCwd, { recursive: true, force: true });
|
|
118
|
-
fs.rmSync(runDir, { recursive: true, force: true });
|
|
119
|
-
}
|
|
120
|
-
});
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scheduler-writeprd-epic-rollback.test.cjs — remote.writePrd may only JOIN
|
|
3
|
-
* an existing, already-human-approved Epic (via `sourcePromptId`); it must
|
|
4
|
-
* never mint a new one (no mintAuthority, see epicMint.cjs's ensureEpic).
|
|
5
|
-
* A write with no resolvable Epic fails outright with zero residual
|
|
6
|
-
* active-index.json entries — no orphaned Epic can ever be left behind by
|
|
7
|
-
* this path because none is ever created by it.
|
|
8
|
-
*
|
|
9
|
-
* A PRD write that joins a pre-existing Epic and then fails (e.g. an invalid
|
|
10
|
-
* slug) must leave that Epic's history untouched.
|
|
11
|
-
*
|
|
12
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
'use strict';
|
|
16
|
-
|
|
17
|
-
import { test, expect, afterEach } from 'vitest';
|
|
18
|
-
const fs = require('node:fs');
|
|
19
|
-
const os = require('node:os');
|
|
20
|
-
const path = require('node:path');
|
|
21
|
-
const { remote } = require('../scheduler.cjs');
|
|
22
|
-
const { ensureEpic, readActiveIndex, MINT_AUTHORITY_NEW_EPIC_UI } = require('../lib/epicMint.cjs');
|
|
23
|
-
const config = require('../config.cjs');
|
|
24
|
-
|
|
25
|
-
const tmpDirs = [];
|
|
26
|
-
|
|
27
|
-
function makeFixtureCwd() {
|
|
28
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-writeprd-epic-rollback-'));
|
|
29
|
-
tmpDirs.push(cwd);
|
|
30
|
-
return cwd;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
while (tmpDirs.length) {
|
|
35
|
-
const dir = tmpDirs.pop();
|
|
36
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// A slug that escapes safeSlugPathIn's containment check, tripping the
|
|
41
|
-
// "invalid slug" branch of writePrd (scheduler.cjs's writePrd). Only the
|
|
42
|
-
// second test below (joining a pre-existing Epic) actually reaches this
|
|
43
|
-
// check — the first test fails earlier, at Epic resolution, before the slug
|
|
44
|
-
// is ever considered.
|
|
45
|
-
const ESCAPING_SLUG = '../escape-attempt';
|
|
46
|
-
|
|
47
|
-
test('writePrd: no sourcePromptId refuses to write (no Epic to join) and mints nothing', async () => {
|
|
48
|
-
const cwd = makeFixtureCwd();
|
|
49
|
-
const body = '---\ntitle: No epic given\n---\n\nbody text\n';
|
|
50
|
-
|
|
51
|
-
const result = await remote.writePrd(ESCAPING_SLUG, body, cwd);
|
|
52
|
-
|
|
53
|
-
expect(result.ok).toBe(false);
|
|
54
|
-
expect(result.error).toMatch(/no existing Epic to join/);
|
|
55
|
-
|
|
56
|
-
const index = readActiveIndex(cwd);
|
|
57
|
-
expect(Object.keys(index.sessions)).toHaveLength(0);
|
|
58
|
-
expect(Object.keys(index.events)).toHaveLength(0);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('writePrd: invalid-slug failure when joining a pre-existing Epic leaves that Epic untouched', async () => {
|
|
62
|
-
const cwd = makeFixtureCwd();
|
|
63
|
-
|
|
64
|
-
const preexisting = await ensureEpic(cwd, { goalText: 'pre-existing epic goal', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
|
|
65
|
-
expect(preexisting.created).toBe(true);
|
|
66
|
-
|
|
67
|
-
const beforeIndex = readActiveIndex(cwd);
|
|
68
|
-
expect(Object.keys(beforeIndex.sessions)).toEqual([preexisting.epicId]);
|
|
69
|
-
|
|
70
|
-
const body = `---\ntitle: Joins the existing epic\nsourcePromptId: ${preexisting.epicId}\n---\n\nbody text\n`;
|
|
71
|
-
const result = await remote.writePrd(ESCAPING_SLUG, body, cwd);
|
|
72
|
-
|
|
73
|
-
expect(result.ok).toBe(false);
|
|
74
|
-
expect(result.error).toBe('invalid slug');
|
|
75
|
-
|
|
76
|
-
const afterIndex = readActiveIndex(cwd);
|
|
77
|
-
expect(Object.keys(afterIndex.sessions)).toEqual([preexisting.epicId]);
|
|
78
|
-
expect(afterIndex.sessions[preexisting.epicId]).toEqual(beforeIndex.sessions[preexisting.epicId]);
|
|
79
|
-
expect(afterIndex.events[preexisting.epicId]).toEqual(beforeIndex.events[preexisting.epicId]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Provenance (PRD 905): writePrd's ensureEpic call passes
|
|
83
|
-
// source: { producer: 'scheduler-dispatch', prdSlug }, but this call is
|
|
84
|
-
// join-only (no mintAuthority, explicit epicId) — ensureEpic's
|
|
85
|
-
// explicitEpicId branch returns before ever reading `source`, so a
|
|
86
|
-
// successful join must NOT write/alter the joined Epic's `source` field.
|
|
87
|
-
test('writePrd: successful join does not write a source field onto the pre-existing Epic (join-only no-op)', async () => {
|
|
88
|
-
// Must live under the home dir — config.cjs's writeTextAtomic validates
|
|
89
|
-
// paths against allowedRoots=home dir, unlike the failure-path tests above
|
|
90
|
-
// (which never reach the write) os.tmpdir() is not sufficient here.
|
|
91
|
-
const cwd = fs.mkdtempSync(path.join(os.homedir(), '.sm-writeprd-epic-source-'));
|
|
92
|
-
tmpDirs.push(cwd);
|
|
93
|
-
config.addAllowedRoot(cwd);
|
|
94
|
-
|
|
95
|
-
const preexisting = await ensureEpic(cwd, { goalText: 'pre-existing epic goal for join', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
|
|
96
|
-
expect(preexisting.created).toBe(true);
|
|
97
|
-
|
|
98
|
-
const body = `---\ntitle: Joins the existing epic\nsourcePromptId: ${preexisting.epicId}\n---\n\nbody text\n`;
|
|
99
|
-
const result = await remote.writePrd('valid-slug', body, cwd);
|
|
100
|
-
|
|
101
|
-
expect(result.ok).toBe(true);
|
|
102
|
-
|
|
103
|
-
const index = readActiveIndex(cwd);
|
|
104
|
-
expect('source' in index.sessions[preexisting.epicId]).toBe(false);
|
|
105
|
-
});
|