claude-code-session-manager 0.87.0 → 0.88.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 +26 -53
- package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-BZ6IY2g1.js} +1 -1
- package/dist/assets/{DataModel--mISIJ6h.js → DataModel-dAoGAODX.js} +1 -1
- package/dist/assets/{History-C2ahUXTg.js → History-CF157HyX.js} +1 -1
- package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CCPPipBk.js} +1 -1
- package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-Dlmz2m06.js} +1 -1
- package/dist/assets/{Library-Dc8Qst1R.js → Library-BaXcboWX.js} +1 -1
- package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CoUCzG5S.js} +1 -1
- package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-DTg3JlOT.js} +1 -1
- package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-DdjY1sA3.js} +1 -1
- package/dist/assets/{Memory-CW62MXlh.js → Memory-BJ3Jlw0C.js} +1 -1
- package/dist/assets/{Panel-Bw1FhRuF.js → Panel-ygRSgc41.js} +1 -1
- package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-m0KAnccF.js} +1 -1
- package/dist/assets/{Plugins-BnKx9flD.js → Plugins-CJRl6B3Q.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
- package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BpyPcUIk.js} +1 -1
- package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-D2ppxSfy.js} +7 -7
- package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
- package/dist/assets/{Settings-DluB-vN1.js → Settings-BHSGGFEc.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
- package/dist/assets/{Skills-gNdo_HNK.js → Skills-Dl3q75QV.js} +1 -1
- package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
- package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-BSutflCy.js} +1 -1
- package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody-CXih4xrc.js} +1 -1
- package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-2qtUHhA9.js} +1 -1
- package/dist/assets/{index-fc_JjdxL.js → index-B3emy2oI.js} +356 -356
- package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-CdzxaqwM.js} +1 -1
- package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
- package/dist/index.html +2 -2
- package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
- package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
- package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
- package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
- package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
- package/package.json +10 -13
- package/scripts/README.md +59 -0
- package/scripts/audit-ops-hygiene.cjs +350 -0
- package/scripts/hooks/guard-destructive-git.cjs +10 -34
- package/scripts/hooks/guard-inline-implementation.cjs +14 -8
- package/scripts/hooks/guard-prd-writes.cjs +7 -36
- package/scripts/hooks/guard-self-schedule.cjs +175 -0
- package/scripts/ops-sweep.cjs +355 -0
- package/scripts/scheduler-mcp-server.cjs +28 -71
- package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
- package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
- package/src/main/__tests__/chat-queue.test.cjs +2 -2
- package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
- package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
- package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
- package/src/main/__tests__/dod-report.test.cjs +2 -2
- package/src/main/__tests__/dod-reverify.test.cjs +2 -2
- package/src/main/__tests__/epicMint.test.cjs +2 -2
- package/src/main/__tests__/exchanges.test.cjs +2 -2
- package/src/main/__tests__/extractJson.test.cjs +2 -2
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
- package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +15 -1
- package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
- package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
- package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
- package/src/main/__tests__/historyRollup.test.cjs +2 -2
- package/src/main/__tests__/kg-augment.test.cjs +2 -2
- package/src/main/__tests__/mcpStatus.test.cjs +2 -2
- package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
- package/src/main/__tests__/memoryStale.test.cjs +1 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +25 -1
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +33 -3
- package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
- package/src/main/__tests__/prdLocationsArchived.test.cjs +20 -13
- package/src/main/__tests__/proc-role-env.test.cjs +125 -0
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
- package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
- package/src/main/__tests__/projectPages.test.cjs +63 -149
- package/src/main/__tests__/queue-health-verdict.test.cjs +9 -9
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
- package/src/main/__tests__/queueHistory.test.cjs +2 -2
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
- package/src/main/__tests__/runVerify.test.cjs +5 -5
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +2 -2
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +78 -97
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +46 -0
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
- package/src/main/__tests__/scheduler-looks-done.test.cjs +26 -7
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +23 -4
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +81 -1
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +4 -4
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +37 -11
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
- package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
- package/src/main/bilkoHost.cjs +16 -7
- package/src/main/build-info.json +8 -0
- package/src/main/chatRunner.cjs +4 -2
- package/src/main/config.cjs +2 -3
- package/src/main/docEdit.cjs +4 -2
- package/src/main/health.cjs +267 -51
- package/src/main/heapSnapshot.cjs +2 -2
- package/src/main/historyAggregator.cjs +1 -1
- package/src/main/index.cjs +61 -10
- package/src/main/ipcSchemas.cjs +6 -17
- package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
- package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
- package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +1 -1
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
- package/src/main/lib/__tests__/gateFixtures.json +20 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +8 -1
- package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
- package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
- package/src/main/lib/__tests__/procName.test.cjs +92 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
- package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +61 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +63 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +73 -0
- package/src/main/lib/activeIndexRebuild.cjs +1 -3
- package/src/main/lib/activeSessions.cjs +20 -88
- package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
- package/src/main/lib/agentModelResolve.cjs +33 -1
- package/src/main/lib/agentPersonaSchema.cjs +2 -2
- package/src/main/lib/auditLog.cjs +30 -5
- package/src/main/lib/buildIdentity.cjs +113 -0
- package/src/main/lib/classifyPromptTicket.cjs +3 -2
- package/src/main/lib/claudeBin.cjs +37 -2
- package/src/main/lib/cleanEnv.cjs +27 -1
- package/src/main/lib/credentials.cjs +4 -2
- package/src/main/lib/cwdClassify.cjs +185 -0
- package/src/main/lib/definitionOfDone.cjs +296 -52
- package/src/main/lib/dispatchLoop.cjs +40 -0
- package/src/main/lib/effectiveModelInfo.cjs +9 -10
- package/src/main/lib/ephemeralCwd.cjs +7 -28
- package/src/main/lib/gitWorktree.cjs +95 -8
- package/src/main/lib/guardShims.cjs +3 -3
- package/src/main/lib/historyRollup.cjs +6 -7
- package/src/main/lib/instanceLock.cjs +32 -5
- package/src/main/lib/jobSupervisorRecord.cjs +147 -0
- package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
- package/src/main/lib/localAdminHttp.cjs +9 -27
- package/src/main/lib/mcpToolCatalog.cjs +29 -77
- package/src/main/lib/opsOwnership.cjs +27 -19
- package/src/main/lib/prdAuthoringSeed.cjs +36 -0
- package/src/main/lib/prdLocations.cjs +48 -1
- package/src/main/lib/procIdentity.cjs +126 -0
- package/src/main/lib/procName.cjs +98 -0
- package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
- package/src/main/lib/queueHistory.cjs +8 -7
- package/src/main/lib/queueStore.cjs +57 -37
- package/src/main/lib/quietMachineLease.cjs +19 -2
- package/src/main/lib/reservationExpiry.cjs +30 -0
- package/src/main/lib/runClaudeP.cjs +4 -2
- package/src/main/lib/runLogRetention.cjs +3 -2
- package/src/main/lib/scheduleJobSchema.cjs +2 -2
- package/src/main/lib/scheduleJobTransitions.cjs +29 -2
- package/src/main/lib/schedulerBatch.cjs +27 -2
- package/src/main/lib/schedulerPaths.cjs +175 -0
- package/src/main/lib/schedulerRuntimeState.cjs +59 -0
- package/src/main/lib/sessionSlots.cjs +47 -9
- package/src/main/lib/smProcNames.cjs +51 -0
- package/src/main/lib/upgradeDrain.cjs +188 -0
- package/src/main/lib/usageCircuit.cjs +53 -6
- package/src/main/lib/watchdogHelpers.cjs +70 -37
- package/src/main/lib/withTimeout.cjs +33 -0
- package/src/main/mcpStatus.cjs +4 -2
- package/src/main/pluginInstall.cjs +6 -2
- package/src/main/projectPages.cjs +41 -145
- package/src/main/pty.cjs +8 -1
- package/src/main/queueOps.cjs +10 -10
- package/src/main/runVerify.cjs +69 -3
- package/src/main/scheduler.cjs +1598 -381
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +7 -6
- package/src/main/seedStatus.cjs +1 -1
- package/src/main/supervisor.cjs +5 -3
- package/src/main/usage.cjs +126 -62
- package/src/preload/api.d.ts +18 -64
- package/src/preload/index.cjs +2 -4
- package/src/seed/agents/project-home-builder.md +31 -48
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +0 -13
- package/src/main/lib/projectPageSummarySchema.cjs +0 -181
- package/src/main/teams.cjs +0 -95
- package/src/main/templates/project-pages-catalog.json +0 -741
- package/src/main/templates/project-pages-default-home.html +0 -123
- package/src/main/templates/project-pages-pipeline.md +0 -417
- package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
- package/src/seed/prompts/code-review/correctness-only.md +0 -8
- package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
- package/src/seed/prompts/code-review/hallucination-check.md +0 -8
- package/src/seed/prompts/code-review/public-api-compat.md +0 -8
- package/src/seed/prompts/code-review/readability-naming.md +0 -8
- package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
- package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
- package/src/seed/prompts/documentation/adr-from-change.md +0 -8
- package/src/seed/prompts/documentation/module-readme.md +0 -8
- package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
- package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
- package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
- package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
- package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
- package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
- package/src/seed/prompts/performance/complexity-audit.md +0 -8
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
- package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
- package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
- package/src/seed/prompts/qa/api-contract-tests.md +0 -8
- package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
- package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
- package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
- package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
- package/src/seed/prompts/qa/tdd-red-first.md +0 -8
- package/src/seed/prompts/qa/visual-regression-review.md +0 -8
- package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
- package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
- package/src/seed/prompts/security/authz-audit.md +0 -8
- package/src/seed/prompts/security/crypto-correctness.md +0 -8
- package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
- package/src/seed/prompts/security/dependency-audit.md +0 -8
- package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
- package/src/seed/prompts/security/secret-credential-scan.md +0 -8
- package/web/README.md +0 -41
- package/web/project-pages/logic/dist/logic.cjs +0 -4709
- package/web/project-pages/render.cjs +0 -70
- package/web/project-pages/renderer/dist/renderer.cjs +0 -18900
- package/web/project-pages/validate-summary.cjs +0 -62
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-heartbeat-payload.test.cjs — the heartbeat line carries build
|
|
3
|
+
* identity, a subsystem throw yields a `degraded: true` line, and both
|
|
4
|
+
* watchdogHelpers.heartbeatFresh and health.readFreshHeartbeat treat a
|
|
5
|
+
* degraded line as NOT fresh.
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-heartbeat-payload.test.cjs
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, vi } from 'vitest';
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const schedulerPaths = require('../lib/schedulerPaths.cjs');
|
|
14
|
+
const scheduler = require('../scheduler.cjs');
|
|
15
|
+
const { heartbeatFresh } = require('../lib/watchdogHelpers.cjs');
|
|
16
|
+
const { readFreshHeartbeat } = require('../health.cjs');
|
|
17
|
+
|
|
18
|
+
const HB = () => schedulerPaths.heartbeatPath();
|
|
19
|
+
const lastLine = () => JSON.parse(fs.readFileSync(HB(), 'utf8').trim().split('\n').pop());
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
fs.mkdirSync(require('node:path').dirname(HB()), { recursive: true });
|
|
23
|
+
fs.rmSync(HB(), { force: true });
|
|
24
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('healthy tick writes build {version, codeSha, builtAt} and is fresh', () => {
|
|
28
|
+
const entry = scheduler.heartbeatTick({ readQueueSync: () => ({ jobs: [], unreadable: true, paused: null }) });
|
|
29
|
+
expect(entry.degraded).toBeUndefined();
|
|
30
|
+
const line = lastLine();
|
|
31
|
+
expect(line.build).toEqual(expect.objectContaining({ version: expect.anything() }));
|
|
32
|
+
expect(Object.keys(line.build).sort()).toEqual(['builtAt', 'codeSha', 'version']);
|
|
33
|
+
expect(line.counts).toBeDefined();
|
|
34
|
+
expect(heartbeatFresh(HB())).toBe(true);
|
|
35
|
+
expect(readFreshHeartbeat(HB())).not.toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('a throwing queue read writes a degraded line with the error, never a fresh one', () => {
|
|
39
|
+
scheduler.heartbeatTick({ readQueueSync: () => { throw new Error('boom-queue'); } });
|
|
40
|
+
const line = lastLine();
|
|
41
|
+
expect(line.degraded).toBe(true);
|
|
42
|
+
expect(line.errors[0]).toEqual({ subsystem: 'queue-read-starvation-watchdog', error: 'boom-queue' });
|
|
43
|
+
expect(line.build).toBeDefined();
|
|
44
|
+
expect(line.utilization).toBeUndefined();
|
|
45
|
+
expect(heartbeatFresh(HB())).toBe(false);
|
|
46
|
+
expect(readFreshHeartbeat(HB())).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a throw in the stall detector degrades the line too', () => {
|
|
50
|
+
scheduler.heartbeatTick({ readQueueSync: () => ({ jobs: null, unreadable: true, paused: null }) });
|
|
51
|
+
const line = lastLine();
|
|
52
|
+
expect(line.degraded).toBe(true);
|
|
53
|
+
expect(line.errors.some((e) => e.subsystem === 'stall-detector' || e.subsystem === 'heartbeat-write')).toBe(true);
|
|
54
|
+
expect(heartbeatFresh(HB())).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('watchdogHelpers treats a hand-written degraded line as stale', () => {
|
|
58
|
+
fs.writeFileSync(HB(), JSON.stringify({ ts: Date.now(), degraded: true }) + '\n');
|
|
59
|
+
expect(heartbeatFresh(HB())).toBe(false);
|
|
60
|
+
fs.writeFileSync(HB(), JSON.stringify({ ts: Date.now() }) + '\n');
|
|
61
|
+
expect(heartbeatFresh(HB())).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('healthy tick carries dispatch {..., pendingDispatchable excludes blocked chains}', () => {
|
|
65
|
+
const jobs = [
|
|
66
|
+
{ slug: '1-a', cwd: '/p', status: 'failed' },
|
|
67
|
+
{ slug: '2-b', cwd: '/p', status: 'pending', dependsOn: ['1-a'] },
|
|
68
|
+
{ slug: '3-c', cwd: '/p', status: 'pending' },
|
|
69
|
+
];
|
|
70
|
+
const entry = scheduler.heartbeatTick({
|
|
71
|
+
readQueueSync: () => ({ jobs, unreadable: true, paused: null, lastRunAt: '2026-01-01T00:00:00.000Z' }),
|
|
72
|
+
});
|
|
73
|
+
expect(entry.dispatch).toEqual(expect.objectContaining({
|
|
74
|
+
lastRunAt: '2026-01-01T00:00:00.000Z', pendingDispatchable: 1, runningCount: 0, paused: false,
|
|
75
|
+
}));
|
|
76
|
+
expect(Object.keys(entry.dispatch).sort()).toEqual(
|
|
77
|
+
['drain', 'lastDispatchAttemptAt', 'lastRunAt', 'lastTickReason', 'paused', 'pendingDispatchable', 'runningCount'],
|
|
78
|
+
);
|
|
79
|
+
expect(entry.dispatch.drain).toBeNull();
|
|
80
|
+
});
|
|
@@ -18,6 +18,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
18
18
|
const fs = require('node:fs');
|
|
19
19
|
const os = require('node:os');
|
|
20
20
|
const path = require('node:path');
|
|
21
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
21
22
|
const { execFileSync } = require('node:child_process');
|
|
22
23
|
|
|
23
24
|
let tmpHome;
|
|
@@ -52,6 +53,12 @@ function registerActiveProject(cwd) {
|
|
|
52
53
|
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
// Every PRD fixture below lives Epic-scoped, not in the legacy flat prds/
|
|
57
|
+
// dir: the flat dir is retired and swept into prds-archived/ by
|
|
58
|
+
// consolidateFlatPrds the moment a job is no longer pending/running, which
|
|
59
|
+
// would strand a just-completed/failed row the instant reconcile()'s
|
|
60
|
+
// auto-archive-drop path (see scheduler-reconcile-history-backfill.test.cjs)
|
|
61
|
+
// treats the now-missing PRD as "archived on purpose" and drops it.
|
|
55
62
|
function writeProjectQueue(cwd, jobs) {
|
|
56
63
|
const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
|
|
57
64
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
@@ -68,16 +75,13 @@ function writeProjectQueue(cwd, jobs) {
|
|
|
68
75
|
// which is not the scenario under test here — this must land as a genuine
|
|
69
76
|
// non-zero exit.
|
|
70
77
|
function writeKilledClaudeStub() {
|
|
71
|
-
|
|
72
|
-
const body = `
|
|
78
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
73
79
|
const fs = require('fs');
|
|
74
80
|
const path = require('path');
|
|
75
81
|
fs.writeFileSync(path.join(process.cwd(), 'README.md'), 'hello\\nedited by job\\n', 'utf8');
|
|
76
82
|
fs.writeFileSync(path.join(process.cwd(), 'job-output.txt'), 'work the job produced before dying\\n', 'utf8');
|
|
77
83
|
process.exit(137);
|
|
78
|
-
|
|
79
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
80
|
-
return stubPath;
|
|
84
|
+
` });
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
beforeAll(() => {
|
|
@@ -117,7 +121,7 @@ test('an in-place job killed (exit 137) mid-run salvages a delta-scoped patch th
|
|
|
117
121
|
fs.appendFileSync(path.join(projectCwd, 'human-wip.txt'), 'more human edits\n', 'utf8');
|
|
118
122
|
|
|
119
123
|
const slug = `1098-test-inplace-killed-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
120
|
-
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
124
|
+
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
121
125
|
fs.mkdirSync(prdsDir, { recursive: true });
|
|
122
126
|
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Do a thing that gets killed mid-run.', 'utf8');
|
|
123
127
|
|
|
@@ -192,13 +196,10 @@ test('an in-place job killed (exit 137) mid-run salvages a delta-scoped patch th
|
|
|
192
196
|
// — a job whose only working-tree dirt is pre-existing human/sibling WIP
|
|
193
197
|
// present before the run even started.
|
|
194
198
|
function writeNoopClaudeStub() {
|
|
195
|
-
|
|
196
|
-
const body = `
|
|
199
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
197
200
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'nothing to do', is_error: false }) + '\\n');
|
|
198
201
|
process.exit(0);
|
|
199
|
-
|
|
200
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
201
|
-
return stubPath;
|
|
202
|
+
` });
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
// Stub `claude` binary that blocks until a `go` marker file appears in its
|
|
@@ -206,11 +207,19 @@ function writeNoopClaudeStub() {
|
|
|
206
207
|
// window to read the queue row's intermediate dispatchPhase before the run
|
|
207
208
|
// finalizes and deletes it.
|
|
208
209
|
function writeGatedClaudeStub() {
|
|
209
|
-
|
|
210
|
-
const body = `
|
|
210
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
211
211
|
const fs = require('fs');
|
|
212
212
|
const path = require('path');
|
|
213
213
|
const { execFileSync } = require('child_process');
|
|
214
|
+
// spawnJob's dispatch path probes 'claude --version' (claudeBin.cjs)
|
|
215
|
+
// BEFORE the real dispatch — a real claude binary answers that
|
|
216
|
+
// near-instantly regardless of any in-flight work, so the stub must
|
|
217
|
+
// too, or the probe's own 8s exec timeout eats almost this whole test's
|
|
218
|
+
// poll window before the row ever advances past 'running-stamped'.
|
|
219
|
+
if (process.argv.includes('--version')) {
|
|
220
|
+
process.stdout.write('1.0.0 (stub)\\n');
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
214
223
|
const goFile = path.join(process.cwd(), 'go.marker');
|
|
215
224
|
const deadline = Date.now() + 10_000;
|
|
216
225
|
while (!fs.existsSync(goFile) && Date.now() < deadline) {
|
|
@@ -218,9 +227,7 @@ function writeGatedClaudeStub() {
|
|
|
218
227
|
}
|
|
219
228
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok', is_error: false }) + '\\n');
|
|
220
229
|
process.exit(0);
|
|
221
|
-
|
|
222
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
223
|
-
return stubPath;
|
|
230
|
+
` });
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
test('dispatchPhase breadcrumb advances to "spawned" mid-run and is gone after finalize', async () => {
|
|
@@ -229,7 +236,7 @@ test('dispatchPhase breadcrumb advances to "spawned" mid-run and is gone after f
|
|
|
229
236
|
registerActiveProject(projectCwd);
|
|
230
237
|
|
|
231
238
|
const slug = `1163-test-dispatchphase-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
232
|
-
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
239
|
+
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
233
240
|
fs.mkdirSync(prdsDir, { recursive: true });
|
|
234
241
|
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Do a thing, gated on a marker file.', 'utf8');
|
|
235
242
|
|
|
@@ -285,7 +292,7 @@ test('a job whose tree is dirty only from pre-existing baseline WIP (human/sibli
|
|
|
285
292
|
fs.writeFileSync(path.join(projectCwd, 'human-wip.txt'), 'human work in progress\n', 'utf8');
|
|
286
293
|
|
|
287
294
|
const slug = `1098-test-inplace-noop-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
288
|
-
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
295
|
+
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
289
296
|
fs.mkdirSync(prdsDir, { recursive: true });
|
|
290
297
|
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Do nothing.', 'utf8');
|
|
291
298
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* scheduler-investigation-prompt.test.cjs — unit tests for buildInvestigationPrompt.
|
|
3
3
|
*
|
|
4
|
-
* Run: timeout
|
|
4
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/scheduler-investigation-prompt.test.cjs
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import { test } from 'vitest';
|
|
10
10
|
const assert = require('node:assert/strict');
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const os = require('node:os');
|
|
@@ -93,7 +93,7 @@ test('prompt instructs that cwd must be the git repo root where the fix will lan
|
|
|
93
93
|
assert.match(prompt, /needs_review/);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
test('prompt frontmatter block still declares exactly the
|
|
96
|
+
test('prompt frontmatter block still declares exactly the five expected keys', () => {
|
|
97
97
|
const prompt = buildInvestigationPrompt(makeArgs());
|
|
98
98
|
const fenceMatch = prompt.match(/```\s*\n\s*---\n([\s\S]*?)\n\s*---\n\s*```/);
|
|
99
99
|
assert.ok(fenceMatch, 'expected a fenced frontmatter block in the prompt');
|
|
@@ -101,7 +101,7 @@ test('prompt frontmatter block still declares exactly the four expected keys', (
|
|
|
101
101
|
.split('\n')
|
|
102
102
|
.filter((line) => line.trim().length > 0)
|
|
103
103
|
.map((line) => line.split(':')[0].trim());
|
|
104
|
-
assert.deepEqual(keys, ['title', 'cwd', 'parallelGroup', 'estimateMinutes']);
|
|
104
|
+
assert.deepEqual(keys, ['title', 'cwd', 'parallelGroup', 'estimateMinutes', 'isFixPlan']);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
// ─── isGitRepoSync (scheduler.cjs) ─────────────────────────────────────────────
|
|
@@ -20,6 +20,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
20
20
|
const fs = require('node:fs');
|
|
21
21
|
const os = require('node:os');
|
|
22
22
|
const path = require('node:path');
|
|
23
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
23
24
|
|
|
24
25
|
let tmpHome;
|
|
25
26
|
let originalHome;
|
|
@@ -50,8 +51,7 @@ afterEach(() => {
|
|
|
50
51
|
const THINKING_400 = 'API Error: 400 {"detail":{"error":"{\\"message\\":\\"\\\\\\"thinking.type.enabled\\\\\\" is not supported for this model. Use \\\\\\"thinking.type.adaptive\\\\\\" and \\\\\\"output_config.effort\\\\\\" to control thinking behavior.\\"}"}}';
|
|
51
52
|
|
|
52
53
|
function writeStub({ mode }) {
|
|
53
|
-
|
|
54
|
-
const body = `
|
|
54
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
55
55
|
const fs = require('fs');
|
|
56
56
|
const path = require('path');
|
|
57
57
|
fs.writeFileSync(path.join(process.cwd(), 'env.marker'), JSON.stringify({
|
|
@@ -67,9 +67,7 @@ function writeStub({ mode }) {
|
|
|
67
67
|
}
|
|
68
68
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'error', is_error: true, num_turns: 9, usage: { input_tokens: 100, output_tokens: 250 }, result: 'Error: tests failed' }) + '\\n');
|
|
69
69
|
process.exit(1);
|
|
70
|
-
|
|
71
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
72
|
-
return stubPath;
|
|
70
|
+
` });
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
function mkProject() {
|
|
@@ -34,7 +34,7 @@ const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'looks-done-test-'));
|
|
|
34
34
|
process.env.HOME = tmpHome;
|
|
35
35
|
|
|
36
36
|
const { reverifyNeedsReview, computeLooksDone, applyNeedsReviewAutoResolve, findSatisfyingCommitOnMain } = require('../scheduler.cjs');
|
|
37
|
-
const {
|
|
37
|
+
const { resolveEpicPrdWriteDir } = require('../lib/prdLocations.cjs');
|
|
38
38
|
const { bustCwdCache } = require('../lib/queueStore.cjs');
|
|
39
39
|
|
|
40
40
|
function git(args, cwd) {
|
|
@@ -92,13 +92,32 @@ function writeRunLog(runId, slug, lines) {
|
|
|
92
92
|
fs.writeFileSync(path.join(runDir, `${slug}.log`), lines.join('\n') + '\n');
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Epic-scoped, not the legacy flat prds/ dir: the flat dir is retired and
|
|
96
|
+
// swept into prds-archived/ by consolidateFlatPrds the moment a job is no
|
|
97
|
+
// longer live (pending/running/needs_review/investigating), which would
|
|
98
|
+
// strand a row that starts 'failed' before reverifyNeedsReview ever gets a
|
|
99
|
+
// chance to decide anything about it.
|
|
95
100
|
function writePrd(cwd, slug, body) {
|
|
96
|
-
const dir =
|
|
101
|
+
const dir = resolveEpicPrdWriteDir(cwd, 'test-fixture-epic');
|
|
97
102
|
fs.mkdirSync(dir, { recursive: true });
|
|
98
103
|
const text = `---\ntitle: Test PRD\ncwd: ${cwd}\nestimateMinutes: 30\n---\n${body}\n`;
|
|
99
104
|
fs.writeFileSync(path.join(dir, `${slug}.md`), text);
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
// A job reverifyNeedsReview() heals to 'completed' has its PRD archived
|
|
108
|
+
// immediately (archiveCompletedPrd) and its row backfilled to history.jsonl
|
|
109
|
+
// + dropped from queue.json on the very same reconcile pass — see
|
|
110
|
+
// scheduler-reconcile-history-backfill.test.cjs. Look in both places.
|
|
111
|
+
function readTerminalRow(queuePath, projectCwd, slug) {
|
|
112
|
+
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
113
|
+
const row = jobs.find((j) => j.slug === slug);
|
|
114
|
+
if (row) return row;
|
|
115
|
+
const historyPath = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state', 'history.jsonl');
|
|
116
|
+
if (!fs.existsSync(historyPath)) return undefined;
|
|
117
|
+
const lines = fs.readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
118
|
+
return lines.map((l) => JSON.parse(l)).reverse().find((j) => j.slug === slug);
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
async function wait(ms) {
|
|
103
122
|
await new Promise((r) => setTimeout(r, ms));
|
|
104
123
|
}
|
|
@@ -220,11 +239,11 @@ test('needs_review with an in-window commit heals exactly as today (regression
|
|
|
220
239
|
|
|
221
240
|
await reverifyNeedsReview();
|
|
222
241
|
|
|
223
|
-
const
|
|
224
|
-
assert.
|
|
225
|
-
assert.equal(
|
|
226
|
-
assert.equal(
|
|
227
|
-
assert.equal(
|
|
242
|
+
const row = readTerminalRow(queuePath, projectCwd, '20-clean-feature');
|
|
243
|
+
assert.ok(row, 'healed job must be found in queue.json or (once archived+backfilled) history.jsonl');
|
|
244
|
+
assert.equal(row.status, 'completed', 'a clean re-verify on a RESCANNABLE verdict must still heal');
|
|
245
|
+
assert.equal(row.verifierVerdict, undefined);
|
|
246
|
+
assert.equal(row.error, null);
|
|
228
247
|
});
|
|
229
248
|
|
|
230
249
|
test('computeLooksDone: no declared paths on the PRD → null, never fabricates evidence', async () => {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-manual-pause.test.cjs — user-initiated pause (schedule:pause /
|
|
3
|
+
* POST /admin/scheduler/pause). A 'manual' pause stops NEW dispatch, never
|
|
4
|
+
* kills running jobs, survives restart, and is cleared only by an explicit
|
|
5
|
+
* Resume / Run now — never by the rate-limit reset timer or network recovery.
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-manual-pause.test.cjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
import { test, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
|
|
17
|
+
let tmpHome;
|
|
18
|
+
let originalHome;
|
|
19
|
+
let scheduler;
|
|
20
|
+
let queueStore;
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
originalHome = process.env.HOME;
|
|
24
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-manual-pause-'));
|
|
25
|
+
process.env.HOME = tmpHome;
|
|
26
|
+
process.env.SM_JOB_WORKTREE_DISABLE = '1';
|
|
27
|
+
scheduler = require('../scheduler.cjs');
|
|
28
|
+
queueStore = require('../lib/queueStore.cjs');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
process.env.HOME = originalHome;
|
|
33
|
+
delete process.env.SM_JOB_WORKTREE_DISABLE;
|
|
34
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
await scheduler.clearPause('manual');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('pause engages reason "manual" with no resumeAt, even inside the manual-override cooldown', async () => {
|
|
42
|
+
await scheduler.clearPause('manual'); // starts the 5-minute cooldown
|
|
43
|
+
await scheduler.setPaused('manual', null);
|
|
44
|
+
const state = await queueStore.readMerged();
|
|
45
|
+
expect(state.paused).toBeTruthy();
|
|
46
|
+
expect(state.paused.reason).toBe('manual');
|
|
47
|
+
expect(state.paused.resumeAt).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('a manual pause never arms a resume time, even if a resumeAt is supplied', async () => {
|
|
51
|
+
await scheduler.setPaused('manual', new Date(Date.now() + 3_600_000).toISOString());
|
|
52
|
+
const state = await queueStore.readMerged();
|
|
53
|
+
expect(state.paused.resumeAt).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('pause blocks tickQueue dispatch', async () => {
|
|
57
|
+
await scheduler.setPaused('manual', null);
|
|
58
|
+
const result = await scheduler.tickQueue();
|
|
59
|
+
expect(result.fired).toBe(false);
|
|
60
|
+
expect(result.reason).toBe('paused');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('an auto rate_limit / network / auth pause cannot displace a manual pause', async () => {
|
|
64
|
+
await scheduler.setPaused('manual', null);
|
|
65
|
+
await scheduler.setPaused('rate_limit', new Date(Date.now() + 3_600_000).toISOString());
|
|
66
|
+
await scheduler.setPaused('network', null);
|
|
67
|
+
await scheduler.setPaused('auth', null);
|
|
68
|
+
const state = await queueStore.readMerged();
|
|
69
|
+
expect(state.paused.reason).toBe('manual');
|
|
70
|
+
expect(state.paused.resumeAt).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('poll recovery and the resume-time computation never clear/arm a manual pause', () => {
|
|
74
|
+
expect(scheduler.pollRecoveryClearSource('manual', true)).toBeNull();
|
|
75
|
+
expect(scheduler.pollRecoveryClearSource('manual', false)).toBeNull();
|
|
76
|
+
expect(scheduler.computeEffectiveResumeAt('manual', null)).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a running job survives a pause untouched', async () => {
|
|
80
|
+
const projectCwd = fs.mkdtempSync(path.join(tmpHome, 'pause-survivor-'));
|
|
81
|
+
const slugDir = path.join(tmpHome, '.claude', 'projects', 'pause-survivor-slug');
|
|
82
|
+
fs.mkdirSync(slugDir, { recursive: true });
|
|
83
|
+
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd: projectCwd }) + '\n');
|
|
84
|
+
queueStore.bustCwdCache();
|
|
85
|
+
const stateDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state');
|
|
86
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
87
|
+
fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({
|
|
88
|
+
jobs: [{ slug: 'pause-survivor', status: 'running', cwd: projectCwd, pid: process.pid, runId: 'r1', startedAt: new Date().toISOString() }],
|
|
89
|
+
}));
|
|
90
|
+
await scheduler.setPaused('manual', null);
|
|
91
|
+
const state = await queueStore.readMerged();
|
|
92
|
+
const row = state.jobs.find((j) => j.slug === 'pause-survivor');
|
|
93
|
+
expect(row.status).toBe('running');
|
|
94
|
+
expect(row.runId).toBe('r1');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('a manual pause survives restart (re-read from disk; boot never clears it)', async () => {
|
|
98
|
+
await scheduler.setPaused('manual', null);
|
|
99
|
+
const onDisk = await queueStore.readMerged(); // what a fresh process reads at boot
|
|
100
|
+
expect(onDisk.paused.reason).toBe('manual');
|
|
101
|
+
// Boot only clears a pause whose resumeAt has elapsed / re-arms one with a
|
|
102
|
+
// resumeAt; a manual pause has none, so neither branch applies.
|
|
103
|
+
expect(onDisk.paused.resumeAt).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('resume clears a manual pause', async () => {
|
|
107
|
+
await scheduler.setPaused('manual', null);
|
|
108
|
+
await scheduler.clearPause('manual');
|
|
109
|
+
const state = await queueStore.readMerged();
|
|
110
|
+
expect(state.paused).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('remote.pause / remote.resume are the admin-route twins of the IPC handlers', async () => {
|
|
114
|
+
expect(await scheduler.remote.pause()).toEqual({ ok: true });
|
|
115
|
+
expect((await queueStore.readMerged()).paused.reason).toBe('manual');
|
|
116
|
+
expect(await scheduler.remote.resume()).toEqual({ ok: true });
|
|
117
|
+
expect((await queueStore.readMerged()).paused).toBeNull();
|
|
118
|
+
});
|
|
@@ -5,19 +5,42 @@
|
|
|
5
5
|
* scheduler process is otherwise invisible in the run record, which cost a
|
|
6
6
|
* full investigation to diagnose a false pass_no_commit.
|
|
7
7
|
*
|
|
8
|
+
* SCHEDULER_CODE_SHA now resolves through src/main/lib/buildIdentity.cjs
|
|
9
|
+
* (build-identity-stamp PRD): a production npx install ships no .git, so a
|
|
10
|
+
* runtime `git rev-parse` was structurally always null there — this file
|
|
11
|
+
* used to accept that null unconditionally, which made the dead stamp green
|
|
12
|
+
* forever. With src/main/build-info.json present (ensured below — the exact
|
|
13
|
+
* artifact scripts/write-build-info.cjs bakes at publish time), the sha must
|
|
14
|
+
* be real.
|
|
15
|
+
*
|
|
8
16
|
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-meta-code-sha.test.cjs
|
|
9
17
|
*/
|
|
10
18
|
|
|
11
19
|
'use strict';
|
|
12
20
|
|
|
13
|
-
import { test, expect } from 'vitest';
|
|
21
|
+
import { test, expect, afterAll } from 'vitest';
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
|
|
25
|
+
const BUILD_INFO_PATH = path.join(__dirname, '..', 'build-info.json');
|
|
26
|
+
const preexistingBuildInfo = fs.existsSync(BUILD_INFO_PATH) ? fs.readFileSync(BUILD_INFO_PATH, 'utf8') : null;
|
|
27
|
+
if (preexistingBuildInfo === null) {
|
|
28
|
+
// eslint-disable-next-line global-require
|
|
29
|
+
require('../../../scripts/write-build-info.cjs').writeBuildInfo();
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
const { SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA } = require('../scheduler.cjs');
|
|
15
33
|
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
if (preexistingBuildInfo === null) fs.rmSync(BUILD_INFO_PATH, { force: true });
|
|
36
|
+
else fs.writeFileSync(BUILD_INFO_PATH, preexistingBuildInfo);
|
|
37
|
+
});
|
|
38
|
+
|
|
16
39
|
test('SCHEDULER_BOOTED_AT is a valid ISO date string', () => {
|
|
17
40
|
expect(typeof SCHEDULER_BOOTED_AT).toBe('string');
|
|
18
41
|
expect(Number.isNaN(new Date(SCHEDULER_BOOTED_AT).getTime())).toBe(false);
|
|
19
42
|
});
|
|
20
43
|
|
|
21
|
-
test('SCHEDULER_CODE_SHA is
|
|
22
|
-
expect(SCHEDULER_CODE_SHA
|
|
44
|
+
test('SCHEDULER_CODE_SHA is a real short git hex SHA when build-info.json is present — never null', () => {
|
|
45
|
+
expect(SCHEDULER_CODE_SHA).toMatch(/^[0-9a-f]{7,40}$/);
|
|
23
46
|
});
|
|
@@ -103,6 +103,22 @@ function writeProjectQueue(cwd, jobs) {
|
|
|
103
103
|
return path.join(stateDir, 'queue.json');
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// This test's whole premise is a PRD source missing EVERYWHERE — so the row
|
|
107
|
+
// this produces is exactly what reconcile()'s auto-archive-drop path treats
|
|
108
|
+
// as "archived on purpose": backfilled to history.jsonl and dropped from
|
|
109
|
+
// queue.json in the very same reconcile pass that spawnJob's own
|
|
110
|
+
// broadcast({flush:true}) triggers on completion (see
|
|
111
|
+
// scheduler-reconcile-history-backfill.test.cjs). Look in both places.
|
|
112
|
+
function readTerminalRow(queuePath, projectCwd, slug) {
|
|
113
|
+
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
114
|
+
const row = jobs.find((j) => j.slug === slug);
|
|
115
|
+
if (row) return row;
|
|
116
|
+
const historyPath = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state', 'history.jsonl');
|
|
117
|
+
if (!fs.existsSync(historyPath)) return undefined;
|
|
118
|
+
const lines = fs.readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
119
|
+
return lines.map((l) => JSON.parse(l)).reverse().find((j) => j.slug === slug);
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
// This is the actual regression case: PRD 812's fix only covered executeJob's
|
|
107
123
|
// own return value. The bug lived one layer up, in spawnJob's post-run
|
|
108
124
|
// handler, which only short-circuited on skipped === 'prd-archived' and let
|
|
@@ -123,8 +139,7 @@ test("spawnJob's post-run handler retires a prd-missing skip as a plain completi
|
|
|
123
139
|
const job = { slug, cwd: projectCwd };
|
|
124
140
|
await spawnJob(job, 'run-spawnjob-missing', runDir, projectCwd);
|
|
125
141
|
|
|
126
|
-
const
|
|
127
|
-
const row = state.jobs.find((j) => j.slug === slug);
|
|
142
|
+
const row = readTerminalRow(queuePath, projectCwd, slug);
|
|
128
143
|
expect(row).toBeDefined();
|
|
129
144
|
// A 'prd-missing' skip means no executor ever ran — it must land on the
|
|
130
145
|
// distinct 'skipped' status, never 'completed' (the status a genuine
|
|
@@ -17,6 +17,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
17
17
|
const fs = require('node:fs');
|
|
18
18
|
const os = require('node:os');
|
|
19
19
|
const path = require('node:path');
|
|
20
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
20
21
|
|
|
21
22
|
let tmpHome;
|
|
22
23
|
let originalHome;
|
|
@@ -66,16 +67,13 @@ test('buildClaudeSpawnArgs: model is never omitted even without a systemPrompt',
|
|
|
66
67
|
// stream-json success result and exits 0 — same technique as
|
|
67
68
|
// scheduler-bash-timeout-env.test.cjs's env-var proof, applied to argv.
|
|
68
69
|
function writeClaudeStub() {
|
|
69
|
-
|
|
70
|
-
const body = `
|
|
70
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
71
71
|
const fs = require('fs');
|
|
72
72
|
const path = require('path');
|
|
73
73
|
fs.writeFileSync(path.join(process.cwd(), 'argv.marker'), JSON.stringify(process.argv.slice(2)), 'utf8');
|
|
74
74
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
|
|
75
75
|
process.exit(0);
|
|
76
|
-
|
|
77
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
78
|
-
return stubPath;
|
|
76
|
+
` });
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
function writePersona(dir, name, frontmatterLines, bodyText) {
|
|
@@ -19,6 +19,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const os = require('node:os');
|
|
21
21
|
const path = require('node:path');
|
|
22
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
22
23
|
const { execFileSync } = require('node:child_process');
|
|
23
24
|
|
|
24
25
|
let tmpHome;
|
|
@@ -52,10 +53,41 @@ function registerActiveProject(cwd) {
|
|
|
52
53
|
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
53
54
|
}
|
|
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
|
+
|
|
55
84
|
function writeProjectQueue(cwd, jobs) {
|
|
56
85
|
const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
|
|
57
86
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
58
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
|
+
}
|
|
59
91
|
return path.join(stateDir, 'queue.json');
|
|
60
92
|
}
|
|
61
93
|
|
|
@@ -66,15 +98,12 @@ function writeProjectQueue(cwd, jobs) {
|
|
|
66
98
|
// as a genuine kill-with-partial-work, not an auto-retryable transient blip
|
|
67
99
|
// with no evidence of real execution.
|
|
68
100
|
function writeKilledClaudeStub() {
|
|
69
|
-
|
|
70
|
-
const body = `
|
|
101
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
71
102
|
const fs = require('fs');
|
|
72
103
|
const path = require('path');
|
|
73
104
|
fs.writeFileSync(path.join(process.cwd(), 'job-output.txt'), 'work in progress when killed\\n', 'utf8');
|
|
74
105
|
process.exit(137);
|
|
75
|
-
|
|
76
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
77
|
-
return stubPath;
|
|
106
|
+
` });
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
let reapDeadRunningJobs;
|
|
@@ -109,7 +138,13 @@ test('a killed quietMachine job releases the exclusive lease, and an ordinary jo
|
|
|
109
138
|
|
|
110
139
|
const quietSlug = `1107-test-quiet-killed-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
111
140
|
const ordinarySlug = `1107-test-ordinary-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
112
|
-
|
|
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');
|
|
113
148
|
fs.mkdirSync(prdsDir, { recursive: true });
|
|
114
149
|
fs.writeFileSync(path.join(prdsDir, `${quietSlug}.md`), '---\nquietMachine: true\n---\nMeasure timing without contention, then get killed.', 'utf8');
|
|
115
150
|
fs.writeFileSync(path.join(prdsDir, `${ordinarySlug}.md`), 'An ordinary PRD queued behind the quiet job.', 'utf8');
|