claude-code-session-manager 0.74.0 → 0.75.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/dist/assets/AgentLibrary-CGplUKxY.js +3 -0
- package/dist/assets/DataModel-DQ15KxVb.js +1 -0
- package/dist/assets/{History-ShwkUTny.js → History-v2TSC_6A.js} +2 -2
- package/dist/assets/{Hooks-Dc5y1rZH.js → Hooks-Co7s6BNB.js} +2 -2
- package/dist/assets/HostBilko-DJj8FWPK.js +1 -0
- package/dist/assets/{Library-BOL6GqFX.js → Library-DQaC9VQ1.js} +1 -1
- package/dist/assets/ListDetail-BwFRfAZm.js +1 -0
- package/dist/assets/MarkdownEditor-C155gNpc.js +1 -0
- package/dist/assets/{McpServers-DuOTxmc4.js → McpServers-BmLgE_gS.js} +2 -2
- package/dist/assets/{Memory-BYJHDz7W.js → Memory-ISwNcS5j.js} +4 -4
- package/dist/assets/Panel-1adjEpj-.js +1 -0
- package/dist/assets/{Permissions-DhYERDVR.js → Permissions-FrCz5cGa.js} +3 -3
- package/dist/assets/Plugins-zUssKp_b.js +2 -0
- package/dist/assets/ProvenanceBadge-CAXqA_HX.js +1 -0
- package/dist/assets/SaveBar-D9cqms3Y.js +1 -0
- package/dist/assets/{Scheduler-Bu6kSkl3.js → Scheduler-dNNc9S11.js} +6 -6
- package/dist/assets/ScopeSwitcher-1wBTne7S.js +1 -0
- package/dist/assets/Settings-BT_expIg.js +3 -0
- package/dist/assets/{SkillReferenceGraph-rKaTHmOg.js → SkillReferenceGraph-CVccq4Cu.js} +1 -1
- package/dist/assets/{Skills-BHlH2uG5.js → Skills-xFpRY0ld.js} +2 -2
- package/dist/assets/SystemPrompt-CEk_71th.js +1 -0
- package/dist/assets/TagLibrary-C2zhihQG.js +1 -0
- package/dist/assets/{TiptapBody-XoQHiqfF.js → TiptapBody-CQKcHKaD.js} +1 -1
- package/dist/assets/Toggle-CQ-IBb8m.js +1 -0
- package/dist/assets/{index-Cs_Lq9-w.js → index-BgbV3sKX.js} +659 -655
- package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
- package/dist/assets/{settingsSchema-BTDJcu2x.js → settingsSchema-D6ccSKqm.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/scripts/scheduler-mcp-server.cjs +471 -0
- package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +116 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +113 -7
- package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +119 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +105 -0
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +145 -0
- package/src/main/__tests__/seedStatus.test.cjs +100 -0
- package/src/main/agentLibrary.cjs +43 -0
- package/src/main/index.cjs +42 -1
- package/src/main/ipcSchemas.cjs +25 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +185 -0
- package/src/main/lib/__tests__/epicDelegationStats.test.cjs +137 -0
- package/src/main/lib/delegationReadiness.cjs +141 -0
- package/src/main/lib/epicDelegationStats.cjs +124 -0
- package/src/main/lib/promptSessionSchema.cjs +1 -1
- package/src/main/scheduler.cjs +136 -27
- package/src/main/seedAgentPersonas.cjs +92 -0
- package/src/main/seedDevPlugin.cjs +14 -2
- package/src/main/seedSchedulerMcp.cjs +148 -0
- package/src/main/seedStatus.cjs +59 -0
- package/src/preload/api.d.ts +40 -1
- package/src/preload/index.cjs +7 -0
- package/src/seed/agents/architect.md +64 -0
- package/src/seed/agents/dev-lead.md +42 -0
- package/src/seed/prompts/code-review/ac-coverage-check.md +8 -0
- package/src/seed/prompts/code-review/correctness-only.md +8 -0
- package/src/seed/prompts/code-review/full-spectrum-high.md +8 -0
- package/src/seed/prompts/code-review/hallucination-check.md +8 -0
- package/src/seed/prompts/code-review/public-api-compat.md +8 -0
- package/src/seed/prompts/code-review/readability-naming.md +8 -0
- package/src/seed/prompts/debugging/bug-as-failing-test.md +8 -0
- package/src/seed/prompts/debugging/git-bisect-regression.md +8 -0
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +8 -0
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +8 -0
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +8 -0
- package/src/seed/prompts/documentation/adr-from-change.md +8 -0
- package/src/seed/prompts/documentation/module-readme.md +8 -0
- package/src/seed/prompts/documentation/onboarding-plan.md +8 -0
- package/src/seed/prompts/documentation/refresh-claude-md.md +8 -0
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +8 -0
- package/src/seed/prompts/git-pr/conventional-commit.md +8 -0
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +8 -0
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +8 -0
- package/src/seed/prompts/git-pr/release-notes-block.md +8 -0
- package/src/seed/prompts/git-pr/split-large-pr.md +8 -0
- package/src/seed/prompts/performance/bundle-startup-audit.md +8 -0
- package/src/seed/prompts/performance/complexity-audit.md +8 -0
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +8 -0
- package/src/seed/prompts/performance/db-query-plan-review.md +8 -0
- package/src/seed/prompts/performance/memory-leak-hunt.md +8 -0
- package/src/seed/prompts/qa/api-contract-tests.md +8 -0
- package/src/seed/prompts/qa/e2e-critical-path.md +8 -0
- package/src/seed/prompts/qa/failing-test-for-bug.md +8 -0
- package/src/seed/prompts/qa/find-missing-test-coverage.md +8 -0
- package/src/seed/prompts/qa/stabilize-flaky-test.md +8 -0
- package/src/seed/prompts/qa/tdd-red-first.md +8 -0
- package/src/seed/prompts/qa/visual-regression-review.md +8 -0
- package/src/seed/prompts/qa/wcag-axe-scan.md +8 -0
- package/src/seed/prompts/refactoring/dead-code-sweep.md +8 -0
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +8 -0
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +8 -0
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +8 -0
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +8 -0
- package/src/seed/prompts/security/authz-audit.md +8 -0
- package/src/seed/prompts/security/crypto-correctness.md +8 -0
- package/src/seed/prompts/security/cwe-top-25-hunt.md +8 -0
- package/src/seed/prompts/security/dependency-audit.md +8 -0
- package/src/seed/prompts/security/ipc-boundary-hardening.md +8 -0
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +8 -0
- package/src/seed/prompts/security/secret-credential-scan.md +8 -0
- package/dist/assets/AgentLibrary-CSnWV6uv.js +0 -3
- package/dist/assets/HostBilko-evd1hD8q.js +0 -1
- package/dist/assets/ListDetail-BPJb2et3.js +0 -1
- package/dist/assets/MarkdownEditor-CACsHVuf.js +0 -1
- package/dist/assets/Panel-n-p8OQLc.js +0 -1
- package/dist/assets/Plugins-D1ckVgo3.js +0 -2
- package/dist/assets/ProvenanceBadge-ByTjDwAX.js +0 -1
- package/dist/assets/SaveBar-_ORUCmZw.js +0 -1
- package/dist/assets/ScopeSwitcher-Css6Sg7Y.js +0 -1
- package/dist/assets/Settings-DlyJjexf.js +0 -3
- package/dist/assets/SystemPrompt-DsgF8xsl.js +0 -1
- package/dist/assets/TagLibrary-Q5QsKMes.js +0 -1
- package/dist/assets/Toggle-BJHhHQOq.js +0 -1
|
@@ -11,7 +11,7 @@ const assert = require('node:assert/strict');
|
|
|
11
11
|
const {
|
|
12
12
|
selectAutoFixTargets, isUnresolvableNeedsReview, isRescanCandidate,
|
|
13
13
|
healTargetForFix, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH,
|
|
14
|
-
isEligibleForImmediateAutoFix,
|
|
14
|
+
isEligibleForImmediateAutoFix, isExhaustedAutoFix, isPlanUnqueued, fixSlugFor,
|
|
15
15
|
} = require('../scheduler.cjs');
|
|
16
16
|
|
|
17
17
|
const noSiblingOnDisk = () => false;
|
|
@@ -34,8 +34,8 @@ test('selects a fresh needs_review job', () => {
|
|
|
34
34
|
assert.strictEqual(result[0].slug, '05-my-feature');
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
test('excludes job with autoFixAttempted: true', () => {
|
|
38
|
-
const jobs = [makeJob({ autoFixAttempted: true })];
|
|
37
|
+
test('excludes job with autoFixAttempted: true whose retry is already spent', () => {
|
|
38
|
+
const jobs = [makeJob({ autoFixAttempted: true, autoFixRetries: 1 })];
|
|
39
39
|
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
40
40
|
assert.strictEqual(result.length, 0);
|
|
41
41
|
});
|
|
@@ -105,12 +105,112 @@ test('autoFixOutcome no-plan with retries=1 is exhausted (not selected)', () =>
|
|
|
105
105
|
assert.strictEqual(result.length, 0);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
// ---- PRD: unstamped/error outcomes must retry once, not dead-end forever ----
|
|
109
|
+
|
|
110
|
+
test('autoFixAttempted true with NO outcome recorded (unstamped — spawn threw before onExit wired) is retry-eligible once', () => {
|
|
111
|
+
const jobs = [makeJob({ autoFixAttempted: true, autoFixRetries: 0 })];
|
|
112
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
113
|
+
assert.strictEqual(result.length, 1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('autoFixAttempted true with no outcome AND retries=1 is exhausted (not selected)', () => {
|
|
117
|
+
const jobs = [makeJob({ autoFixAttempted: true, autoFixRetries: 1 })];
|
|
110
118
|
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
111
119
|
assert.strictEqual(result.length, 0);
|
|
112
120
|
});
|
|
113
121
|
|
|
122
|
+
test('autoFixOutcome error with retries=0 is retry-eligible', () => {
|
|
123
|
+
const jobs = [makeJob({ autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 0 })];
|
|
124
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
125
|
+
assert.strictEqual(result.length, 1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('autoFixOutcome error with retries=1 is exhausted (not selected)', () => {
|
|
129
|
+
const jobs = [makeJob({ autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 1 })];
|
|
130
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
131
|
+
assert.strictEqual(result.length, 0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('autoFixOutcome plan is NEVER retried, regardless of autoFixRetries', () => {
|
|
135
|
+
const jobs = [makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan', autoFixRetries: 0 })];
|
|
136
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
137
|
+
assert.strictEqual(result.length, 0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ---- isExhaustedAutoFix / isPlanUnqueued (reverifyNeedsReview annotation) ----
|
|
141
|
+
|
|
142
|
+
test('isExhaustedAutoFix: no-plan outcome with retries spent → true', () => {
|
|
143
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'no-plan', autoFixRetries: 1 });
|
|
144
|
+
assert.strictEqual(isExhaustedAutoFix(job), true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('isExhaustedAutoFix: error outcome with retries spent → true', () => {
|
|
148
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 1 });
|
|
149
|
+
assert.strictEqual(isExhaustedAutoFix(job), true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('isExhaustedAutoFix: unstamped outcome with retries spent → true', () => {
|
|
153
|
+
const job = makeJob({ autoFixAttempted: true, autoFixRetries: 1 });
|
|
154
|
+
assert.strictEqual(isExhaustedAutoFix(job), true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('isExhaustedAutoFix: retries not yet spent → false (still eligible for its one retry)', () => {
|
|
158
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'no-plan', autoFixRetries: 0 });
|
|
159
|
+
assert.strictEqual(isExhaustedAutoFix(job), false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('isExhaustedAutoFix: plan outcome is never "exhausted" here — isPlanUnqueued\'s job instead', () => {
|
|
163
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan', autoFixRetries: 1 });
|
|
164
|
+
assert.strictEqual(isExhaustedAutoFix(job), false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('isPlanUnqueued: plan outcome, fix slug absent from queue → true', () => {
|
|
168
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan' });
|
|
169
|
+
assert.strictEqual(isPlanUnqueued(job, new Set()), true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('isPlanUnqueued: plan outcome, fix slug present in queue → false', () => {
|
|
173
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan' });
|
|
174
|
+
assert.strictEqual(isPlanUnqueued(job, new Set([fixSlugFor(job)])), false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('isPlanUnqueued: no-plan outcome is never "plan unqueued"', () => {
|
|
178
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'no-plan' });
|
|
179
|
+
assert.strictEqual(isPlanUnqueued(job, new Set()), false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---- invariant: every needs_review job is either an auto-fix target, or
|
|
183
|
+
// classified by one of the annotation predicates (i.e. would receive a
|
|
184
|
+
// non-null verifierVerdict) — no job may fall through both. ----
|
|
185
|
+
|
|
186
|
+
test('invariant: attempted+no-outcome, retries spent → not a target, but IS annotatable', () => {
|
|
187
|
+
const job = makeJob({ autoFixAttempted: true, autoFixRetries: 1 });
|
|
188
|
+
const isTarget = selectAutoFixTargets([job], { fixSlugExists: noSiblingOnDisk }).length === 1;
|
|
189
|
+
const isAnnotatable = isExhaustedAutoFix(job) || isPlanUnqueued(job, new Set())
|
|
190
|
+
|| isUnresolvableNeedsReview(job, { hasRunDir: true });
|
|
191
|
+
assert.strictEqual(isTarget, false);
|
|
192
|
+
assert.strictEqual(isAnnotatable, true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('invariant: plan outcome, no queue row → not a target, but IS annotatable (autofix_plan_unqueued)', () => {
|
|
196
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan' });
|
|
197
|
+
const isTarget = selectAutoFixTargets([job], { fixSlugExists: noSiblingOnDisk }).length === 1;
|
|
198
|
+
const isAnnotatable = isExhaustedAutoFix(job) || isPlanUnqueued(job, new Set())
|
|
199
|
+
|| isUnresolvableNeedsReview(job, { hasRunDir: true });
|
|
200
|
+
assert.strictEqual(isTarget, false);
|
|
201
|
+
assert.strictEqual(isAnnotatable, true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('invariant: plan outcome WITH a queue row → not a target and NOT annotated (waiting on the fix-plan job itself)', () => {
|
|
205
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan' });
|
|
206
|
+
const queuedSlugs = new Set([fixSlugFor(job)]);
|
|
207
|
+
const isTarget = selectAutoFixTargets([job], { fixSlugExists: noSiblingOnDisk }).length === 1;
|
|
208
|
+
const isAnnotatable = isExhaustedAutoFix(job) || isPlanUnqueued(job, queuedSlugs)
|
|
209
|
+
|| isUnresolvableNeedsReview(job, { hasRunDir: true });
|
|
210
|
+
assert.strictEqual(isTarget, false);
|
|
211
|
+
assert.strictEqual(isAnnotatable, false);
|
|
212
|
+
});
|
|
213
|
+
|
|
114
214
|
test('excludes when fix sibling exists on disk', () => {
|
|
115
215
|
const jobs = [makeJob()];
|
|
116
216
|
// fixSlug = '05-fix-my-feature'
|
|
@@ -224,12 +324,18 @@ test('isEligibleForImmediateAutoFix: fresh needs_review job (fresh from spawnJob
|
|
|
224
324
|
assert.strictEqual(result, true);
|
|
225
325
|
});
|
|
226
326
|
|
|
227
|
-
test('isEligibleForImmediateAutoFix: job already
|
|
228
|
-
const job = makeJob({ autoFixAttempted: true });
|
|
327
|
+
test('isEligibleForImmediateAutoFix: attempted job with retry already spent is excluded (prevents double-spawn with the periodic pass)', () => {
|
|
328
|
+
const job = makeJob({ autoFixAttempted: true, autoFixRetries: 1 });
|
|
229
329
|
const result = isEligibleForImmediateAutoFix(job, [job], noSiblingOnDisk);
|
|
230
330
|
assert.strictEqual(result, false);
|
|
231
331
|
});
|
|
232
332
|
|
|
333
|
+
test('isEligibleForImmediateAutoFix: attempted job with an unstamped outcome and its retry still unspent IS eligible (bounded once)', () => {
|
|
334
|
+
const job = makeJob({ autoFixAttempted: true, autoFixRetries: 0 });
|
|
335
|
+
const result = isEligibleForImmediateAutoFix(job, [job], noSiblingOnDisk);
|
|
336
|
+
assert.strictEqual(result, true);
|
|
337
|
+
});
|
|
338
|
+
|
|
233
339
|
test('isEligibleForImmediateAutoFix: fix sibling already on disk excludes the job', () => {
|
|
234
340
|
const job = makeJob();
|
|
235
341
|
// fixSlug = '05-fix-my-feature'
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-fix-plan-path.test.cjs — regression test for the self-heal
|
|
3
|
+
* dead-letter bug: spawnInvestigation used to write every auto-fix plan into
|
|
4
|
+
* the RETIRED flat `<cwd>/session-manager-operations/scheduler/prds/` dir
|
|
5
|
+
* (prdDirForCwd), which reconcile()'s consolidateAllFlatPrds sweeps BEFORE
|
|
6
|
+
* scanning for queue rows — so a freshly-authored fix plan (no queue row yet
|
|
7
|
+
* by definition) was archived into `prds-archived/` on the very next pass
|
|
8
|
+
* and never enqueued. Confirmed live in starry-night-ships: job
|
|
9
|
+
* 105-arena-perf-fences produced a valid fix plan that landed in
|
|
10
|
+
* prds-archived/ having never appeared in queue.json or history.jsonl.
|
|
11
|
+
*
|
|
12
|
+
* The fix: resolveFixPlanPath resolves the write dir from the ORIGINAL job's
|
|
13
|
+
* live PRD location (findPrdDir, via resolveVerifyPrdPath) — normally the
|
|
14
|
+
* originating Epic's own `epics/<epicId>/prds/`, which consolidateFlatPrds
|
|
15
|
+
* never touches — falling back to the flat dir only when the original can't
|
|
16
|
+
* be found live anywhere on disk.
|
|
17
|
+
*
|
|
18
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-fix-plan-path.test.cjs
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const os = require('node:os');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
let tmpHome;
|
|
29
|
+
let originalHome;
|
|
30
|
+
let resolveFixPlanPath;
|
|
31
|
+
let PRDS_DIR;
|
|
32
|
+
let consolidateFlatPrds;
|
|
33
|
+
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
originalHome = process.env.HOME;
|
|
36
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-fix-plan-path-'));
|
|
37
|
+
process.env.HOME = tmpHome;
|
|
38
|
+
|
|
39
|
+
// Load AFTER HOME is stubbed — scheduler.cjs snapshots os.homedir() into
|
|
40
|
+
// PRDS_DIR at module load, not lazily (see scheduler-find-prd-dir.test.cjs).
|
|
41
|
+
({ resolveFixPlanPath, PRDS_DIR } = require('../scheduler.cjs'));
|
|
42
|
+
({ consolidateFlatPrds } = require('../lib/prdMigration.cjs'));
|
|
43
|
+
|
|
44
|
+
if (!PRDS_DIR.startsWith(tmpHome)) {
|
|
45
|
+
throw new Error(`refusing to run: PRDS_DIR (${PRDS_DIR}) is not under the temp HOME (${tmpHome})`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// allProjectCwds()/activeProjectCwds() (consumed by findPrdDir's candidate
|
|
49
|
+
// search) discover project cwds by scanning ~/.claude/projects/*/*.jsonl
|
|
50
|
+
// for a `cwd` field — fake one project transcript pointing at the real
|
|
51
|
+
// on-disk cwd used below (mirrors scheduler-verify-prd-path.test.cjs).
|
|
52
|
+
const projectsDir = path.join(tmpHome, '.claude', 'projects');
|
|
53
|
+
const projDir = path.join(projectsDir, 'fake-fix-plan-project-slug');
|
|
54
|
+
fs.mkdirSync(projDir, { recursive: true });
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(projDir, 'session.jsonl'),
|
|
57
|
+
`${JSON.stringify({ cwd: projectCwd() })}\n`,
|
|
58
|
+
'utf8',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
process.env.HOME = originalHome;
|
|
64
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function projectCwd() {
|
|
68
|
+
return path.join(tmpHome, 'fix-plan-project-cwd');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test('resolveFixPlanPath resolves the fix plan into the SAME Epic-scoped dir as the original PRD, and it survives consolidateFlatPrds', async () => {
|
|
72
|
+
const cwd = projectCwd();
|
|
73
|
+
const epicId = 'test-epic-arena-perf';
|
|
74
|
+
const slug = `105-arena-perf-fences-${process.pid}`;
|
|
75
|
+
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId, 'prds');
|
|
76
|
+
fs.mkdirSync(prdsDir, { recursive: true });
|
|
77
|
+
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), '---\ntitle: arena perf fences\n---\n\n# Goal\n\ntest\n', 'utf8');
|
|
78
|
+
|
|
79
|
+
const failedJob = { slug, cwd, parallelGroup: 5 };
|
|
80
|
+
const resolved = await resolveFixPlanPath(failedJob);
|
|
81
|
+
|
|
82
|
+
const expectedFixSlug = '05-fix-arena-perf-fences-' + process.pid;
|
|
83
|
+
expect(resolved.fixSlug).toBe(expectedFixSlug);
|
|
84
|
+
expect(resolved.livePrdDir).toBe(prdsDir);
|
|
85
|
+
expect(resolved.fixPath).toBe(path.join(prdsDir, `${expectedFixSlug}.md`));
|
|
86
|
+
|
|
87
|
+
// A fix plan written at the resolved path must survive a consolidateFlatPrds
|
|
88
|
+
// pass over the SAME project cwd — the sweep only ever touches the flat dir.
|
|
89
|
+
fs.writeFileSync(resolved.fixPath, `---\ntitle: Fix: arena perf fences\ncwd: ${cwd}\nparallelGroup: 5\nestimateMinutes: 30\n---\n\nfix body\n`, 'utf8');
|
|
90
|
+
await consolidateFlatPrds(cwd);
|
|
91
|
+
expect(fs.existsSync(resolved.fixPath)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('resolveFixPlanPath falls back to the flat dir when the original PRD cannot be found live anywhere on disk', async () => {
|
|
95
|
+
const cwd = path.join(tmpHome, 'fix-plan-project-cwd-missing');
|
|
96
|
+
const slug = `999-nonexistent-${process.pid}`;
|
|
97
|
+
const failedJob = { slug, cwd, parallelGroup: 3 };
|
|
98
|
+
|
|
99
|
+
const resolved = await resolveFixPlanPath(failedJob);
|
|
100
|
+
|
|
101
|
+
expect(resolved.livePrdDir).toBe(null);
|
|
102
|
+
const flatDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
103
|
+
expect(resolved.fixPath).toBe(path.join(flatDir, `${resolved.fixSlug}.md`));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('the OLD flat-dir path (regression guard) does NOT survive consolidateFlatPrds — proves the bug this fix addresses', async () => {
|
|
107
|
+
const cwd = path.join(tmpHome, 'fix-plan-project-cwd-old-behavior');
|
|
108
|
+
const flatDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
109
|
+
fs.mkdirSync(flatDir, { recursive: true });
|
|
110
|
+
const oldFixPath = path.join(flatDir, '07-fix-old-behavior.md');
|
|
111
|
+
fs.writeFileSync(oldFixPath, `---\ntitle: Fix: old behavior\ncwd: ${cwd}\nparallelGroup: 7\nestimateMinutes: 30\n---\n\nfix body\n`, 'utf8');
|
|
112
|
+
|
|
113
|
+
// No queue.json at all — the file has no live queue row, exactly the
|
|
114
|
+
// freshly-authored-fix-plan situation the bug hit.
|
|
115
|
+
await consolidateFlatPrds(cwd);
|
|
116
|
+
|
|
117
|
+
expect(fs.existsSync(oldFixPath)).toBe(false);
|
|
118
|
+
expect(fs.existsSync(path.join(cwd, 'session-manager-operations', 'scheduler', 'prds-archived', '07-fix-old-behavior.md'))).toBe(true);
|
|
119
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seedAgentPersonas.test.cjs — unit tests for src/main/seedAgentPersonas.cjs.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/seedAgentPersonas.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
let tmpHome;
|
|
15
|
+
let realHome;
|
|
16
|
+
let seedAgentPersonas;
|
|
17
|
+
|
|
18
|
+
// config.cjs bakes os.homedir() into top-level consts at first require
|
|
19
|
+
// (allowedRoots/WRITE_PREFIXES, used by writeJsonSync) — must be purged
|
|
20
|
+
// alongside seedAgentPersonas.cjs so each test's tmpHome takes effect.
|
|
21
|
+
const MODULES_TO_RELOAD = ['../seedAgentPersonas.cjs', '../config.cjs'];
|
|
22
|
+
|
|
23
|
+
function purgeRequireCache() {
|
|
24
|
+
for (const m of MODULES_TO_RELOAD) {
|
|
25
|
+
try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
realHome = process.env.HOME;
|
|
31
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-agent-personas-test-'));
|
|
32
|
+
process.env.HOME = tmpHome;
|
|
33
|
+
delete process.env.SM_SEED_AGENT_PERSONAS_DISABLE;
|
|
34
|
+
purgeRequireCache();
|
|
35
|
+
({ seedAgentPersonas } = require('../seedAgentPersonas.cjs'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.env.HOME = realHome;
|
|
40
|
+
delete process.env.SM_SEED_AGENT_PERSONAS_DISABLE;
|
|
41
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
42
|
+
purgeRequireCache();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function agentsDir() {
|
|
46
|
+
return path.join(tmpHome, '.claude', 'agents');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function markerPath() {
|
|
50
|
+
return path.join(tmpHome, '.claude', 'session-manager', '.agent-personas-seeded');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function silentLogger() {
|
|
54
|
+
return { log: () => {}, warn: () => {} };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('fresh seed writes both persona files', async () => {
|
|
58
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
59
|
+
|
|
60
|
+
const architect = path.join(agentsDir(), 'architect.md');
|
|
61
|
+
const devLead = path.join(agentsDir(), 'dev-lead.md');
|
|
62
|
+
expect(fs.existsSync(architect)).toBe(true);
|
|
63
|
+
expect(fs.existsSync(devLead)).toBe(true);
|
|
64
|
+
|
|
65
|
+
const bundledArchitect = fs.readFileSync(
|
|
66
|
+
path.join(__dirname, '..', '..', 'seed', 'agents', 'architect.md'),
|
|
67
|
+
'utf8'
|
|
68
|
+
);
|
|
69
|
+
expect(fs.readFileSync(architect, 'utf8')).toBe(bundledArchitect);
|
|
70
|
+
|
|
71
|
+
const marker = JSON.parse(fs.readFileSync(markerPath(), 'utf8'));
|
|
72
|
+
expect(marker.done).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a pre-existing persona file is left byte-identical', async () => {
|
|
76
|
+
fs.mkdirSync(agentsDir(), { recursive: true });
|
|
77
|
+
const architect = path.join(agentsDir(), 'architect.md');
|
|
78
|
+
const customContent = '# My custom architect\nnever overwrite me\n';
|
|
79
|
+
fs.writeFileSync(architect, customContent);
|
|
80
|
+
|
|
81
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
82
|
+
|
|
83
|
+
expect(fs.readFileSync(architect, 'utf8')).toBe(customContent);
|
|
84
|
+
// dev-lead had no pre-existing file, so it should still be seeded.
|
|
85
|
+
expect(fs.existsSync(path.join(agentsDir(), 'dev-lead.md'))).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('done:true marker short-circuits without touching agents dir', async () => {
|
|
89
|
+
const mPath = markerPath();
|
|
90
|
+
fs.mkdirSync(path.dirname(mPath), { recursive: true });
|
|
91
|
+
fs.writeFileSync(mPath, JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
|
|
92
|
+
|
|
93
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
94
|
+
|
|
95
|
+
expect(fs.existsSync(agentsDir())).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('SM_SEED_AGENT_PERSONAS_DISABLE=1 short-circuits', async () => {
|
|
99
|
+
process.env.SM_SEED_AGENT_PERSONAS_DISABLE = '1';
|
|
100
|
+
|
|
101
|
+
await seedAgentPersonas({ logger: silentLogger() });
|
|
102
|
+
|
|
103
|
+
expect(fs.existsSync(agentsDir())).toBe(false);
|
|
104
|
+
expect(fs.existsSync(markerPath())).toBe(false);
|
|
105
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seedSchedulerMcp.test.cjs — unit tests for src/main/seedSchedulerMcp.cjs.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/seedSchedulerMcp.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
let tmpHome;
|
|
15
|
+
let realHome;
|
|
16
|
+
let seedSchedulerMcp;
|
|
17
|
+
|
|
18
|
+
// config.cjs bakes os.homedir() into top-level consts at first require
|
|
19
|
+
// (allowedRoots/WRITE_PREFIXES, used by writeJsonSync) — must be purged
|
|
20
|
+
// alongside seedSchedulerMcp.cjs so each test's tmpHome takes effect.
|
|
21
|
+
const MODULES_TO_RELOAD = ['../seedSchedulerMcp.cjs', '../config.cjs'];
|
|
22
|
+
|
|
23
|
+
function purgeRequireCache() {
|
|
24
|
+
for (const m of MODULES_TO_RELOAD) {
|
|
25
|
+
try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
realHome = process.env.HOME;
|
|
31
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-scheduler-mcp-test-'));
|
|
32
|
+
process.env.HOME = tmpHome;
|
|
33
|
+
delete process.env.SM_SEED_SCHEDULER_MCP_DISABLE;
|
|
34
|
+
purgeRequireCache();
|
|
35
|
+
({ seedSchedulerMcp } = require('../seedSchedulerMcp.cjs'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.env.HOME = realHome;
|
|
40
|
+
delete process.env.SM_SEED_SCHEDULER_MCP_DISABLE;
|
|
41
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
42
|
+
purgeRequireCache();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function markerPath() {
|
|
46
|
+
return path.join(tmpHome, '.claude', 'session-manager', '.scheduler-mcp-seeded');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function claudeJsonPath() {
|
|
50
|
+
return path.join(tmpHome, '.claude.json');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function silentLogger() {
|
|
54
|
+
return { log: () => {}, warn: () => {} };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readMarker() {
|
|
58
|
+
return JSON.parse(fs.readFileSync(markerPath(), 'utf8'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('fresh registration calls addFn with the absolute server path and writes done marker', async () => {
|
|
62
|
+
const calls = [];
|
|
63
|
+
const addFn = async (serverPath) => {
|
|
64
|
+
calls.push(serverPath);
|
|
65
|
+
return { ok: true, exitCode: 0 };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
69
|
+
|
|
70
|
+
expect(calls).toHaveLength(1);
|
|
71
|
+
expect(path.isAbsolute(calls[0])).toBe(true);
|
|
72
|
+
expect(calls[0].endsWith(path.join('scripts', 'scheduler-mcp-server.cjs'))).toBe(true);
|
|
73
|
+
|
|
74
|
+
const marker = readMarker();
|
|
75
|
+
expect(marker.done).toBe(true);
|
|
76
|
+
expect(marker.attempts).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('already-registered at user scope short-circuits without calling addFn', async () => {
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
claudeJsonPath(),
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
mcpServers: {
|
|
84
|
+
'session-manager-scheduler': { type: 'stdio', command: 'node', args: ['/some/path/scheduler-mcp-server.cjs'] },
|
|
85
|
+
fetch: { type: 'stdio', command: 'uvx', args: ['mcp-server-fetch'] },
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
let called = false;
|
|
90
|
+
const addFn = async () => { called = true; return { ok: true }; };
|
|
91
|
+
|
|
92
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
93
|
+
|
|
94
|
+
expect(called).toBe(false);
|
|
95
|
+
const marker = readMarker();
|
|
96
|
+
expect(marker.done).toBe(true);
|
|
97
|
+
|
|
98
|
+
// Untouched — the pre-existing registration (and sibling entries) stay as-is.
|
|
99
|
+
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath(), 'utf8'));
|
|
100
|
+
expect(claudeJson.mcpServers['session-manager-scheduler'].args).toEqual(['/some/path/scheduler-mcp-server.cjs']);
|
|
101
|
+
expect(claudeJson.mcpServers.fetch).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('done:true marker short-circuits without calling addFn', async () => {
|
|
105
|
+
fs.mkdirSync(path.dirname(markerPath()), { recursive: true });
|
|
106
|
+
fs.writeFileSync(markerPath(), JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
|
|
107
|
+
let called = false;
|
|
108
|
+
const addFn = async () => { called = true; return { ok: true }; };
|
|
109
|
+
|
|
110
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
111
|
+
|
|
112
|
+
expect(called).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('SM_SEED_SCHEDULER_MCP_DISABLE=1 short-circuits', async () => {
|
|
116
|
+
process.env.SM_SEED_SCHEDULER_MCP_DISABLE = '1';
|
|
117
|
+
let called = false;
|
|
118
|
+
const addFn = async () => { called = true; return { ok: true }; };
|
|
119
|
+
|
|
120
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
121
|
+
|
|
122
|
+
expect(called).toBe(false);
|
|
123
|
+
expect(fs.existsSync(markerPath())).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('a failed attempt bumps the counter and stops after MAX_ATTEMPTS', async () => {
|
|
127
|
+
const addFn = async () => ({ ok: false, error: 'boom' });
|
|
128
|
+
|
|
129
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
130
|
+
expect(readMarker()).toMatchObject({ done: false, attempts: 1 });
|
|
131
|
+
|
|
132
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
133
|
+
expect(readMarker()).toMatchObject({ done: false, attempts: 2 });
|
|
134
|
+
|
|
135
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn });
|
|
136
|
+
expect(readMarker()).toMatchObject({ done: false, attempts: 3 });
|
|
137
|
+
|
|
138
|
+
// MAX_ATTEMPTS reached — a 4th run must not call addFn again or bump further.
|
|
139
|
+
let called = false;
|
|
140
|
+
const trackedAddFn = async () => { called = true; return { ok: false, error: 'boom' }; };
|
|
141
|
+
await seedSchedulerMcp({ logger: silentLogger(), addFn: trackedAddFn });
|
|
142
|
+
|
|
143
|
+
expect(called).toBe(false);
|
|
144
|
+
expect(readMarker()).toMatchObject({ done: false, attempts: 3 });
|
|
145
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seedStatus.test.cjs — unit tests for src/main/seedStatus.cjs.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/seedStatus.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
let tmpHome;
|
|
15
|
+
let realHome;
|
|
16
|
+
let getSeedStatus;
|
|
17
|
+
|
|
18
|
+
// config.cjs (via seedSchedulerMcp/seedAgentPersonas) and each seed*.cjs
|
|
19
|
+
// module bake os.homedir() into top-level consts at first require — purge
|
|
20
|
+
// all of them alongside seedStatus.cjs so each test's tmpHome takes effect.
|
|
21
|
+
const MODULES_TO_RELOAD = [
|
|
22
|
+
'../seedStatus.cjs',
|
|
23
|
+
'../seedDevPlugin.cjs',
|
|
24
|
+
'../seedSchedulerMcp.cjs',
|
|
25
|
+
'../seedAgentPersonas.cjs',
|
|
26
|
+
'../config.cjs',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function purgeRequireCache() {
|
|
30
|
+
for (const m of MODULES_TO_RELOAD) {
|
|
31
|
+
try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
realHome = process.env.HOME;
|
|
37
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-status-test-'));
|
|
38
|
+
process.env.HOME = tmpHome;
|
|
39
|
+
purgeRequireCache();
|
|
40
|
+
({ getSeedStatus } = require('../seedStatus.cjs'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
process.env.HOME = realHome;
|
|
45
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
46
|
+
purgeRequireCache();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const MARKER_NAMES = {
|
|
50
|
+
'dev-plugin': '.dev-plugin-seeded',
|
|
51
|
+
'scheduler-mcp': '.scheduler-mcp-seeded',
|
|
52
|
+
'agent-personas': '.agent-personas-seeded',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function markerPath(id) {
|
|
56
|
+
return path.join(tmpHome, '.claude', 'session-manager', MARKER_NAMES[id]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeMarker(id, contents) {
|
|
60
|
+
const p = markerPath(id);
|
|
61
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
62
|
+
fs.writeFileSync(p, contents);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test('no marker files at all → every seeder reports pending with a fix string', () => {
|
|
66
|
+
const status = getSeedStatus();
|
|
67
|
+
for (const id of Object.keys(MARKER_NAMES)) {
|
|
68
|
+
expect(status[id].status).toBe('pending');
|
|
69
|
+
expect(typeof status[id].fix).toBe('string');
|
|
70
|
+
expect(status[id].fix.length).toBeGreaterThan(0);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (const id of Object.keys(MARKER_NAMES)) {
|
|
75
|
+
test(`${id}: done:true marker → done`, () => {
|
|
76
|
+
writeMarker(id, JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
|
|
77
|
+
expect(getSeedStatus()[id].status).toBe('done');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test(`${id}: done:false, attempts below max → pending`, () => {
|
|
81
|
+
writeMarker(id, JSON.stringify({ done: false, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
|
|
82
|
+
expect(getSeedStatus()[id].status).toBe('pending');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test(`${id}: done:false, attempts >= max → exhausted`, () => {
|
|
86
|
+
writeMarker(id, JSON.stringify({ done: false, attempts: 3, ts: '2026-01-01T00:00:00.000Z' }));
|
|
87
|
+
expect(getSeedStatus()[id].status).toBe('exhausted');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test(`${id}: unparseable marker → pending, never throws`, () => {
|
|
91
|
+
writeMarker(id, 'not json at all {{{');
|
|
92
|
+
expect(() => getSeedStatus()).not.toThrow();
|
|
93
|
+
expect(getSeedStatus()[id].status).toBe('pending');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test(`${id}: missing marker file → pending`, () => {
|
|
97
|
+
// No file written for this id — directory may not even exist.
|
|
98
|
+
expect(getSeedStatus()[id].status).toBe('pending');
|
|
99
|
+
});
|
|
100
|
+
}
|