claude-code-session-manager 0.86.0 → 0.87.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/dist/assets/AgentLibrary-DyLWzZDf.js +3 -0
- package/dist/assets/{DataModel-Q4jhl24R.js → DataModel--mISIJ6h.js} +1 -1
- package/dist/assets/{History-Cj2FejEo.js → History-C2ahUXTg.js} +2 -2
- package/dist/assets/{Hooks-CaelQI6t.js → Hooks-BiC6oyR2.js} +3 -3
- package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-BPleEOld.js} +1 -1
- package/dist/assets/{Library-DgI9oCCZ.js → Library-Dc8Qst1R.js} +1 -1
- package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-DIXh-OLX.js} +1 -1
- package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
- package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DqcbLOLZ.js} +2 -2
- package/dist/assets/{Memory-C2qYp-3M.js → Memory-CW62MXlh.js} +4 -4
- package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-Bw1FhRuF.js} +1 -1
- package/dist/assets/Permissions-BcUC-5y8.js +3 -0
- package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-BnKx9flD.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
- package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-CWr0O_w-.js} +1 -1
- package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
- package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
- package/dist/assets/Settings-DluB-vN1.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-CHLSseay.js} +1 -1
- package/dist/assets/{Skills-C_YHkAy-.js → Skills-gNdo_HNK.js} +2 -2
- package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Cru05-Ia.js} +1 -1
- package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-DNHY0xou.js} +1 -1
- package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-I4lmbCgP.js} +1 -1
- package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-bWMHjmRh.js} +1 -1
- package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
- package/dist/assets/{index-CXFQIPhO.js → index-fc_JjdxL.js} +724 -724
- package/dist/assets/settingsSchema-BfhtZnGD.js +3 -0
- package/dist/index.html +2 -2
- package/package.json +14 -14
- package/plugins/CLAUDE.md +61 -0
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
- package/scripts/scheduler-mcp-server.cjs +7 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
- package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
- package/src/main/__tests__/epicMint.test.cjs +2 -2
- package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
- package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +3 -3
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +15 -1
- package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
- package/src/main/__tests__/prdLocations.test.cjs +100 -2
- package/src/main/__tests__/prdLocationsArchived.test.cjs +43 -1
- package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
- package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
- package/src/main/__tests__/queueHistory.test.cjs +63 -0
- package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +101 -1
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +2 -2
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +344 -0
- package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +93 -3
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +160 -0
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +88 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +14 -4
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
- package/src/main/chatRunner.cjs +8 -5
- package/src/main/health.cjs +1 -1
- package/src/main/historyAggregator.cjs +5 -0
- package/src/main/index.cjs +95 -44
- package/src/main/ipcSchemas.cjs +47 -0
- package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +322 -43
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +89 -0
- package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
- package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
- package/src/main/lib/__tests__/usageCircuit.test.cjs +224 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +312 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +193 -0
- package/{scripts → src/main}/lib/activeSessions.cjs +50 -4
- package/src/main/lib/agentModelResolve.cjs +65 -27
- package/src/main/lib/bootSelfHeal.cjs +88 -0
- package/src/main/lib/delegationReadiness.cjs +290 -225
- package/src/main/lib/effectiveModelInfo.cjs +333 -0
- package/src/main/lib/ephemeralCwd.cjs +1 -1
- package/src/main/lib/epicMint.cjs +3 -3
- package/src/main/lib/gitWorktree.cjs +42 -12
- package/src/main/lib/guardShims.cjs +156 -0
- package/src/main/lib/jobDirtFilter.cjs +7 -2
- package/src/main/lib/launchFailure.cjs +2 -1
- package/src/main/lib/mcpToolCatalog.cjs +4 -1
- package/src/main/lib/needsReviewLedger.cjs +205 -0
- package/src/main/lib/opsErrorLog.cjs +1 -1
- package/src/main/lib/opsOwnership.cjs +1 -1
- package/src/main/lib/prdCreate.cjs +56 -1
- package/src/main/lib/prdDisposition.cjs +199 -0
- package/src/main/lib/prdFrontmatter.cjs +8 -2
- package/src/main/lib/prdLocations.cjs +167 -45
- package/src/main/lib/projectHomeAdminRoutes.cjs +4 -4
- package/src/main/lib/projectPageSummarySchema.cjs +1 -1
- package/src/main/lib/projectRootResolve.cjs +1 -1
- package/src/main/lib/queueHistory.cjs +19 -1
- package/src/main/lib/queueStore.cjs +6 -1
- package/src/main/lib/reaperHelpers.cjs +181 -15
- package/src/main/lib/scheduleJobSchema.cjs +8 -0
- package/src/main/lib/scheduleJobTransitions.cjs +33 -0
- package/src/main/lib/schedulerConfig.cjs +24 -0
- package/src/main/lib/usageCircuit.cjs +159 -0
- package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
- package/src/main/scheduler/prdParser.cjs +13 -0
- package/src/main/scheduler.cjs +1285 -143
- package/src/main/templates/PRD_AUTHORING.md +50 -0
- package/src/main/templates/project-pages-catalog.json +1 -1
- package/src/main/usage.cjs +21 -3
- package/src/preload/api.d.ts +92 -1
- package/src/preload/index.cjs +10 -0
- package/web/README.md +41 -0
- package/{scripts/render-project-pages.cjs → web/project-pages/render.cjs} +4 -4
- package/{scripts/render-project-pages → web/project-pages/renderer}/dist/renderer.cjs +1 -1
- package/{scripts/validate-project-pages-summary.cjs → web/project-pages/validate-summary.cjs} +5 -5
- package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
- package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
- package/dist/assets/Permissions-BiYZNGYW.js +0 -3
- package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
- package/dist/assets/Settings-Cv-pRyms.js +0 -3
- package/dist/assets/settingsSchema-BJVciriw.js +0 -3
- /package/{scripts/project-pages-logic → web/project-pages/logic}/dist/logic.cjs +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Run: timeout 120 npx vitest run src/main/lib/__tests__/active-sessions.test.cjs
|
|
4
|
+
|
|
5
|
+
import { vi } from 'vitest';
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { activeProjectCwds, allProjectCwds, bustProjectCwdCache } = require('../activeSessions.cjs');
|
|
11
|
+
|
|
12
|
+
function tmpDir() {
|
|
13
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'active-sessions-test-'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeTranscript(projectsDir, slug, fileName, cwd, mtimeMsAgo) {
|
|
17
|
+
const projDir = path.join(projectsDir, slug);
|
|
18
|
+
fs.mkdirSync(projDir, { recursive: true });
|
|
19
|
+
const fp = path.join(projDir, fileName);
|
|
20
|
+
fs.writeFileSync(fp, JSON.stringify({ cwd }) + '\n');
|
|
21
|
+
const mtime = new Date(Date.now() - mtimeMsAgo);
|
|
22
|
+
fs.utimesSync(fp, mtime, mtime);
|
|
23
|
+
return fp;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('two recent + one stale + one nonexistent → returns exactly the two recent existing cwds', () => {
|
|
27
|
+
const base = tmpDir();
|
|
28
|
+
try {
|
|
29
|
+
const projectsDir = path.join(base, 'projects');
|
|
30
|
+
const cwdA = path.join(base, 'proj-a');
|
|
31
|
+
const cwdB = path.join(base, 'proj-b');
|
|
32
|
+
const cwdC = path.join(base, 'proj-c'); // stale — exists on disk
|
|
33
|
+
const cwdMissing = path.join(base, 'proj-missing'); // never created
|
|
34
|
+
fs.mkdirSync(cwdA);
|
|
35
|
+
fs.mkdirSync(cwdB);
|
|
36
|
+
fs.mkdirSync(cwdC);
|
|
37
|
+
|
|
38
|
+
const MAX_AGE_MIN = 90;
|
|
39
|
+
writeTranscript(projectsDir, 'slug-a', 'a.jsonl', cwdA, 10 * 60 * 1000);
|
|
40
|
+
writeTranscript(projectsDir, 'slug-b', 'b.jsonl', cwdB, 10 * 60 * 1000);
|
|
41
|
+
writeTranscript(projectsDir, 'slug-c', 'c.jsonl', cwdC, 120 * 60 * 1000);
|
|
42
|
+
writeTranscript(projectsDir, 'slug-missing', 'm.jsonl', cwdMissing, 10 * 60 * 1000);
|
|
43
|
+
|
|
44
|
+
const result = activeProjectCwds(MAX_AGE_MIN, { projectsDir });
|
|
45
|
+
|
|
46
|
+
assert.equal(
|
|
47
|
+
result.length, 2,
|
|
48
|
+
`expected 2 cwds, got ${result.length}: ${JSON.stringify(result)}`,
|
|
49
|
+
);
|
|
50
|
+
assert.ok(result.includes(cwdA), `cwdA must be in result`);
|
|
51
|
+
assert.ok(result.includes(cwdB), `cwdB must be in result`);
|
|
52
|
+
assert.ok(!result.includes(cwdC), 'stale cwdC must not appear');
|
|
53
|
+
assert.ok(!result.includes(cwdMissing), 'nonexistent path must not appear');
|
|
54
|
+
} finally {
|
|
55
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('duplicate cwd across two project dirs → deduped to one entry', () => {
|
|
60
|
+
const base = tmpDir();
|
|
61
|
+
try {
|
|
62
|
+
const projectsDir = path.join(base, 'projects');
|
|
63
|
+
const cwdA = path.join(base, 'proj-a');
|
|
64
|
+
fs.mkdirSync(cwdA);
|
|
65
|
+
|
|
66
|
+
writeTranscript(projectsDir, 'slug-1', 'a.jsonl', cwdA, 5 * 60 * 1000);
|
|
67
|
+
writeTranscript(projectsDir, 'slug-2', 'b.jsonl', cwdA, 5 * 60 * 1000);
|
|
68
|
+
|
|
69
|
+
const result = activeProjectCwds(90, { projectsDir });
|
|
70
|
+
assert.equal(result.length, 1, 'duplicate cwd must be deduped');
|
|
71
|
+
assert.equal(result[0], cwdA);
|
|
72
|
+
} finally {
|
|
73
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('missing projects dir → empty result', () => {
|
|
78
|
+
const base = tmpDir();
|
|
79
|
+
try {
|
|
80
|
+
const result = activeProjectCwds(90, { projectsDir: path.join(base, 'no-such-dir') });
|
|
81
|
+
assert.deepEqual(result, []);
|
|
82
|
+
} finally {
|
|
83
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('prompts.jsonl absent → transcripts still detected (sole detection path)', () => {
|
|
88
|
+
const base = tmpDir();
|
|
89
|
+
try {
|
|
90
|
+
// No knowledge-log / prompts.jsonl anywhere in this tmp tree — the
|
|
91
|
+
// function must not need it. logPath, if passed, is a harmless no-op.
|
|
92
|
+
const projectsDir = path.join(base, 'projects');
|
|
93
|
+
const cwdA = path.join(base, 'proj-a');
|
|
94
|
+
fs.mkdirSync(cwdA);
|
|
95
|
+
|
|
96
|
+
writeTranscript(projectsDir, 'slug-a', 'a.jsonl', cwdA, 5 * 60 * 1000);
|
|
97
|
+
|
|
98
|
+
const missingLogPath = path.join(base, 'knowledge-log', 'prompts.jsonl');
|
|
99
|
+
assert.ok(!fs.existsSync(missingLogPath), 'sanity: log path must not exist');
|
|
100
|
+
|
|
101
|
+
const result = activeProjectCwds(90, { projectsDir, logPath: missingLogPath });
|
|
102
|
+
assert.deepEqual(result, [cwdA]);
|
|
103
|
+
} finally {
|
|
104
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('fresh transcript mtime with no prompt log present → cwd is returned', () => {
|
|
109
|
+
const base = tmpDir();
|
|
110
|
+
try {
|
|
111
|
+
const projectsDir = path.join(base, 'projects');
|
|
112
|
+
const cwdA = path.join(base, 'proj-fresh');
|
|
113
|
+
fs.mkdirSync(cwdA);
|
|
114
|
+
|
|
115
|
+
writeTranscript(projectsDir, 'slug-fresh', 'fresh.jsonl', cwdA, 60 * 1000); // 1 min ago
|
|
116
|
+
|
|
117
|
+
const result = activeProjectCwds(90, { projectsDir });
|
|
118
|
+
assert.equal(result.length, 1);
|
|
119
|
+
assert.equal(result[0], cwdA);
|
|
120
|
+
} finally {
|
|
121
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('garbage lines in transcript are skipped, cwd read from last parseable line', () => {
|
|
126
|
+
const base = tmpDir();
|
|
127
|
+
try {
|
|
128
|
+
const projectsDir = path.join(base, 'projects');
|
|
129
|
+
const cwdA = path.join(base, 'proj-a');
|
|
130
|
+
fs.mkdirSync(cwdA);
|
|
131
|
+
|
|
132
|
+
const projDir = path.join(projectsDir, 'slug-a');
|
|
133
|
+
fs.mkdirSync(projDir, { recursive: true });
|
|
134
|
+
const fp = path.join(projDir, 'a.jsonl');
|
|
135
|
+
fs.writeFileSync(fp, [
|
|
136
|
+
JSON.stringify({ cwd: cwdA }),
|
|
137
|
+
'not json at all!!!',
|
|
138
|
+
'{ broken',
|
|
139
|
+
].join('\n') + '\n');
|
|
140
|
+
const mtime = new Date(Date.now() - 5 * 60 * 1000);
|
|
141
|
+
fs.utimesSync(fp, mtime, mtime);
|
|
142
|
+
|
|
143
|
+
const result = activeProjectCwds(90, { projectsDir });
|
|
144
|
+
assert.equal(result.length, 1);
|
|
145
|
+
assert.equal(result[0], cwdA);
|
|
146
|
+
} finally {
|
|
147
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('5 consecutive allProjectCwds() calls with the same args perform ONE directory scan', () => {
|
|
152
|
+
const base = tmpDir();
|
|
153
|
+
try {
|
|
154
|
+
const projectsDir = path.join(base, 'projects');
|
|
155
|
+
const cwdA = path.join(base, 'proj-a');
|
|
156
|
+
fs.mkdirSync(cwdA);
|
|
157
|
+
writeTranscript(projectsDir, 'slug-a', 'a.jsonl', cwdA, 5 * 60 * 1000);
|
|
158
|
+
|
|
159
|
+
bustProjectCwdCache();
|
|
160
|
+
const readdirSpy = vi.spyOn(fs, 'readdirSync');
|
|
161
|
+
|
|
162
|
+
const first = allProjectCwds({ projectsDir });
|
|
163
|
+
const callsAfterFirst = readdirSpy.mock.calls.length;
|
|
164
|
+
assert.ok(callsAfterFirst > 0, 'first call must actually scan');
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < 4; i++) {
|
|
167
|
+
const again = allProjectCwds({ projectsDir });
|
|
168
|
+
assert.deepEqual(again, first);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
assert.equal(
|
|
172
|
+
readdirSpy.mock.calls.length, callsAfterFirst,
|
|
173
|
+
'the 4 repeat calls must not perform any additional directory scan',
|
|
174
|
+
);
|
|
175
|
+
} finally {
|
|
176
|
+
vi.restoreAllMocks();
|
|
177
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('bustProjectCwdCache() forces the very next call to rescan', () => {
|
|
182
|
+
const base = tmpDir();
|
|
183
|
+
try {
|
|
184
|
+
const projectsDir = path.join(base, 'projects');
|
|
185
|
+
const cwdA = path.join(base, 'proj-a');
|
|
186
|
+
fs.mkdirSync(cwdA);
|
|
187
|
+
writeTranscript(projectsDir, 'slug-a', 'a.jsonl', cwdA, 5 * 60 * 1000);
|
|
188
|
+
|
|
189
|
+
bustProjectCwdCache();
|
|
190
|
+
allProjectCwds({ projectsDir }); // warm the cache
|
|
191
|
+
|
|
192
|
+
const readdirSpy = vi.spyOn(fs, 'readdirSync');
|
|
193
|
+
allProjectCwds({ projectsDir });
|
|
194
|
+
assert.equal(readdirSpy.mock.calls.length, 0, 'a cache hit must not scan');
|
|
195
|
+
|
|
196
|
+
bustProjectCwdCache();
|
|
197
|
+
allProjectCwds({ projectsDir });
|
|
198
|
+
assert.ok(readdirSpy.mock.calls.length > 0, 'a bust must force a rescan on the next call');
|
|
199
|
+
} finally {
|
|
200
|
+
vi.restoreAllMocks();
|
|
201
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('allProjectCwds() and activeProjectCwds(90) do not share a cache entry', () => {
|
|
206
|
+
const base = tmpDir();
|
|
207
|
+
try {
|
|
208
|
+
const projectsDir = path.join(base, 'projects');
|
|
209
|
+
const cwdFresh = path.join(base, 'proj-fresh');
|
|
210
|
+
const cwdStale = path.join(base, 'proj-stale');
|
|
211
|
+
fs.mkdirSync(cwdFresh);
|
|
212
|
+
fs.mkdirSync(cwdStale);
|
|
213
|
+
writeTranscript(projectsDir, 'slug-fresh', 'fresh.jsonl', cwdFresh, 5 * 60 * 1000);
|
|
214
|
+
writeTranscript(projectsDir, 'slug-stale', 'stale.jsonl', cwdStale, 120 * 60 * 1000);
|
|
215
|
+
|
|
216
|
+
bustProjectCwdCache();
|
|
217
|
+
// allProjectCwds (maxAgeMin Infinity, maxCwds 500) sees both — cached
|
|
218
|
+
// first under its own key.
|
|
219
|
+
const all = allProjectCwds({ projectsDir });
|
|
220
|
+
assert.ok(all.includes(cwdFresh) && all.includes(cwdStale));
|
|
221
|
+
|
|
222
|
+
// activeProjectCwds(90) must recompute under its OWN key, not return
|
|
223
|
+
// allProjectCwds' cached (Infinity, 500) result — proving the cache key
|
|
224
|
+
// includes maxAgeMin/maxCwds, not just projectsDir.
|
|
225
|
+
const active = activeProjectCwds(90, { projectsDir });
|
|
226
|
+
assert.ok(active.includes(cwdFresh), 'fresh cwd must be present');
|
|
227
|
+
assert.ok(!active.includes(cwdStale), 'stale cwd must be excluded by the 90-min filter');
|
|
228
|
+
} finally {
|
|
229
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('missing projectsDir returns [] and does not poison the cache once the dir appears', () => {
|
|
234
|
+
const base = tmpDir();
|
|
235
|
+
try {
|
|
236
|
+
const projectsDir = path.join(base, 'projects');
|
|
237
|
+
bustProjectCwdCache();
|
|
238
|
+
|
|
239
|
+
assert.deepEqual(activeProjectCwds(90, { projectsDir }), []);
|
|
240
|
+
assert.deepEqual(activeProjectCwds(90, { projectsDir }), []);
|
|
241
|
+
|
|
242
|
+
const cwdA = path.join(base, 'proj-a');
|
|
243
|
+
fs.mkdirSync(cwdA);
|
|
244
|
+
writeTranscript(projectsDir, 'slug-a', 'a.jsonl', cwdA, 5 * 60 * 1000);
|
|
245
|
+
|
|
246
|
+
const result = activeProjectCwds(90, { projectsDir });
|
|
247
|
+
assert.deepEqual(result, [cwdA], 'the dir appearing later must be visible, not stuck on a cached []');
|
|
248
|
+
} finally {
|
|
249
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bootSelfHeal.test.cjs — the epic-index self-heal scan must be SCHEDULED
|
|
3
|
+
* off app.whenReady()'s synchronous critical path, not executed inline, so
|
|
4
|
+
* the ~270ms allProjectCwds() scan can't compete with the renderer's first
|
|
5
|
+
* IPC round trips (schedule.state, billing.fetch, teams.list) for the event
|
|
6
|
+
* loop at boot.
|
|
7
|
+
*
|
|
8
|
+
* Run: timeout 300 npx vitest run src/main/lib/__tests__/bootSelfHeal.test.cjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
import { test, expect, vi } from 'vitest';
|
|
14
|
+
const { EventEmitter } = require('node:events');
|
|
15
|
+
const { scheduleBootSelfHeal, runEpicIndexSelfHeal } = require('../bootSelfHeal.cjs');
|
|
16
|
+
|
|
17
|
+
function flush(times = 2) {
|
|
18
|
+
return (async () => {
|
|
19
|
+
for (let i = 0; i < times; i++) {
|
|
20
|
+
await new Promise((resolve) => { setImmediate(resolve); });
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeDeps() {
|
|
26
|
+
return {
|
|
27
|
+
fs: { existsSync: vi.fn(() => true), readFileSync: vi.fn(() => '{}') },
|
|
28
|
+
allProjectCwds: vi.fn(() => ['/proj-a']),
|
|
29
|
+
promptSessionsActiveIndexPath: (cwd) => `${cwd}/active-index.json`,
|
|
30
|
+
rebuildActiveIndex: vi.fn(() => ({ rows: [], skipped: [] })),
|
|
31
|
+
logs: { writeLine: vi.fn() },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fakeWindow() {
|
|
36
|
+
const webContents = new EventEmitter();
|
|
37
|
+
webContents.isDestroyed = () => false;
|
|
38
|
+
return { isDestroyed: () => false, webContents };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('scheduleBootSelfHeal does not run the scan synchronously — only after did-finish-load', async () => {
|
|
42
|
+
const deps = makeDeps();
|
|
43
|
+
const mainWindow = fakeWindow();
|
|
44
|
+
|
|
45
|
+
scheduleBootSelfHeal(mainWindow, deps);
|
|
46
|
+
expect(deps.allProjectCwds).not.toHaveBeenCalled();
|
|
47
|
+
|
|
48
|
+
mainWindow.webContents.emit('did-finish-load');
|
|
49
|
+
expect(deps.allProjectCwds).toHaveBeenCalledTimes(1);
|
|
50
|
+
await flush();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('a window destroyed before did-finish-load falls back to an unconditional setImmediate run', async () => {
|
|
54
|
+
const deps = makeDeps();
|
|
55
|
+
const mainWindow = fakeWindow();
|
|
56
|
+
|
|
57
|
+
scheduleBootSelfHeal(mainWindow, deps);
|
|
58
|
+
mainWindow.webContents.emit('destroyed');
|
|
59
|
+
expect(deps.allProjectCwds).not.toHaveBeenCalled();
|
|
60
|
+
|
|
61
|
+
await flush();
|
|
62
|
+
expect(deps.allProjectCwds).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('a headless / no-window boot still self-heals via setImmediate', async () => {
|
|
66
|
+
const deps = makeDeps();
|
|
67
|
+
|
|
68
|
+
scheduleBootSelfHeal(null, deps);
|
|
69
|
+
expect(deps.allProjectCwds).not.toHaveBeenCalled();
|
|
70
|
+
|
|
71
|
+
await flush();
|
|
72
|
+
expect(deps.allProjectCwds).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('runs exactly once per boot even if did-finish-load and destroyed both fire', async () => {
|
|
76
|
+
const deps = makeDeps();
|
|
77
|
+
const mainWindow = fakeWindow();
|
|
78
|
+
|
|
79
|
+
scheduleBootSelfHeal(mainWindow, deps);
|
|
80
|
+
mainWindow.webContents.emit('did-finish-load');
|
|
81
|
+
mainWindow.webContents.emit('destroyed');
|
|
82
|
+
await flush();
|
|
83
|
+
|
|
84
|
+
expect(deps.allProjectCwds).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('runEpicIndexSelfHeal rebuilds only a project whose index is missing or unparseable — a clean index is never rewritten', async () => {
|
|
88
|
+
const files = {
|
|
89
|
+
'/clean-proj/active-index.json': '{"sessions":{}}',
|
|
90
|
+
'/broken-proj/active-index.json': '{ not json',
|
|
91
|
+
};
|
|
92
|
+
const fs = {
|
|
93
|
+
existsSync: (p) => Object.prototype.hasOwnProperty.call(files, p),
|
|
94
|
+
readFileSync: (p) => files[p],
|
|
95
|
+
};
|
|
96
|
+
const rebuildActiveIndex = vi.fn(() => ({ rows: [], skipped: [] }));
|
|
97
|
+
const logs = { writeLine: vi.fn() };
|
|
98
|
+
const allProjectCwds = () => ['/clean-proj', '/broken-proj', '/missing-proj'];
|
|
99
|
+
const promptSessionsActiveIndexPath = (cwd) => `${cwd}/active-index.json`;
|
|
100
|
+
|
|
101
|
+
await runEpicIndexSelfHeal({ fs, allProjectCwds, promptSessionsActiveIndexPath, rebuildActiveIndex, logs });
|
|
102
|
+
|
|
103
|
+
expect(rebuildActiveIndex).toHaveBeenCalledTimes(2);
|
|
104
|
+
expect(rebuildActiveIndex).toHaveBeenCalledWith('/broken-proj');
|
|
105
|
+
expect(rebuildActiveIndex).toHaveBeenCalledWith('/missing-proj');
|
|
106
|
+
expect(rebuildActiveIndex).not.toHaveBeenCalledWith('/clean-proj');
|
|
107
|
+
});
|