claude-code-session-manager 0.87.0 → 0.88.1
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-kQF8_wAw.js} +1 -1
- package/dist/assets/{DataModel--mISIJ6h.js → DataModel-CY_mMnsr.js} +1 -1
- package/dist/assets/{History-C2ahUXTg.js → History-Sji2brb0.js} +1 -1
- package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CtqpO-p5.js} +1 -1
- package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-DN0ydOCr.js} +1 -1
- package/dist/assets/{Library-Dc8Qst1R.js → Library-CoiLZTKh.js} +1 -1
- package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CmqiQ_io.js} +1 -1
- package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-HPoNeKhT.js} +1 -1
- package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-Bj1X3_NT.js} +1 -1
- package/dist/assets/{Memory-CW62MXlh.js → Memory-1PKjsM3b.js} +1 -1
- package/dist/assets/{Panel-Bw1FhRuF.js → Panel-I1s6ghPo.js} +1 -1
- package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-BhiiZjHQ.js} +1 -1
- package/dist/assets/{Plugins-BnKx9flD.js → Plugins-gv3_fkPX.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-BGcgBd8V.js} +1 -1
- package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BGr9eZ4g.js} +1 -1
- package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-Bdo_awPq.js} +7 -7
- package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-ukl0qWaA.js} +1 -1
- package/dist/assets/{Settings-DluB-vN1.js → Settings-DJkFj22G.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-0t1Qblp6.js} +1 -1
- package/dist/assets/{Skills-gNdo_HNK.js → Skills-CeVdBGE-.js} +1 -1
- package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-CfKvNHi3.js} +1 -1
- package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-DAzNaUPd.js} +1 -1
- package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody--S_rGXoA.js} +1 -1
- package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-Dszm0xnp.js} +1 -1
- package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/{index-fc_JjdxL.js → index-Dp4Rc--q.js} +356 -356
- package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-BLbssMYo.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 +27 -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-stall-per-project.test.cjs +14 -0
- 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 +276 -52
- 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 +1607 -383
- 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,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-build-freshness.test.cjs — evaluateBuildFreshness separates
|
|
3
|
+
* restart-needed (installed differs from running) from publish-needed
|
|
4
|
+
* (repo HEAD differs from installed); both reported, never critical.
|
|
5
|
+
*
|
|
6
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/health-build-freshness.test.cjs
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
import { test, expect } from 'vitest';
|
|
11
|
+
const { evaluateBuildFreshness } = require('../health.cjs');
|
|
12
|
+
|
|
13
|
+
test('all shas equal → nothing needed', () => {
|
|
14
|
+
const r = evaluateBuildFreshness({ running: 'abc1234', installedOnDisk: 'abc1234', repoHead: 'abc1234' });
|
|
15
|
+
expect(r).toMatchObject({ ok: true, restartNeeded: false, publishNeeded: false });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('installed differs from running → restart needed only', () => {
|
|
19
|
+
const r = evaluateBuildFreshness({ running: 'aaa1111', installedOnDisk: 'bbb2222', repoHead: 'bbb2222' });
|
|
20
|
+
expect(r).toMatchObject({ ok: true, restartNeeded: true, publishNeeded: false });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('repo HEAD differs from installed → publish needed only', () => {
|
|
24
|
+
const r = evaluateBuildFreshness({ running: 'bbb2222', installedOnDisk: 'bbb2222', repoHead: 'ccc3333' });
|
|
25
|
+
expect(r).toMatchObject({ ok: true, restartNeeded: false, publishNeeded: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('both differ → both reported, still ok (non-critical)', () => {
|
|
29
|
+
const r = evaluateBuildFreshness({ running: 'aaa1111', installedOnDisk: 'bbb2222', repoHead: 'ccc3333' });
|
|
30
|
+
expect(r).toMatchObject({ ok: true, restartNeeded: true, publishNeeded: true });
|
|
31
|
+
expect(r.note).toMatch(/restart needed/);
|
|
32
|
+
expect(r.note).toMatch(/publish needed/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('unknown inputs never claim a difference; short/long sha prefixes match', () => {
|
|
36
|
+
expect(evaluateBuildFreshness({})).toMatchObject({ restartNeeded: false, publishNeeded: false });
|
|
37
|
+
expect(evaluateBuildFreshness({ running: null, installedOnDisk: 'abc1234', repoHead: 'abc1234567890' }))
|
|
38
|
+
.toMatchObject({ restartNeeded: false, publishNeeded: false });
|
|
39
|
+
});
|
|
@@ -84,7 +84,21 @@ test('failing check with no fix omits the "(fix: ...)" suffix', () => {
|
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
test('end-to-end: check() reports delegation_chain for this repo, expected to pass here', async () => {
|
|
87
|
-
|
|
87
|
+
// Probes THIS machine's real user-scope config (~/.claude.json MCP registration, guard
|
|
88
|
+
// shims), so it reads the real home — not the per-run sandbox HOME. Read-only: scheduler
|
|
89
|
+
// state stays redirected to the sandbox (SM_SCHEDULER_HOME re-pinned after the HOME swap).
|
|
90
|
+
const sandboxHome = process.env.HOME;
|
|
91
|
+
const sandboxSchedulerHome = process.env.SM_SCHEDULER_HOME;
|
|
92
|
+
process.env.HOME = require('node:os').userInfo().homedir;
|
|
93
|
+
if (sandboxSchedulerHome !== undefined) process.env.SM_SCHEDULER_HOME = sandboxSchedulerHome;
|
|
94
|
+
process.env.SM_ALLOW_LIVE_ROOT_READS = '1';
|
|
95
|
+
let status;
|
|
96
|
+
try {
|
|
97
|
+
status = await check();
|
|
98
|
+
} finally {
|
|
99
|
+
delete process.env.SM_ALLOW_LIVE_ROOT_READS;
|
|
100
|
+
process.env.HOME = sandboxHome;
|
|
101
|
+
}
|
|
88
102
|
expect(status.components.delegation_chain).toBeDefined();
|
|
89
103
|
expect(status.components.delegation_chain.checks).toHaveLength(9);
|
|
90
104
|
const ids = status.components.delegation_chain.checks.map((c) => c.id);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* health-queue-dispatch.test.cjs — `npm run health` must report non-GREEN
|
|
3
3
|
* when there is at least one DISPATCHABLE pending job (reusing
|
|
4
4
|
* classifyQueueStarvation's own 'starved' verdict, not merely "pending"),
|
|
5
|
-
* nothing running, and no
|
|
5
|
+
* nothing running, and no launch in a long time — the exact shape
|
|
6
6
|
* of the 2026-09-11 incident (27 pending across two projects, 0 running, for
|
|
7
7
|
* days, every other health surface reporting green). A dependsOn-blocked
|
|
8
8
|
* queue must NOT trip this the same way: ticking can't fix a blocked chain,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
'use strict';
|
|
19
19
|
|
|
20
20
|
import { test, expect } from 'vitest';
|
|
21
|
-
const { evaluateQueueDispatchHealth, DISPATCH_STALL_THRESHOLD_MS } = require('../health.cjs');
|
|
21
|
+
const { evaluateQueueDispatchHealth, DISPATCH_STALL_THRESHOLD_MS, DISPATCH_WARN_THRESHOLD_MS } = require('../health.cjs');
|
|
22
22
|
|
|
23
23
|
const CWD = '/home/bilko/Projects/starry-night-ships';
|
|
24
24
|
const NOW = Date.parse('2026-09-11T12:00:00.000Z');
|
|
@@ -28,7 +28,7 @@ test('a dispatchable pending job, 0 running, stale past the threshold — non-GR
|
|
|
28
28
|
const state = {
|
|
29
29
|
jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
|
|
30
30
|
paused: null,
|
|
31
|
-
|
|
31
|
+
lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
|
|
32
32
|
};
|
|
33
33
|
const result = evaluateQueueDispatchHealth(state, 0, NOW);
|
|
34
34
|
expect(result.ok).toBe(false);
|
|
@@ -44,7 +44,7 @@ test('every pending row behind a blocked dependsOn chain stays GREEN, reported a
|
|
|
44
44
|
{ slug: 'blocked-child', cwd: CWD, status: 'pending', dependsOn: ['dead'] },
|
|
45
45
|
],
|
|
46
46
|
paused: null,
|
|
47
|
-
|
|
47
|
+
lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
|
|
48
48
|
};
|
|
49
49
|
const result = evaluateQueueDispatchHealth(state, 0, NOW);
|
|
50
50
|
expect(result.ok).toBe(true);
|
|
@@ -57,7 +57,7 @@ test('healthy queue (recent dispatch attempt) is GREEN with no verdict', () => {
|
|
|
57
57
|
const state = {
|
|
58
58
|
jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
|
|
59
59
|
paused: null,
|
|
60
|
-
|
|
60
|
+
lastRunAt: idleFor(60_000),
|
|
61
61
|
};
|
|
62
62
|
const result = evaluateQueueDispatchHealth(state, 0, NOW);
|
|
63
63
|
expect(result).toEqual({ ok: true });
|
|
@@ -67,7 +67,7 @@ test('nothing pending is GREEN regardless of how stale the dispatch timestamp is
|
|
|
67
67
|
const state = {
|
|
68
68
|
jobs: [{ slug: 'a', cwd: CWD, status: 'completed' }],
|
|
69
69
|
paused: null,
|
|
70
|
-
|
|
70
|
+
lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
|
|
71
71
|
};
|
|
72
72
|
const result = evaluateQueueDispatchHealth(state, 0, NOW);
|
|
73
73
|
expect(result).toEqual({ ok: true });
|
|
@@ -77,8 +77,59 @@ test('work already running is GREEN — the queue is flowing', () => {
|
|
|
77
77
|
const state = {
|
|
78
78
|
jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
|
|
79
79
|
paused: null,
|
|
80
|
-
|
|
80
|
+
lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
|
|
81
81
|
};
|
|
82
82
|
const result = evaluateQueueDispatchHealth(state, 1, NOW);
|
|
83
83
|
expect(result).toEqual({ ok: true });
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
test('the two tiers are exported: 15-minute warn, 2-hour cold report', () => {
|
|
87
|
+
expect(DISPATCH_WARN_THRESHOLD_MS).toBe(15 * 60_000);
|
|
88
|
+
expect(DISPATCH_STALL_THRESHOLD_MS).toBe(2 * 60 * 60_000);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('starved between the warn and cold thresholds — ok:true but flagged warn', () => {
|
|
92
|
+
const state = {
|
|
93
|
+
jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
|
|
94
|
+
paused: null,
|
|
95
|
+
lastRunAt: idleFor(DISPATCH_WARN_THRESHOLD_MS + 60_000),
|
|
96
|
+
};
|
|
97
|
+
const result = evaluateQueueDispatchHealth(state, 0, NOW);
|
|
98
|
+
expect(result.ok).toBe(true);
|
|
99
|
+
expect(result.warn).toBe(true);
|
|
100
|
+
expect(result.starved).toBeUndefined();
|
|
101
|
+
expect(result.dispatchable).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('below the warn threshold is plain GREEN', () => {
|
|
105
|
+
const state = {
|
|
106
|
+
jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
|
|
107
|
+
paused: null,
|
|
108
|
+
lastRunAt: idleFor(DISPATCH_WARN_THRESHOLD_MS - 60_000),
|
|
109
|
+
};
|
|
110
|
+
expect(evaluateQueueDispatchHealth(state, 0, NOW)).toEqual({ ok: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('ticks that launch nothing (fresh lastDispatchAttemptAt, stale lastRunAt) still go non-GREEN', () => {
|
|
114
|
+
const state = {
|
|
115
|
+
jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
|
|
116
|
+
paused: null,
|
|
117
|
+
lastDispatchAttemptAt: idleFor(5_000),
|
|
118
|
+
lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
|
|
119
|
+
};
|
|
120
|
+
const result = evaluateQueueDispatchHealth(state, 0, NOW);
|
|
121
|
+
expect(result.ok).toBe(false);
|
|
122
|
+
expect(result.starved).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('rows held by an open launch breaker read blocked, not starved', () => {
|
|
126
|
+
const state = {
|
|
127
|
+
jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
|
|
128
|
+
paused: null,
|
|
129
|
+
lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
|
|
130
|
+
launchBlocks: { default: { kind: 'auth', at: NOW - 1000 } },
|
|
131
|
+
};
|
|
132
|
+
const result = evaluateQueueDispatchHealth(state, 0, NOW);
|
|
133
|
+
expect(result.ok).toBe(true);
|
|
134
|
+
expect(result.blocked).toBe(true);
|
|
135
|
+
});
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* since they're pure/fs-isolated — no need to mock os.homedir() through the
|
|
7
7
|
* full check() to cover the liveness logic.
|
|
8
8
|
*
|
|
9
|
-
* Run: timeout
|
|
9
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/health-tick-liveness.test.cjs
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
import { test } from 'vitest';
|
|
15
15
|
const assert = require('node:assert/strict');
|
|
16
16
|
const fs = require('node:fs');
|
|
17
17
|
const os = require('node:os');
|
|
@@ -72,6 +72,7 @@ test('no false positive: running is already at concurrencyCap', () => {
|
|
|
72
72
|
{ slug: '544-stuck-job', status: 'pending' },
|
|
73
73
|
],
|
|
74
74
|
});
|
|
75
|
+
q.slotCap = 3; // machine-wide sessionSlots pool is the only cap now
|
|
75
76
|
const result = evaluateTickLiveness(q, null, NOW);
|
|
76
77
|
assert.strictEqual(result.stalled, false);
|
|
77
78
|
assert.strictEqual(result.reason, 'at-capacity');
|
|
@@ -105,9 +106,16 @@ test('degrades honestly: heartbeat missing/stale means utilization cannot be rul
|
|
|
105
106
|
assert.strictEqual(result.reason, 'cannot-verify-utilization');
|
|
106
107
|
});
|
|
107
108
|
|
|
108
|
-
test('
|
|
109
|
+
test('manual firePolicy: pending work with free capacity is configured behaviour, not a stall', () => {
|
|
109
110
|
const q = baseQueue({ config: { ...baseQueue().config, firePolicy: 'manual' } });
|
|
110
111
|
const result = evaluateTickLiveness(q, null, NOW);
|
|
112
|
+
assert.strictEqual(result.stalled, false);
|
|
113
|
+
assert.strictEqual(result.reason, 'manual-fire-policy');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('does not caveat when firePolicy is neither when-available nor manual (no heartbeat needed)', () => {
|
|
117
|
+
const q = baseQueue({ config: { ...baseQueue().config, firePolicy: 'always' } });
|
|
118
|
+
const result = evaluateTickLiveness(q, null, NOW);
|
|
111
119
|
assert.strictEqual(result.stalled, true);
|
|
112
120
|
});
|
|
113
121
|
|
|
@@ -141,3 +149,19 @@ test('readFreshHeartbeat: returns the parsed last line when fresh', () => {
|
|
|
141
149
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
142
150
|
}
|
|
143
151
|
});
|
|
152
|
+
|
|
153
|
+
test('no false positive: pending rows blocked on an unfinished dependsOn are waiting on a job, not a tick', () => {
|
|
154
|
+
const result = evaluateTickLiveness(
|
|
155
|
+
baseQueue({
|
|
156
|
+
jobs: [
|
|
157
|
+
{ slug: '265-head', status: 'running' },
|
|
158
|
+
{ slug: '266-next', status: 'pending', dependsOn: ['265-head'] },
|
|
159
|
+
],
|
|
160
|
+
}),
|
|
161
|
+
{ ts: NOW - 30_000, utilization: 0 },
|
|
162
|
+
NOW,
|
|
163
|
+
1,
|
|
164
|
+
);
|
|
165
|
+
assert.strictEqual(result.stalled, false);
|
|
166
|
+
assert.strictEqual(result.reason, 'no-pending-jobs');
|
|
167
|
+
});
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* health-usage-poller.test.cjs — `npm run health` must report
|
|
3
|
-
* the usage/rate-limit poller
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* health-usage-poller.test.cjs — `npm run health` must report GREEN / YELLOW
|
|
3
|
+
* / RED for the usage/rate-limit poller (scheduler.cjs pollLoop), driven by
|
|
4
|
+
* the shared usageCircuit's OWN open/closed state and how long it's been
|
|
5
|
+
* open (usageCircuitState/usageCircuitOpenedAt, persisted to
|
|
6
|
+
* ~/.claude/session-manager/scheduler-state.json by persistSchedulerState —
|
|
7
|
+
* this is a separate `npm run health` process, so it reads the PERSISTED
|
|
8
|
+
* circuit fields rather than holding the live in-memory breaker) — not the
|
|
9
|
+
* old binary consecutiveFailures<5 check, which read flatly non-GREEN
|
|
10
|
+
* forever after the fifth failure with no way to distinguish "just tripped"
|
|
11
|
+
* from "down for hours." Exercises evaluateUsagePollerHealth() and
|
|
12
|
+
* loadUsagePollerState() directly since both are pure/file-scoped, matching
|
|
10
13
|
* every other evaluate* helper's test pattern in this file — deliberately NOT
|
|
11
14
|
* driving the full (slow, machine-state-coupled) check() for this.
|
|
12
15
|
*
|
|
@@ -20,44 +23,88 @@ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
|
20
23
|
import { tmpdir } from 'node:os';
|
|
21
24
|
import { join } from 'node:path';
|
|
22
25
|
const { evaluateUsagePollerHealth, loadUsagePollerState } = require('../health.cjs');
|
|
23
|
-
const {
|
|
26
|
+
const { FAILURE_STREAK_ESCALATION_MS } = require('../scheduler.cjs');
|
|
24
27
|
|
|
25
28
|
test('missing state (fresh install / poller never ran) is ok, not applicable', () => {
|
|
26
29
|
expect(evaluateUsagePollerHealth(null)).toEqual({ ok: true, applicable: false });
|
|
27
30
|
expect(evaluateUsagePollerHealth(undefined)).toEqual({ ok: true, applicable: false });
|
|
28
31
|
});
|
|
29
32
|
|
|
30
|
-
test('
|
|
31
|
-
const result = evaluateUsagePollerHealth({
|
|
33
|
+
test('circuit closed is GREEN, regardless of a nonzero consecutiveFailures count', () => {
|
|
34
|
+
const result = evaluateUsagePollerHealth({ usageCircuitState: 'closed', consecutiveFailures: 2, backoffMs: 60_000, lastPollAt: Date.now() });
|
|
32
35
|
expect(result.ok).toBe(true);
|
|
33
36
|
expect(result.applicable).toBe(true);
|
|
37
|
+
expect(result.color).toBe('GREEN');
|
|
34
38
|
expect(result.message).toBeUndefined();
|
|
35
39
|
});
|
|
36
40
|
|
|
37
|
-
test('
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
expect(result.ok).toBe(false);
|
|
42
|
-
expect(result.message).toMatch(new RegExp(`${FAILURE_STREAK_WARN_THRESHOLD} consecutive failures`));
|
|
41
|
+
test('no usageCircuitState field at all (pre-upgrade sidecar) defaults to closed -> GREEN', () => {
|
|
42
|
+
const result = evaluateUsagePollerHealth({ consecutiveFailures: 0, backoffMs: null, lastPollAt: Date.now() });
|
|
43
|
+
expect(result.ok).toBe(true);
|
|
44
|
+
expect(result.color).toBe('GREEN');
|
|
43
45
|
});
|
|
44
46
|
|
|
45
|
-
test('
|
|
46
|
-
const
|
|
47
|
+
test('circuit open for under FAILURE_STREAK_ESCALATION_MS (30 min) is YELLOW, not GREEN — but still ok:true, not yet critical', () => {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const result = evaluateUsagePollerHealth({
|
|
50
|
+
usageCircuitState: 'open',
|
|
51
|
+
usageCircuitOpenedAt: now - 5 * 60_000, // 5 minutes ago
|
|
52
|
+
consecutiveFailures: 6,
|
|
53
|
+
backoffMs: 480_000,
|
|
54
|
+
lastPollAt: now,
|
|
55
|
+
});
|
|
56
|
+
// ok:true is deliberate — the circuit's own open threshold (3 consecutive
|
|
57
|
+
// failures) is LOWER than the old 5-failure WARN, so an ok:false YELLOW
|
|
58
|
+
// would silently tighten the overall `npm run health` rollup's failure bar
|
|
59
|
+
// from 5 down to 3. Graceful degradation must not itself read as critical.
|
|
60
|
+
expect(result.ok).toBe(true);
|
|
61
|
+
expect(result.color).toBe('YELLOW');
|
|
62
|
+
expect(result.message).toMatch(/YELLOW/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('circuit open for AT LEAST FAILURE_STREAK_ESCALATION_MS (30 min) is RED — ok:false, this is the critical state', () => {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const result = evaluateUsagePollerHealth({
|
|
68
|
+
usageCircuitState: 'open',
|
|
69
|
+
usageCircuitOpenedAt: now - FAILURE_STREAK_ESCALATION_MS,
|
|
70
|
+
consecutiveFailures: 57,
|
|
71
|
+
backoffMs: 480_000,
|
|
72
|
+
lastPollAt: now,
|
|
73
|
+
});
|
|
47
74
|
expect(result.ok).toBe(false);
|
|
75
|
+
expect(result.color).toBe('RED');
|
|
48
76
|
expect(result.consecutiveFailures).toBe(57);
|
|
49
77
|
expect(result.backoffMs).toBe(480_000);
|
|
50
|
-
expect(result.message).toMatch(/
|
|
78
|
+
expect(result.message).toMatch(/RED/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('half_open (mid-probe) is treated the same as open — not GREEN just because a probe is in flight', () => {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const result = evaluateUsagePollerHealth({
|
|
84
|
+
usageCircuitState: 'half_open',
|
|
85
|
+
usageCircuitOpenedAt: now - 60_000,
|
|
86
|
+
consecutiveFailures: 5,
|
|
87
|
+
lastPollAt: now,
|
|
88
|
+
});
|
|
89
|
+
expect(result.ok).toBe(true); // still YELLOW-shaped (under the red threshold), so still ok:true
|
|
90
|
+
expect(result.color).toBe('YELLOW');
|
|
51
91
|
});
|
|
52
92
|
|
|
53
|
-
test('respects a custom
|
|
54
|
-
|
|
55
|
-
|
|
93
|
+
test('respects a custom redThresholdMs argument', () => {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const openedAt = now - 10_000;
|
|
96
|
+
const red = evaluateUsagePollerHealth({ usageCircuitState: 'open', usageCircuitOpenedAt: openedAt, consecutiveFailures: 5 }, 5_000);
|
|
97
|
+
expect(red.color).toBe('RED');
|
|
98
|
+
expect(red.ok).toBe(false);
|
|
99
|
+
const yellow = evaluateUsagePollerHealth({ usageCircuitState: 'open', usageCircuitOpenedAt: openedAt, consecutiveFailures: 5 }, 60_000);
|
|
100
|
+
expect(yellow.color).toBe('YELLOW');
|
|
101
|
+
expect(yellow.ok).toBe(true);
|
|
56
102
|
});
|
|
57
103
|
|
|
58
104
|
test('tolerates a malformed/partial state object without throwing', () => {
|
|
59
105
|
const result = evaluateUsagePollerHealth({});
|
|
60
106
|
expect(result.ok).toBe(true);
|
|
107
|
+
expect(result.color).toBe('GREEN');
|
|
61
108
|
expect(result.consecutiveFailures).toBe(0);
|
|
62
109
|
});
|
|
63
110
|
|
|
@@ -135,7 +135,7 @@ test('appendRollupDays: compacts (dedupes) once file exceeds threshold', async (
|
|
|
135
135
|
{ date: '2026-07-01', projectDir: 'p', modelId: 'sonnet', inputTokens: 2 },
|
|
136
136
|
]);
|
|
137
137
|
await historyRollup.compact();
|
|
138
|
-
const raw = fs.readFileSync(historyRollup.
|
|
138
|
+
const raw = fs.readFileSync(historyRollup.historyRollupPath(), 'utf8');
|
|
139
139
|
const nonEmptyLines = raw.split('\n').filter((l) => l.trim());
|
|
140
140
|
expect(nonEmptyLines.length).toBe(1);
|
|
141
141
|
expect(JSON.parse(nonEmptyLines[0]).inputTokens).toBe(2);
|
|
@@ -305,7 +305,7 @@ test('refreshIntradayToday: upserts a provisional (non-finalized) line for today
|
|
|
305
305
|
// duplicates — today's line for the same key once the file is compacted.
|
|
306
306
|
await historyAggregator.refreshIntradayToday();
|
|
307
307
|
await historyRollup.compact();
|
|
308
|
-
const raw = fs.readFileSync(historyRollup.
|
|
308
|
+
const raw = fs.readFileSync(historyRollup.historyRollupPath(), 'utf8');
|
|
309
309
|
const matchingLines = raw.split('\n').filter((l) => {
|
|
310
310
|
if (!l.trim()) return false;
|
|
311
311
|
const obj = JSON.parse(l);
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* differences between log sources don't break the match.
|
|
15
15
|
*
|
|
16
16
|
* Does NOT spawn claude -p. Run:
|
|
17
|
-
* timeout
|
|
17
|
+
* timeout 300 npx vitest run src/main/__tests__/kg-augment.test.cjs
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
import { test, describe, beforeAll as before, afterAll as after } from 'vitest';
|
|
21
21
|
const assert = require('node:assert/strict');
|
|
22
22
|
const fs = require('node:fs');
|
|
23
23
|
const fsp = require('node:fs/promises');
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mcpStatus.test.cjs — unit tests for the `claude mcp list` output parser.
|
|
3
3
|
*
|
|
4
|
-
* Run: timeout
|
|
4
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/mcpStatus.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 { parseMcpList } = require('../mcpStatus.cjs');
|
|
12
12
|
|
|
@@ -18,10 +18,15 @@ const opsErrorLog = require('../lib/opsErrorLog.cjs');
|
|
|
18
18
|
const tmpDirs = [];
|
|
19
19
|
let originalHome;
|
|
20
20
|
let originalSmTelemetrySpool;
|
|
21
|
+
let originalWorktreeRoot;
|
|
21
22
|
|
|
22
23
|
afterEach(async () => {
|
|
23
24
|
if (originalHome !== undefined) process.env.HOME = originalHome;
|
|
24
25
|
if (originalSmTelemetrySpool === undefined) delete process.env.SM_TELEMETRY_SPOOL; else process.env.SM_TELEMETRY_SPOOL = originalSmTelemetrySpool;
|
|
26
|
+
if (originalWorktreeRoot !== undefined) {
|
|
27
|
+
if (originalWorktreeRoot === null) delete process.env.SM_WORKTREE_ROOT; else process.env.SM_WORKTREE_ROOT = originalWorktreeRoot;
|
|
28
|
+
originalWorktreeRoot = undefined;
|
|
29
|
+
}
|
|
25
30
|
const telemetryPath = require.resolve('../lib/telemetryClient.cjs');
|
|
26
31
|
delete require.cache[telemetryPath];
|
|
27
32
|
const activeSessionsPath = require.resolve('../lib/activeSessions.cjs');
|
|
@@ -85,6 +90,17 @@ test('level "warn" routes to telemetryClient.logLine, default "error" routes to
|
|
|
85
90
|
// ─── ephemeral cwd: local refused, telemetry still fires ─────────────────
|
|
86
91
|
|
|
87
92
|
test('an ephemeral cwd yields zero local lines but one telemetry record, with a normalized projectHash', () => {
|
|
93
|
+
// Why this test depends on the process tmpdir: ephemeral classification
|
|
94
|
+
// (cwdClassify.classifyCwd, via ephemeralCwd.isEphemeralCwd) is an exact match
|
|
95
|
+
// on os.tmpdir() plus a prefix match on the managed job/epic worktree roots,
|
|
96
|
+
// and those roots are <SM_WORKTREE_ROOT || os.tmpdir()>/session-manager-*-worktrees
|
|
97
|
+
// (schedulerPaths.worktreeRoot). The sandbox globalSetup mkdtemp's that base under
|
|
98
|
+
// the tmpdir it saw at startup, so a relocated TMPDIR / prior env mutation changes
|
|
99
|
+
// where KIND_CONFIG.epic.root points. Pin SM_WORKTREE_ROOT to a fixture dir this
|
|
100
|
+
// test owns and derive the cwd from KIND_CONFIG, so expected and actual roots
|
|
101
|
+
// resolve through the same code path regardless of TMPDIR.
|
|
102
|
+
originalWorktreeRoot = process.env.SM_WORKTREE_ROOT === undefined ? null : process.env.SM_WORKTREE_ROOT;
|
|
103
|
+
process.env.SM_WORKTREE_ROOT = mkTmpProject();
|
|
88
104
|
const { KIND_CONFIG } = require('../lib/gitWorktree.cjs');
|
|
89
105
|
const worktreeCwd = path.join(KIND_CONFIG.epic.root, 'fakehash', 'fake-epic-id');
|
|
90
106
|
const realRoot = '/home/bilko/Projects/real-project';
|
|
@@ -103,7 +119,9 @@ test('an ephemeral cwd yields zero local lines but one telemetry record, with a
|
|
|
103
119
|
|
|
104
120
|
opsErrorLog.appendError({ cwd: worktreeCwd, scope: 'chatRunner', message: 'worktree err' });
|
|
105
121
|
|
|
106
|
-
|
|
122
|
+
// resolveProjectRoot now consumes cwdClassify (not the stubbed projectRootOf):
|
|
123
|
+
// an ephemeral worktree cwd is refused outright, so no local path is even built.
|
|
124
|
+
expect(() => opsErrorLog.todayFile(worktreeCwd)).toThrow(/ephemeral/);
|
|
107
125
|
expect(reportErrorCalls).toHaveLength(1);
|
|
108
126
|
expect(reportErrorCalls[0].context.cwd).toBe(realRoot);
|
|
109
127
|
expect(reportErrorCalls[0].context.cwd).not.toBe(worktreeCwd);
|
|
@@ -139,6 +157,12 @@ test('a realistic error with an absolute path and a prompt-like string never rea
|
|
|
139
157
|
message: `run failed while processing "${promptLike}" at ${absPath}`,
|
|
140
158
|
});
|
|
141
159
|
|
|
160
|
+
// The telemetry write is fire-and-forget async; poll (bounded) for the queue
|
|
161
|
+
// file instead of a fixed sleep, which raced under load.
|
|
162
|
+
const deadline = Date.now() + 5000;
|
|
163
|
+
while (!fs.existsSync(client.queuePath()) && Date.now() < deadline) {
|
|
164
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
165
|
+
}
|
|
142
166
|
await new Promise((r) => setTimeout(r, 50));
|
|
143
167
|
|
|
144
168
|
const raw = await fsp.readFile(client.queuePath(), 'utf8');
|
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
* whether that pass ends in an actual job launch (see classifyQueueStarvation's
|
|
17
17
|
* header for why this must be distinct from `lastRunAt`).
|
|
18
18
|
*
|
|
19
|
+
* Usage-meter circuit PRD update: pollLoop no longer fires blind at a hard-
|
|
20
|
+
* coded cachedUtilization=0 on failure — it falls back to
|
|
21
|
+
* usageCircuit.degradedBudget(), which carries forward the last known-good
|
|
22
|
+
* BINDING window's utilization (or conservatively assumes 100/no-headroom
|
|
23
|
+
* when there's no known-good reading yet at all). So each test below first
|
|
24
|
+
* drives ONE successful ('ok') poll — with the queue still empty, so it
|
|
25
|
+
* can't itself cause a dispatch — to seed that known-good low-utilization
|
|
26
|
+
* reading, THEN seeds the pending job and drives the failure. This proves
|
|
27
|
+
* dispatch proceeds under the CARRIED-FORWARD degraded budget, not a blind 0.
|
|
28
|
+
*
|
|
19
29
|
* No existing test in this repo mocks a module import (see
|
|
20
30
|
* pty-epic-worktree-spawn-cwd.test.cjs's header) — this monkey-patches
|
|
21
31
|
* usage.cjs's cached module object directly instead, same pattern.
|
|
@@ -110,11 +120,23 @@ async function flushPendingTick() {
|
|
|
110
120
|
await scheduler.tickQueue();
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
test('pollLoop still attempts a dispatch after a transient billing failure', async () => {
|
|
123
|
+
test('pollLoop still attempts a dispatch after a transient billing failure, using the degraded budget (not utilization 0)', async () => {
|
|
124
|
+
// Seed a known-good low-utilization reading via an 'ok' poll first.
|
|
125
|
+
process.env.SM_E2E = '1';
|
|
126
|
+
process.env.SM_MOCK_BILLING_KIND = 'ok';
|
|
127
|
+
await scheduler.pollLoop();
|
|
128
|
+
// A prior test's leftover project (a REAL, still-registered project cwd —
|
|
129
|
+
// readQueue() merges every such cwd) can still hold its own permanently-
|
|
130
|
+
// pending job, so this seed poll's own maybeLaunchWhenAvailable may itself
|
|
131
|
+
// spawn a fire-and-forget tick; drain it before resetting/reseeding so
|
|
132
|
+
// that stray tick's async mutate() can't land AFTER (and clobber) the
|
|
133
|
+
// reset below.
|
|
134
|
+
await flushPendingTick();
|
|
135
|
+
await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
|
|
136
|
+
|
|
114
137
|
await seedOnePendingJob();
|
|
115
138
|
expect(await lastDispatchAttemptAt()).toBeNull();
|
|
116
139
|
|
|
117
|
-
process.env.SM_E2E = '1';
|
|
118
140
|
process.env.SM_MOCK_BILLING_KIND = 'transient';
|
|
119
141
|
await scheduler.pollLoop();
|
|
120
142
|
await flushPendingTick();
|
|
@@ -122,10 +144,18 @@ test('pollLoop still attempts a dispatch after a transient billing failure', asy
|
|
|
122
144
|
expect(await lastDispatchAttemptAt()).not.toBeNull();
|
|
123
145
|
});
|
|
124
146
|
|
|
125
|
-
test('pollLoop still attempts a dispatch after the billing poll throws', async () => {
|
|
147
|
+
test('pollLoop still attempts a dispatch after the billing poll throws, using the degraded budget (not utilization 0)', async () => {
|
|
148
|
+
process.env.SM_E2E = '1';
|
|
149
|
+
process.env.SM_MOCK_BILLING_KIND = 'ok';
|
|
150
|
+
await scheduler.pollLoop(); // seed a known-good low-utilization reading
|
|
151
|
+
await flushPendingTick();
|
|
152
|
+
await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
|
|
153
|
+
|
|
126
154
|
await seedOnePendingJob();
|
|
127
155
|
expect(await lastDispatchAttemptAt()).toBeNull();
|
|
128
156
|
|
|
157
|
+
delete process.env.SM_E2E;
|
|
158
|
+
delete process.env.SM_MOCK_BILLING_KIND;
|
|
129
159
|
billing.fetchUsage = async () => { throw new Error('simulated IPC transport failure'); };
|
|
130
160
|
await scheduler.pollLoop();
|
|
131
161
|
await flushPendingTick();
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* so a collision silently merges two unrelated projects' PRDs into one
|
|
9
9
|
* group.
|
|
10
10
|
*
|
|
11
|
-
* Run: timeout
|
|
11
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/prd-group-allocator.test.cjs
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import { test } from 'vitest';
|
|
17
17
|
const assert = require('node:assert/strict');
|
|
18
18
|
const fs = require('node:fs');
|
|
19
19
|
const fsp = require('node:fs/promises');
|
|
@@ -40,6 +40,8 @@ const REQUIRED_CAPABILITIES = [
|
|
|
40
40
|
{ capability: 'retag a PRD (parallelGroup/estimateMinutes)', ipcChannel: 'schedule:retag-prd', adminRoute: 'POST /admin/scheduler/retag-prd' },
|
|
41
41
|
{ capability: 'list scheduler jobs', ipcChannel: null, adminRoute: 'GET /admin/scheduler/jobs' },
|
|
42
42
|
{ capability: 'reset a stuck job', ipcChannel: 'schedule:reset-job', adminRoute: 'POST /admin/scheduler/reset-job' },
|
|
43
|
+
{ capability: 'pause new dispatch (user-initiated)', ipcChannel: 'schedule:pause', adminRoute: 'POST /admin/scheduler/pause' },
|
|
44
|
+
{ capability: 'resume a paused scheduler', ipcChannel: 'schedule:resume', adminRoute: 'POST /admin/scheduler/resume' },
|
|
43
45
|
{ capability: 'cancel a not-yet-terminal job', ipcChannel: null, adminRoute: 'POST /admin/scheduler/cancel-job' },
|
|
44
46
|
];
|
|
45
47
|
|
|
@@ -499,6 +499,12 @@ test('POST /admin/scheduler/reset-job with cwd resets the SAME-slug job belongin
|
|
|
499
499
|
bustCwdCache();
|
|
500
500
|
const slug = uniqueSlug('collision-slug');
|
|
501
501
|
const epicId = 'epic-reset-collision';
|
|
502
|
+
// Both projects' rows need a real PRD source — a 'needs_review' row whose
|
|
503
|
+
// PRD is invisible to reconcile()'s onDisk scan is silently dropped (never
|
|
504
|
+
// backfilled to history.jsonl the way a completed/failed/skipped orphan
|
|
505
|
+
// is), which would make jobA vanish for a reason unrelated to what this
|
|
506
|
+
// test actually verifies (cwd-scoped job lookup).
|
|
507
|
+
await writePrdFixture({ cwd: cwdA, epicId, slug });
|
|
502
508
|
await writePrdFixture({ cwd: cwdB, epicId, slug });
|
|
503
509
|
await scheduler.writeQueue({
|
|
504
510
|
jobs: [
|
|
@@ -518,8 +524,14 @@ test('POST /admin/scheduler/reset-job with cwd resets the SAME-slug job belongin
|
|
|
518
524
|
expect(res.json).toEqual({ ok: true, slug, status: 'pending' });
|
|
519
525
|
|
|
520
526
|
const state = await scheduler.remote.listJobs();
|
|
521
|
-
|
|
522
|
-
|
|
527
|
+
// Filter by slug too, not just cwd: cwdA is a shared fixture directory
|
|
528
|
+
// reused by many tests in this file, each of which leaves its own
|
|
529
|
+
// never-archived PRD file behind (only the queue row is cleared in
|
|
530
|
+
// `finally`) — reconcile()'s fresh-discovery pass can quarantine one of
|
|
531
|
+
// those leftovers as a NEW row under this same cwd on any later pass, so
|
|
532
|
+
// `cwd` alone no longer uniquely identifies this test's own job.
|
|
533
|
+
const jobA = state.find((j) => j.cwd === cwdA && j.slug === slug);
|
|
534
|
+
const jobB = state.find((j) => j.cwd === cwdB && j.slug === slug);
|
|
523
535
|
expect(jobA.status).toBe('needs_review'); // untouched
|
|
524
536
|
expect(jobB.status).toBe('pending'); // the one actually targeted
|
|
525
537
|
} finally {
|