claude-code-session-manager 0.75.3 → 0.77.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-CzQqcObq.js → AgentLibrary-B2ie8bbw.js} +2 -2
- package/dist/assets/{DataModel-Bj_WlLz8.js → DataModel-BIJPYw32.js} +1 -1
- package/dist/assets/{History-DnSi_OHm.js → History-CeY6dk9S.js} +2 -2
- package/dist/assets/{Hooks-0BB0dp3S.js → Hooks-BFH2ocKg.js} +2 -2
- package/dist/assets/{HostBilko-DHpwwsLQ.js → HostBilko-36gj9wLz.js} +1 -1
- package/dist/assets/{Library-CaJVqVvi.js → Library-C-hBct39.js} +1 -1
- package/dist/assets/{ListDetail-C1W2HmC2.js → ListDetail-CNq64VWV.js} +1 -1
- package/dist/assets/{MarkdownEditor-5Ob9FW3z.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
- package/dist/assets/{McpServers-JxCSfm1S.js → McpServers-DpGN0oyz.js} +1 -1
- package/dist/assets/{Memory-BDeqlqwH.js → Memory-D59hUjC4.js} +6 -6
- package/dist/assets/{Panel-Dh9ZHuEj.js → Panel-DCgbaoci.js} +1 -1
- package/dist/assets/{Permissions-DXy-CbEY.js → Permissions-DAmQ0DYV.js} +2 -2
- package/dist/assets/{Plugins-_n1Iuc8T.js → Plugins-Dyfgn6Is.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BP_evfxE.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
- package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
- package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
- package/dist/assets/{ScopeSwitcher-CAWzM6RI.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
- package/dist/assets/{Settings-DRRozLyT.js → Settings-BL4vf5aX.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-DGHDWlz4.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
- package/dist/assets/{Skills-D8L66eiX.js → Skills-BV08gDUH.js} +2 -2
- package/dist/assets/{SystemPrompt-CYtUsonD.js → SystemPrompt-CLftSsDw.js} +1 -1
- package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
- package/dist/assets/{TiptapBody-B2hRgbPE.js → TiptapBody-jCpuB6E5.js} +1 -1
- package/dist/assets/{Toggle-BTwsbxam.js → Toggle-D2paA1xf.js} +1 -1
- package/dist/assets/{index-DijufvkJ.js → index-BDRSqBl3.js} +704 -704
- package/dist/assets/{index-CMLnzdZC.css → index-CYhdtisq.css} +1 -1
- package/dist/assets/{settingsSchema-D6wzxAi6.js → settingsSchema-6IOLjZZN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -2
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/scripts/lib/activeSessions.cjs +116 -6
- package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
- package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
- package/scripts/render-project-pages.cjs +70 -0
- package/scripts/scheduler-mcp-server.cjs +269 -96
- package/scripts/validate-project-pages-summary.cjs +62 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
- package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +106 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
- package/src/main/__tests__/prdAgentType.test.cjs +103 -0
- package/src/main/__tests__/prdCreate.test.cjs +247 -0
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
- package/src/main/__tests__/projectPages.test.cjs +73 -1
- package/src/main/__tests__/rcaReport.test.cjs +54 -0
- package/src/main/__tests__/runVerify.test.cjs +94 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +58 -3
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +207 -1
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
- package/src/main/bilkoHost.cjs +4 -3
- package/src/main/chatRunner.cjs +6 -1
- package/src/main/config.cjs +25 -33
- package/src/main/health.cjs +153 -2
- package/src/main/index.cjs +64 -5
- package/src/main/ipcSchemas.cjs +69 -1
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
- package/src/main/lib/__tests__/childWithLog.test.cjs +141 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +391 -42
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +5 -3
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +290 -5
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
- package/src/main/lib/__tests__/jobWorktree.test.cjs +6 -4
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
- package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
- package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +102 -0
- package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +151 -0
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
- package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +133 -0
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
- package/src/main/lib/activeIndexMerge.cjs +15 -0
- package/src/main/lib/activeIndexRebuild.cjs +133 -0
- package/src/main/lib/agentModelResolve.cjs +58 -0
- package/src/main/lib/buildTarget.cjs +3 -2
- package/src/main/lib/childWithLog.cjs +69 -2
- package/src/main/lib/claudeBin.cjs +54 -1
- package/src/main/lib/crossProjectFeedback.cjs +8 -1
- package/src/main/lib/definitionOfDone.cjs +3 -2
- package/src/main/lib/delegationReadiness.cjs +514 -26
- package/src/main/lib/ephemeralCwd.cjs +78 -0
- package/src/main/lib/epicDelegationStats.cjs +2 -1
- package/src/main/lib/epicMint.cjs +17 -1
- package/src/main/lib/epicStatusMirror.cjs +95 -0
- package/src/main/lib/epicValidationHook.cjs +2 -1
- package/src/main/lib/epicWorktreeMint.cjs +5 -2
- package/src/main/lib/fixChainDepth.cjs +45 -0
- package/src/main/lib/gitWorktree.cjs +520 -21
- package/src/main/lib/jobWorktree.cjs +2 -0
- package/src/main/lib/landedSinceRun.cjs +55 -0
- package/src/main/lib/launchFailure.cjs +357 -0
- package/src/main/lib/loadGate.cjs +134 -0
- package/src/main/lib/mcpToolCatalog.cjs +370 -0
- package/src/main/lib/opsErrorLog.cjs +12 -1
- package/src/main/lib/opsOwnership.cjs +106 -0
- package/src/main/lib/prdAdminRoutes.cjs +43 -3
- package/src/main/lib/prdAgentType.cjs +84 -0
- package/src/main/lib/prdCreate.cjs +103 -15
- package/src/main/lib/prdDeclaredPaths.cjs +70 -0
- package/src/main/lib/prdFrontmatter.cjs +17 -3
- package/src/main/lib/prdLocations.cjs +13 -6
- package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
- package/src/main/lib/projectPageSummarySchema.cjs +181 -0
- package/src/main/lib/projectRootResolve.cjs +134 -0
- package/src/main/lib/promptSessionSchema.cjs +7 -0
- package/src/main/lib/queueHealth.cjs +38 -0
- package/src/main/lib/queueStore.cjs +40 -7
- package/src/main/lib/quietMachineLease.cjs +48 -0
- package/src/main/lib/rcaReport.cjs +54 -4
- package/src/main/lib/reaperHelpers.cjs +64 -1
- package/src/main/lib/scheduleJobSchema.cjs +31 -0
- package/src/main/lib/scheduleJobTransitions.cjs +6 -2
- package/src/main/lib/schedulerBatch.cjs +301 -55
- package/src/main/lib/schedulerConfig.cjs +99 -0
- package/src/main/projectBrief.cjs +3 -2
- package/src/main/projectPages.cjs +162 -3
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/pty.cjs +5 -0
- package/src/main/queueOps.cjs +15 -8
- package/src/main/runVerify.cjs +50 -9
- package/src/main/scheduler/prdParser.cjs +18 -1
- package/src/main/scheduler.cjs +1701 -130
- package/src/main/seedAgentPersonas.cjs +62 -21
- package/src/main/seedSchedulerMcp.cjs +58 -4
- package/src/main/templates/project-pages-catalog.json +741 -0
- package/src/main/templates/project-pages-pipeline.md +417 -0
- package/src/preload/api.d.ts +187 -3
- package/src/preload/index.cjs +9 -0
- package/src/seed/agents/project-home-builder.md +59 -0
- package/dist/assets/SaveBar-D-gCUx4n.js +0 -1
- package/dist/assets/Scheduler-Bpd4OGju.js +0 -14
- package/dist/assets/TagLibrary-E5CLeuVk.js +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin CLI wrapper around validateProjectPageSummary (src/renderer/lib/
|
|
3
|
+
// projectPages/summaryValidate.ts), compiled into
|
|
4
|
+
// scripts/project-pages-logic/dist/logic.cjs by `npm run
|
|
5
|
+
// build:project-pages-logic`. The project-home-builder Epic runs this
|
|
6
|
+
// against its own summary.json before proceeding to Stage 2/3 (see
|
|
7
|
+
// .claude/agents/project-home-builder.md).
|
|
8
|
+
//
|
|
9
|
+
// Usage: node scripts/validate-project-pages-summary.cjs <summary.json path>
|
|
10
|
+
// Exits 0 and prints "valid" on success; exits 1 and prints each error on
|
|
11
|
+
// failure. Same CLI-wrapper pattern as scripts/render-project-pages.cjs.
|
|
12
|
+
//
|
|
13
|
+
// The path argument is used as-is (no allowedRoots validation like
|
|
14
|
+
// config.cjs's validatePath) — this is a standalone local dev/ops script run
|
|
15
|
+
// by a trusted human or agent shell, not an IPC-reachable surface, so it's
|
|
16
|
+
// out of scope for that guard by design.
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
function fail(message) {
|
|
23
|
+
console.error(`validate-project-pages-summary: ${message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function main() {
|
|
28
|
+
const [summaryPath] = process.argv.slice(2);
|
|
29
|
+
if (!summaryPath) {
|
|
30
|
+
fail('usage: node scripts/validate-project-pages-summary.cjs <summary.json path>');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const bundlePath = path.join(__dirname, 'project-pages-logic', 'dist', 'logic.cjs');
|
|
34
|
+
if (!fs.existsSync(bundlePath)) {
|
|
35
|
+
fail(`build bundle not found at ${bundlePath} — run "npm run build:project-pages-logic" first`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let validateProjectPageSummary;
|
|
39
|
+
try {
|
|
40
|
+
({ validateProjectPageSummary } = require(bundlePath));
|
|
41
|
+
} catch (err) {
|
|
42
|
+
fail(`failed to load ${bundlePath}: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let summary;
|
|
46
|
+
try {
|
|
47
|
+
summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
fail(`could not read/parse ${summaryPath}: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = validateProjectPageSummary(summary);
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
console.error('validate-project-pages-summary: invalid summary:');
|
|
55
|
+
for (const err of result.errors) console.error(` - ${err}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('valid');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
resolveEpicModel,
|
|
19
19
|
findAgentTypeByClaudeSessionId,
|
|
20
20
|
readPersonaModel,
|
|
21
|
+
resolvePrdPersonaForSpawn,
|
|
21
22
|
} = require('../lib/agentModelResolve.cjs');
|
|
22
23
|
|
|
23
24
|
const tmpDirs = [];
|
|
@@ -136,3 +137,68 @@ test('readPersonaModel returns null (does not throw) for a persona deleted after
|
|
|
136
137
|
expect(matches).toHaveLength(1);
|
|
137
138
|
expect(matches[0].level).toBe('warn');
|
|
138
139
|
});
|
|
140
|
+
|
|
141
|
+
// resolvePrdPersonaForSpawn — the PRD-path resolver scheduler.cjs's executeJob
|
|
142
|
+
// calls to turn a job's agentType into a --append-system-prompt body and a
|
|
143
|
+
// --model value (PRD 1115).
|
|
144
|
+
|
|
145
|
+
test('resolvePrdPersonaForSpawn returns the persona body and its own model, overriding the fallback', async () => {
|
|
146
|
+
const result = await resolvePrdPersonaForSpawn({
|
|
147
|
+
cwd: '/irrelevant',
|
|
148
|
+
agentType: 'dev-lead',
|
|
149
|
+
deps: {
|
|
150
|
+
getPersonaBody: async ({ name }) => {
|
|
151
|
+
expect(name).toBe('dev-lead');
|
|
152
|
+
return { path: '/home/user/.claude/agents/dev-lead.md', text: '---\nmodel: opus\n---\nOperate methodically.' };
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
expect(result.model).toBe('opus');
|
|
157
|
+
expect(result.systemPrompt).toBe('Operate methodically.');
|
|
158
|
+
expect(result.personaPath).toBe('/home/user/.claude/agents/dev-lead.md');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("resolvePrdPersonaForSpawn falls back to the fallback model when the persona's model is 'inherit'", async () => {
|
|
162
|
+
const result = await resolvePrdPersonaForSpawn({
|
|
163
|
+
cwd: '/irrelevant',
|
|
164
|
+
agentType: 'dev-lead',
|
|
165
|
+
deps: { getPersonaBody: async () => ({ path: '/x/dev-lead.md', text: '---\nmodel: inherit\n---\nBody text.' }) },
|
|
166
|
+
});
|
|
167
|
+
expect(result.model).toBe(FALLBACK_MODEL);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('resolvePrdPersonaForSpawn falls back without throwing when agentType no longer resolves to a persona file, and logs it once', async () => {
|
|
171
|
+
const cwd = await mkTmpDir('sm-agentmodel-cwd-');
|
|
172
|
+
const call = () => resolvePrdPersonaForSpawn({
|
|
173
|
+
cwd,
|
|
174
|
+
agentType: 'ghost-persona',
|
|
175
|
+
deps: { getPersonaBody: async () => null },
|
|
176
|
+
});
|
|
177
|
+
await expect(call()).resolves.toEqual({ model: FALLBACK_MODEL, systemPrompt: null, personaPath: null });
|
|
178
|
+
await call(); // second call must not log a second line (dedup)
|
|
179
|
+
|
|
180
|
+
const { todayFile } = require('../lib/opsErrorLog.cjs');
|
|
181
|
+
const lines = fs.readFileSync(todayFile(cwd), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
182
|
+
const matches = lines.filter((l) => l.message.includes('ghost-persona'));
|
|
183
|
+
expect(matches).toHaveLength(1);
|
|
184
|
+
expect(matches[0].level).toBe('warn');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('resolvePrdPersonaForSpawn returns the fallback (no persona applied) when agentType is absent, without touching getPersonaBody', async () => {
|
|
188
|
+
const getPersonaBody = async () => { throw new Error('must not be called'); };
|
|
189
|
+
const result = await resolvePrdPersonaForSpawn({ cwd: '/irrelevant', agentType: null, deps: { getPersonaBody } });
|
|
190
|
+
expect(result).toEqual({ model: FALLBACK_MODEL, systemPrompt: null, personaPath: null });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('resolvePrdPersonaForSpawn caps the persona body at 6000 characters with a truncation notice naming the persona path', async () => {
|
|
194
|
+
const longBody = 'x'.repeat(6500);
|
|
195
|
+
const result = await resolvePrdPersonaForSpawn({
|
|
196
|
+
cwd: '/irrelevant',
|
|
197
|
+
agentType: 'dev-lead',
|
|
198
|
+
deps: { getPersonaBody: async () => ({ path: '/home/user/.claude/agents/dev-lead.md', text: longBody }) },
|
|
199
|
+
});
|
|
200
|
+
expect(result.systemPrompt.startsWith('x'.repeat(6000))).toBe(true);
|
|
201
|
+
expect(result.systemPrompt.length).toBeLessThan(longBody.length);
|
|
202
|
+
expect(result.systemPrompt.toLowerCase()).toContain('truncat');
|
|
203
|
+
expect(result.systemPrompt).toContain('/home/user/.claude/agents/dev-lead.md');
|
|
204
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* epicStatusMirror.test.cjs — every write path that changes an Epic's status
|
|
3
|
+
* in active-index.json must leave the SAME status mirrored onto
|
|
4
|
+
* prompt-sessions/<id>.json (epicStatusMirror.cjs), so a lost/clobbered index
|
|
5
|
+
* can be reconstructed (activeIndexRebuild.cjs) without silently erasing
|
|
6
|
+
* open Epics.
|
|
7
|
+
*
|
|
8
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/epicStatusMirror.test.cjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
import { test, expect, afterEach } from 'vitest';
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const fsp = require('node:fs/promises');
|
|
16
|
+
const os = require('node:os');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const { ensureEpic, removeEpic, readActiveIndex, MINT_AUTHORITY_NEW_EPIC_UI } = require('../lib/epicMint.cjs');
|
|
19
|
+
const { mergeActiveIndex } = require('../lib/activeIndexMerge.cjs');
|
|
20
|
+
const { epicMirrorPath } = require('../lib/epicStatusMirror.cjs');
|
|
21
|
+
const config = require('../config.cjs');
|
|
22
|
+
|
|
23
|
+
const tmpDirs = [];
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
while (tmpDirs.length) {
|
|
26
|
+
const d = tmpDirs.pop();
|
|
27
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function mkCwd() {
|
|
32
|
+
const cwd = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-statusmirror-cwd-'));
|
|
33
|
+
config.addAllowedRoot(cwd);
|
|
34
|
+
tmpDirs.push(cwd);
|
|
35
|
+
return cwd;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readMirror(cwd, epicId) {
|
|
39
|
+
return JSON.parse(fs.readFileSync(epicMirrorPath(cwd, epicId), 'utf8'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('minting an Epic leaves the file\'s status equal to the index row\'s', async () => {
|
|
43
|
+
const cwd = await mkCwd();
|
|
44
|
+
|
|
45
|
+
const { epicId } = await ensureEpic(cwd, {
|
|
46
|
+
goalText: 'a fresh Epic',
|
|
47
|
+
mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const index = readActiveIndex(cwd);
|
|
51
|
+
const mirror = readMirror(cwd, epicId);
|
|
52
|
+
expect(mirror.status).toBe(index.sessions[epicId].status);
|
|
53
|
+
expect(mirror.status).toBe('proposed');
|
|
54
|
+
expect(mirror.cwd).toBe(cwd);
|
|
55
|
+
expect(mirror.goalText).toBe('a fresh Epic');
|
|
56
|
+
expect(typeof mirror.indexedAt).toBe('string');
|
|
57
|
+
expect(mirror.archivedAt).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('the approve/start transition (mergeActiveIndex) updates the file\'s status to match', async () => {
|
|
61
|
+
const cwd = await mkCwd();
|
|
62
|
+
const { epicId } = await ensureEpic(cwd, {
|
|
63
|
+
goalText: 'to be started',
|
|
64
|
+
mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const proposedIndex = readActiveIndex(cwd);
|
|
68
|
+
const activated = { ...proposedIndex.sessions[epicId], status: 'active' };
|
|
69
|
+
await mergeActiveIndex(cwd, {
|
|
70
|
+
sessions: { [epicId]: activated },
|
|
71
|
+
events: proposedIndex.events,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const index = readActiveIndex(cwd);
|
|
75
|
+
const mirror = readMirror(cwd, epicId);
|
|
76
|
+
expect(index.sessions[epicId].status).toBe('active');
|
|
77
|
+
expect(mirror.status).toBe('active');
|
|
78
|
+
expect(mirror.status).toBe(index.sessions[epicId].status);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('removeEpic (mint rollback) deletes the mirror file too, leaving no orphan', async () => {
|
|
82
|
+
const cwd = await mkCwd();
|
|
83
|
+
const { epicId } = await ensureEpic(cwd, {
|
|
84
|
+
goalText: 'will be rolled back',
|
|
85
|
+
mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI,
|
|
86
|
+
});
|
|
87
|
+
expect(fs.existsSync(epicMirrorPath(cwd, epicId))).toBe(true);
|
|
88
|
+
|
|
89
|
+
await removeEpic(cwd, epicId);
|
|
90
|
+
|
|
91
|
+
expect(fs.existsSync(epicMirrorPath(cwd, epicId))).toBe(false);
|
|
92
|
+
expect(readActiveIndex(cwd).sessions[epicId]).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a merge that tombstones an Epic leaves its prior mirror file in place (rebuild excludes it via the tombstone, not file deletion)', async () => {
|
|
96
|
+
const cwd = await mkCwd();
|
|
97
|
+
const { epicId } = await ensureEpic(cwd, {
|
|
98
|
+
goalText: 'to be removed',
|
|
99
|
+
mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await mergeActiveIndex(cwd, { sessions: {}, events: {}, removedIds: [epicId] });
|
|
103
|
+
|
|
104
|
+
const index = readActiveIndex(cwd);
|
|
105
|
+
expect(index.sessions[epicId]).toBeUndefined();
|
|
106
|
+
expect(index.tombstones[epicId]).toBeDefined();
|
|
107
|
+
// The mirror file is untouched — activeIndexRebuild.cjs is what must keep
|
|
108
|
+
// this id out, by consulting tombstones, not by the file being gone.
|
|
109
|
+
expect(fs.existsSync(epicMirrorPath(cwd, epicId))).toBe(true);
|
|
110
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-delegation-chain.test.cjs — "can this machine actually delegate
|
|
3
|
+
* work to the scheduler?" health section (PRD 1049), built on top of the
|
|
4
|
+
* delegation-readiness probe (delegationReadiness.cjs, PRD 1048). Exercises
|
|
5
|
+
* evaluateDelegationChainHealth() directly since it's pure — no need to spawn
|
|
6
|
+
* the full check() to cover shaping/degradation — plus one end-to-end check()
|
|
7
|
+
* run against this repo's own real config, where every check is expected to
|
|
8
|
+
* pass (this repo registers the scheduler MCP in its own .mcp.json and wires
|
|
9
|
+
* guard-prd-writes in .claude/settings.json).
|
|
10
|
+
*
|
|
11
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/health-delegation-chain.test.cjs
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
import { test, expect } from 'vitest';
|
|
17
|
+
const { evaluateDelegationChainHealth, check } = require('../health.cjs');
|
|
18
|
+
|
|
19
|
+
function makeCheck(id, ok, overrides = {}) {
|
|
20
|
+
return { id, label: `${id} label`, ok, detail: `${id} detail`, fix: ok ? null : `fix ${id}`, ...overrides };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('all checks passing -> component ok, no issues', () => {
|
|
24
|
+
const delegationResult = {
|
|
25
|
+
ok: true,
|
|
26
|
+
checks: [
|
|
27
|
+
makeCheck('scheduler-mcp', true),
|
|
28
|
+
makeCheck('scheduler-mcp-live', true),
|
|
29
|
+
makeCheck('scheduler-mcp-project-duplicate', true, { warn: false }),
|
|
30
|
+
makeCheck('dev-plugin', true),
|
|
31
|
+
makeCheck('agent-personas', true),
|
|
32
|
+
makeCheck('prd-write-guard', true),
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
const { component, issues } = evaluateDelegationChainHealth(delegationResult);
|
|
36
|
+
expect(component.ok).toBe(true);
|
|
37
|
+
expect(component.checks).toHaveLength(6);
|
|
38
|
+
expect(issues).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('one failing check -> component not ok, one issue citing label/detail/fix', () => {
|
|
42
|
+
const delegationResult = {
|
|
43
|
+
ok: false,
|
|
44
|
+
checks: [
|
|
45
|
+
makeCheck('scheduler-mcp', false, { label: 'Scheduler MCP server registered', detail: 'no entry found', fix: 'claude mcp add ...' }),
|
|
46
|
+
makeCheck('scheduler-mcp-live', true, { skipped: true }),
|
|
47
|
+
makeCheck('scheduler-mcp-project-duplicate', true, { warn: false }),
|
|
48
|
+
makeCheck('dev-plugin', true),
|
|
49
|
+
makeCheck('agent-personas', true),
|
|
50
|
+
makeCheck('prd-write-guard', true),
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
const { component, issues } = evaluateDelegationChainHealth(delegationResult);
|
|
54
|
+
expect(component.ok).toBe(false);
|
|
55
|
+
expect(issues).toHaveLength(1);
|
|
56
|
+
expect(issues[0]).toContain('Scheduler MCP server registered');
|
|
57
|
+
expect(issues[0]).toContain('no entry found');
|
|
58
|
+
expect(issues[0]).toContain('claude mcp add ...');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('multiple failing checks -> one issue per failing check, passing checks silent', () => {
|
|
62
|
+
const delegationResult = {
|
|
63
|
+
ok: false,
|
|
64
|
+
checks: [
|
|
65
|
+
makeCheck('scheduler-mcp', false),
|
|
66
|
+
makeCheck('scheduler-mcp-live', false),
|
|
67
|
+
makeCheck('scheduler-mcp-project-duplicate', true, { warn: false }),
|
|
68
|
+
makeCheck('dev-plugin', false),
|
|
69
|
+
makeCheck('agent-personas', true),
|
|
70
|
+
makeCheck('prd-write-guard', true),
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
const { issues } = evaluateDelegationChainHealth(delegationResult);
|
|
74
|
+
expect(issues).toHaveLength(3);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('failing check with no fix omits the "(fix: ...)" suffix', () => {
|
|
78
|
+
const delegationResult = {
|
|
79
|
+
ok: false,
|
|
80
|
+
checks: [makeCheck('scheduler-mcp', false, { fix: null })],
|
|
81
|
+
};
|
|
82
|
+
const { issues } = evaluateDelegationChainHealth(delegationResult);
|
|
83
|
+
expect(issues[0]).not.toContain('(fix:');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('end-to-end: check() reports delegation_chain for this repo, expected to pass here', async () => {
|
|
87
|
+
const status = await check();
|
|
88
|
+
expect(status.components.delegation_chain).toBeDefined();
|
|
89
|
+
expect(status.components.delegation_chain.checks).toHaveLength(7);
|
|
90
|
+
const ids = status.components.delegation_chain.checks.map((c) => c.id);
|
|
91
|
+
expect(ids).toEqual([
|
|
92
|
+
'scheduler-mcp',
|
|
93
|
+
'scheduler-mcp-live',
|
|
94
|
+
'scheduler-mcp-project-duplicate',
|
|
95
|
+
'dev-plugin',
|
|
96
|
+
'agent-personas',
|
|
97
|
+
'prd-write-guard',
|
|
98
|
+
'destructive-git-guard',
|
|
99
|
+
]);
|
|
100
|
+
// This repo registers the scheduler MCP at user scope (~/.claude.json) and
|
|
101
|
+
// wires guard-prd-writes in .claude/settings.json, and no longer carries a
|
|
102
|
+
// project-scope duplicate in its own .mcp.json, so on this machine every
|
|
103
|
+
// check is expected to pass — a failure here means the probe or the
|
|
104
|
+
// wiring is wrong, not that the assertion should be relaxed.
|
|
105
|
+
expect(status.components.delegation_chain.ok).toBe(true);
|
|
106
|
+
}, 90_000);
|
|
@@ -110,6 +110,12 @@ async function startAdmin() {
|
|
|
110
110
|
process.env.SM_E2E = '1';
|
|
111
111
|
const admin = createAdminHttp();
|
|
112
112
|
prdAdminRoutes.registerAdminRoute(admin, scheduler.remote);
|
|
113
|
+
// Also wires reset-job/jobs (scheduler.cjs's own admin routes) onto the
|
|
114
|
+
// SAME admin instance/port, against the real (not faked) `remote` — this
|
|
115
|
+
// file's real-filesystem setup is what the reset-job/cancel-job/archive-prd
|
|
116
|
+
// slug-vs-not-found split needs, unlike scheduler-admin-routes.test.cjs's
|
|
117
|
+
// fake remote.
|
|
118
|
+
scheduler.registerAdminRoutes(admin, scheduler.remote);
|
|
113
119
|
const { port, token } = await admin.start();
|
|
114
120
|
if (prevE2e === undefined) delete process.env.SM_E2E; else process.env.SM_E2E = prevE2e;
|
|
115
121
|
return { admin, port, token };
|
|
@@ -388,6 +394,295 @@ test('POST /admin/scheduler/archive-prd moves a PRD to prds-archived/', async ()
|
|
|
388
394
|
}
|
|
389
395
|
});
|
|
390
396
|
|
|
397
|
+
// ─────────────────────────────────────── invalid-slug vs unknown-slug split
|
|
398
|
+
//
|
|
399
|
+
// PRD: safeSlugPath used to collapse "malformed slug" and "well-formed slug
|
|
400
|
+
// found nowhere" into the same `null` -> "invalid slug" response, so an
|
|
401
|
+
// agent that typed a real slug for a PRD in the wrong project (or a since-
|
|
402
|
+
// archived one) read the error as "I typed it wrong" and retried instead of
|
|
403
|
+
// listing PRDs. These tests pin the split for all three verbs plus the new
|
|
404
|
+
// optional `cwd` narrowing.
|
|
405
|
+
|
|
406
|
+
test('POST /admin/scheduler/reset-job with a malformed slug returns "invalid slug"', async () => {
|
|
407
|
+
const { admin, port, token } = await startAdmin();
|
|
408
|
+
try {
|
|
409
|
+
const res = await request(port, {
|
|
410
|
+
method: 'POST', path: '/admin/scheduler/reset-job', token, body: { slug: 'not a slug!!' },
|
|
411
|
+
});
|
|
412
|
+
expect(res.status).toBe(200);
|
|
413
|
+
expect(res.json.ok).toBe(false);
|
|
414
|
+
expect(res.json.error).toBe('invalid slug');
|
|
415
|
+
} finally {
|
|
416
|
+
await admin.stop();
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('POST /admin/scheduler/reset-job with a well-formed slug that exists nowhere returns a distinct unknown-slug error', async () => {
|
|
421
|
+
const { admin, port, token } = await startAdmin();
|
|
422
|
+
try {
|
|
423
|
+
const slug = uniqueSlug('definitely-not-a-real-prd');
|
|
424
|
+
const res = await request(port, {
|
|
425
|
+
method: 'POST', path: '/admin/scheduler/reset-job', token, body: { slug },
|
|
426
|
+
});
|
|
427
|
+
expect(res.status).toBe(200);
|
|
428
|
+
expect(res.json.ok).toBe(false);
|
|
429
|
+
expect(res.json.error).not.toBe('invalid slug');
|
|
430
|
+
expect(res.json.error).toMatch(/unknown slug/);
|
|
431
|
+
expect(res.json.error).toMatch(/scheduler_list_prds/);
|
|
432
|
+
} finally {
|
|
433
|
+
await admin.stop();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('POST /admin/scheduler/reset-job with a cwd naming a project with no PRD dir yields the unknown-slug error, not a crash', async () => {
|
|
438
|
+
const cwd = projectCwd();
|
|
439
|
+
const epicId = 'epic-reset-cwd-miss';
|
|
440
|
+
const slug = uniqueSlug('reset-cwd-miss');
|
|
441
|
+
await writePrdFixture({ cwd, epicId, slug }); // exists under `cwd`, not under `otherCwd`
|
|
442
|
+
|
|
443
|
+
const otherCwd = path.join(tmpHome, 'fake-project-cwd-empty');
|
|
444
|
+
fs.mkdirSync(otherCwd, { recursive: true });
|
|
445
|
+
|
|
446
|
+
const { admin, port, token } = await startAdmin();
|
|
447
|
+
try {
|
|
448
|
+
const res = await request(port, {
|
|
449
|
+
method: 'POST', path: '/admin/scheduler/reset-job', token, body: { slug, cwd: otherCwd },
|
|
450
|
+
});
|
|
451
|
+
expect(res.status).toBe(200);
|
|
452
|
+
expect(res.json.ok).toBe(false);
|
|
453
|
+
expect(res.json.error).toMatch(/unknown slug/);
|
|
454
|
+
} finally {
|
|
455
|
+
await admin.stop();
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test('POST /admin/scheduler/reset-job with a matching cwd resets a known job', async () => {
|
|
460
|
+
const cwd = projectCwd();
|
|
461
|
+
const epicId = 'epic-reset-cwd-hit';
|
|
462
|
+
const slug = uniqueSlug('reset-cwd-hit');
|
|
463
|
+
await writePrdFixture({ cwd, epicId, slug });
|
|
464
|
+
await scheduler.writeQueue({
|
|
465
|
+
jobs: [{ slug, title: 'x', cwd, status: 'needs_review' }],
|
|
466
|
+
config: {},
|
|
467
|
+
paused: null,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const { admin, port, token } = await startAdmin();
|
|
471
|
+
try {
|
|
472
|
+
const res = await request(port, {
|
|
473
|
+
method: 'POST', path: '/admin/scheduler/reset-job', token, body: { slug, cwd },
|
|
474
|
+
});
|
|
475
|
+
expect(res.status).toBe(200);
|
|
476
|
+
expect(res.json).toEqual({ ok: true, slug, status: 'pending' });
|
|
477
|
+
} finally {
|
|
478
|
+
await admin.stop();
|
|
479
|
+
await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('POST /admin/scheduler/reset-job with cwd resets the SAME-slug job belonging to that project, not a same-slug job from another project (cwd narrows the job lookup, not just the file lookup)', async () => {
|
|
484
|
+
// Slugs derive from title text with no cwd salt, so two different projects
|
|
485
|
+
// can legitimately queue an identically-slugged job. `cwd` must disambiguate
|
|
486
|
+
// which one gets reset, not just which PRD file gets read for existence.
|
|
487
|
+
const cwdA = projectCwd();
|
|
488
|
+
const cwdB = path.join(tmpHome, 'fake-project-cwd-collision-b');
|
|
489
|
+
fs.mkdirSync(cwdB, { recursive: true });
|
|
490
|
+
config.addAllowedRoot(cwdB);
|
|
491
|
+
// queueStore.cjs's readMerged only reads back a project's shard if that
|
|
492
|
+
// project's cwd is discoverable via allProjectCwds (scans ~/.claude/projects/
|
|
493
|
+
// *.jsonl for a `cwd` field) — same fake-transcript trick beforeAll used for
|
|
494
|
+
// the shared fixture cwd.
|
|
495
|
+
const projDirB = path.join(tmpHome, '.claude', 'projects', 'fake-project-slug-b');
|
|
496
|
+
fs.mkdirSync(projDirB, { recursive: true });
|
|
497
|
+
fs.writeFileSync(path.join(projDirB, 'session.jsonl'), `${JSON.stringify({ cwd: cwdB })}\n`, 'utf8');
|
|
498
|
+
const { bustCwdCache } = require('../lib/queueStore.cjs');
|
|
499
|
+
bustCwdCache();
|
|
500
|
+
const slug = uniqueSlug('collision-slug');
|
|
501
|
+
const epicId = 'epic-reset-collision';
|
|
502
|
+
await writePrdFixture({ cwd: cwdB, epicId, slug });
|
|
503
|
+
await scheduler.writeQueue({
|
|
504
|
+
jobs: [
|
|
505
|
+
{ slug, title: 'project A job', cwd: cwdA, status: 'needs_review' },
|
|
506
|
+
{ slug, title: 'project B job', cwd: cwdB, status: 'needs_review' },
|
|
507
|
+
],
|
|
508
|
+
config: {},
|
|
509
|
+
paused: null,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const { admin, port, token } = await startAdmin();
|
|
513
|
+
try {
|
|
514
|
+
const res = await request(port, {
|
|
515
|
+
method: 'POST', path: '/admin/scheduler/reset-job', token, body: { slug, cwd: cwdB },
|
|
516
|
+
});
|
|
517
|
+
expect(res.status).toBe(200);
|
|
518
|
+
expect(res.json).toEqual({ ok: true, slug, status: 'pending' });
|
|
519
|
+
|
|
520
|
+
const state = await scheduler.remote.listJobs();
|
|
521
|
+
const jobA = state.find((j) => j.cwd === cwdA);
|
|
522
|
+
const jobB = state.find((j) => j.cwd === cwdB);
|
|
523
|
+
expect(jobA.status).toBe('needs_review'); // untouched
|
|
524
|
+
expect(jobB.status).toBe('pending'); // the one actually targeted
|
|
525
|
+
} finally {
|
|
526
|
+
await admin.stop();
|
|
527
|
+
await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test('POST /admin/scheduler/cancel-job with a malformed slug is rejected at the request-schema layer (400) — never reaches "not found"', async () => {
|
|
532
|
+
const { admin, port, token } = await startAdmin();
|
|
533
|
+
try {
|
|
534
|
+
const res = await request(port, {
|
|
535
|
+
method: 'POST', path: '/admin/scheduler/cancel-job', token, body: { slug: 'not a slug!!' },
|
|
536
|
+
});
|
|
537
|
+
expect(res.status).toBe(400);
|
|
538
|
+
expect(res.json.ok).toBe(false);
|
|
539
|
+
expect(res.json.error).not.toMatch(/unknown slug|not found/i);
|
|
540
|
+
} finally {
|
|
541
|
+
await admin.stop();
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test('remote.cancelJob itself (bypassing the HTTP schema layer) returns "invalid slug" for a malformed slug, distinct from "unknown slug"', async () => {
|
|
546
|
+
const malformed = await scheduler.remote.cancelJob('not a slug!!');
|
|
547
|
+
expect(malformed).toEqual({ ok: false, error: 'invalid slug' });
|
|
548
|
+
|
|
549
|
+
const wellFormedMissing = await scheduler.remote.cancelJob(uniqueSlug('cancel-remote-missing'));
|
|
550
|
+
expect(wellFormedMissing.ok).toBe(false);
|
|
551
|
+
expect(wellFormedMissing.error).not.toBe('invalid slug');
|
|
552
|
+
expect(wellFormedMissing.error).toMatch(/unknown slug/);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test('POST /admin/scheduler/cancel-job with a well-formed slug that has no queued job returns a distinct unknown-slug error (404)', async () => {
|
|
556
|
+
const { admin, port, token } = await startAdmin();
|
|
557
|
+
try {
|
|
558
|
+
const slug = uniqueSlug('cancel-missing');
|
|
559
|
+
const res = await request(port, {
|
|
560
|
+
method: 'POST', path: '/admin/scheduler/cancel-job', token, body: { slug },
|
|
561
|
+
});
|
|
562
|
+
expect(res.status).toBe(404);
|
|
563
|
+
expect(res.json.ok).toBe(false);
|
|
564
|
+
expect(res.json.error).not.toBe('invalid slug');
|
|
565
|
+
expect(res.json.error).toMatch(/unknown slug/);
|
|
566
|
+
} finally {
|
|
567
|
+
await admin.stop();
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test('POST /admin/scheduler/cancel-job with cwd narrows job lookup to that project', async () => {
|
|
572
|
+
const cwd = projectCwd();
|
|
573
|
+
const slug = uniqueSlug('cancel-cwd-scope');
|
|
574
|
+
await scheduler.writeQueue({
|
|
575
|
+
jobs: [{ slug, title: 'x', cwd, status: 'pending' }],
|
|
576
|
+
config: {},
|
|
577
|
+
paused: null,
|
|
578
|
+
});
|
|
579
|
+
const otherCwd = path.join(tmpHome, 'fake-project-cwd-cancel-miss');
|
|
580
|
+
fs.mkdirSync(otherCwd, { recursive: true });
|
|
581
|
+
|
|
582
|
+
const { admin, port, token } = await startAdmin();
|
|
583
|
+
try {
|
|
584
|
+
const missRes = await request(port, {
|
|
585
|
+
method: 'POST', path: '/admin/scheduler/cancel-job', token, body: { slug, cwd: otherCwd },
|
|
586
|
+
});
|
|
587
|
+
expect(missRes.status).toBe(404);
|
|
588
|
+
expect(missRes.json.ok).toBe(false);
|
|
589
|
+
|
|
590
|
+
const hitRes = await request(port, {
|
|
591
|
+
method: 'POST', path: '/admin/scheduler/cancel-job', token, body: { slug, cwd },
|
|
592
|
+
});
|
|
593
|
+
expect(hitRes.status).toBe(200);
|
|
594
|
+
expect(hitRes.json.ok).toBe(true);
|
|
595
|
+
} finally {
|
|
596
|
+
await admin.stop();
|
|
597
|
+
await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('POST /admin/scheduler/archive-prd with a malformed slug is rejected at the request-schema layer (400) — never reaches "not found"', async () => {
|
|
602
|
+
const { admin, port, token } = await startAdmin();
|
|
603
|
+
try {
|
|
604
|
+
const res = await request(port, {
|
|
605
|
+
method: 'POST', path: '/admin/scheduler/archive-prd', token, body: { slugs: ['not a slug!!'] },
|
|
606
|
+
});
|
|
607
|
+
expect(res.status).toBe(400);
|
|
608
|
+
expect(res.json.ok).toBe(false);
|
|
609
|
+
} finally {
|
|
610
|
+
await admin.stop();
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('queueOps.archiveMany itself (bypassing the HTTP schema layer) splits "invalid slug" from "not found in any PRDs dir"', async () => {
|
|
615
|
+
const queueOps = require('../queueOps.cjs');
|
|
616
|
+
const malformed = await queueOps.archiveMany(['not a slug!!']);
|
|
617
|
+
expect(malformed.results[0]).toMatchObject({ ok: false, error: 'invalid slug' });
|
|
618
|
+
|
|
619
|
+
const missing = await queueOps.archiveMany([uniqueSlug('archive-remote-missing')]);
|
|
620
|
+
expect(missing.results[0]).toMatchObject({ ok: false, error: 'not found in any PRDs dir' });
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('POST /admin/scheduler/archive-prd with a well-formed slug found nowhere returns a distinct not-found error', async () => {
|
|
624
|
+
const { admin, port, token } = await startAdmin();
|
|
625
|
+
try {
|
|
626
|
+
const slug = uniqueSlug('archive-missing');
|
|
627
|
+
const res = await request(port, {
|
|
628
|
+
method: 'POST', path: '/admin/scheduler/archive-prd', token, body: { slugs: [slug] },
|
|
629
|
+
});
|
|
630
|
+
expect(res.status).toBe(200);
|
|
631
|
+
expect(res.json.results[0].ok).toBe(false);
|
|
632
|
+
expect(res.json.results[0].error).toBe('not found in any PRDs dir');
|
|
633
|
+
expect(res.json.results[0].error).not.toBe('invalid slug');
|
|
634
|
+
} finally {
|
|
635
|
+
await admin.stop();
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test('POST /admin/scheduler/archive-prd with cwd narrows the search: a cwd with no PRD dir misses, the real cwd hits', async () => {
|
|
640
|
+
const cwd = projectCwd();
|
|
641
|
+
const epicId = 'epic-archive-cwd';
|
|
642
|
+
const slug = uniqueSlug('archive-cwd-scope');
|
|
643
|
+
const filePath = await writePrdFixture({ cwd, epicId, slug });
|
|
644
|
+
|
|
645
|
+
const otherCwd = path.join(tmpHome, 'fake-project-cwd-archive-miss');
|
|
646
|
+
fs.mkdirSync(otherCwd, { recursive: true });
|
|
647
|
+
|
|
648
|
+
const { admin, port, token } = await startAdmin();
|
|
649
|
+
try {
|
|
650
|
+
const missRes = await request(port, {
|
|
651
|
+
method: 'POST', path: '/admin/scheduler/archive-prd', token, body: { slugs: [slug], cwd: otherCwd },
|
|
652
|
+
});
|
|
653
|
+
expect(missRes.json.results[0].ok).toBe(false);
|
|
654
|
+
expect(missRes.json.results[0].error).toBe('not found in any PRDs dir');
|
|
655
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
656
|
+
|
|
657
|
+
const hitRes = await request(port, {
|
|
658
|
+
method: 'POST', path: '/admin/scheduler/archive-prd', token, body: { slugs: [slug], cwd },
|
|
659
|
+
});
|
|
660
|
+
expect(hitRes.json.results[0].ok).toBe(true);
|
|
661
|
+
expect(fs.existsSync(filePath)).toBe(false);
|
|
662
|
+
} finally {
|
|
663
|
+
await admin.stop();
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test('GET /admin/mcp/readiness requires cwd and returns the delegation-readiness checks for it', async () => {
|
|
668
|
+
const cwd = projectCwd();
|
|
669
|
+
const { admin, port, token } = await startAdmin();
|
|
670
|
+
try {
|
|
671
|
+
const missing = await request(port, { path: '/admin/mcp/readiness', token });
|
|
672
|
+
expect(missing.status).toBe(400);
|
|
673
|
+
expect(missing.json.ok).toBe(false);
|
|
674
|
+
|
|
675
|
+
const res = await request(port, { path: `/admin/mcp/readiness?cwd=${encodeURIComponent(cwd)}`, token });
|
|
676
|
+
expect(res.status).toBe(200);
|
|
677
|
+
expect(res.json.ok).toBe(true);
|
|
678
|
+
expect(typeof res.json.ready).toBe('boolean');
|
|
679
|
+
expect(Array.isArray(res.json.checks)).toBe(true);
|
|
680
|
+
expect(res.json.checks.some((c) => c.id === 'scheduler-mcp')).toBe(true);
|
|
681
|
+
} finally {
|
|
682
|
+
await admin.stop();
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
391
686
|
test('POST /admin/scheduler/retag-prd rewrites estimateMinutes frontmatter', async () => {
|
|
392
687
|
const cwd = projectCwd();
|
|
393
688
|
const epicId = 'epic-retag';
|