claude-code-session-manager 0.57.1 → 0.58.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/{TiptapBody-CJDxVF7X.js → TiptapBody-BEBLdJl_.js} +1 -1
- package/dist/assets/index-DEQzGYa6.js +3227 -0
- package/dist/assets/{index-WUc_GHMf.css → index-DwUffaDq.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -2
- package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -6
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +16 -25
- package/plugins/session-manager-dev/skills/project-status/SKILL.md +5 -5
- package/scripts/lib/watchdogHelpers.cjs +0 -434
- package/scripts/mint-epic.cjs +6 -6
- package/src/main/__tests__/activeIndexMerge.test.cjs +2 -2
- package/src/main/__tests__/agentModelResolve.test.cjs +119 -0
- package/src/main/__tests__/epicContextDigest.test.cjs +100 -0
- package/src/main/__tests__/epicMint.test.cjs +165 -207
- package/src/main/__tests__/promptSessionSchema.test.cjs +101 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +139 -0
- package/src/main/__tests__/rcaReport.test.cjs +188 -0
- package/src/main/__tests__/runVerify.test.cjs +109 -6
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +214 -0
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +5 -5
- package/src/main/chatRunner.cjs +10 -1
- package/src/main/index.cjs +2 -4
- package/src/main/ipcSchemas.cjs +47 -2
- package/src/main/lib/agentModelResolve.cjs +97 -0
- package/src/main/lib/auditLog.cjs +2 -2
- package/src/main/lib/epicContextDigest.cjs +78 -0
- package/src/main/lib/epicMint.cjs +81 -164
- package/src/main/lib/promptSessionSchema.cjs +79 -0
- package/src/main/lib/promptSessionsCreateEpic.cjs +68 -0
- package/src/main/lib/{rcaFeedbackHook.cjs → rcaReport.cjs} +47 -114
- package/src/main/runVerify.cjs +17 -2
- package/src/main/scheduler.cjs +79 -24
- package/src/preload/api.d.ts +25 -48
- package/src/preload/index.cjs +6 -7
- package/dist/assets/index-DCa3QvAi.js +0 -3227
- package/plugins/session-manager-dev/skills/propose-epic/SKILL.md +0 -68
- package/scripts/propose-epic.cjs +0 -56
- package/src/main/__tests__/rcaFeedbackHook.test.cjs +0 -280
- package/src/main/repoAnalyzer.cjs +0 -346
- package/src/main/search.cjs +0 -332
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* epicContextDigest.test.cjs — unit tests for buildContextDigest
|
|
3
|
+
* (lib/epicContextDigest.cjs), which composes an Epic's goalText
|
|
4
|
+
* (active-index.json, via epicMint.cjs) with its most-recent transcript
|
|
5
|
+
* turns (promptSessionTranscript.cjs) into a bounded, real digest string.
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/epicContextDigest.test.cjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
import { test, expect, afterEach } from 'vitest';
|
|
13
|
+
const fsp = require('node:fs/promises');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const { buildContextDigest } = require('../lib/epicContextDigest.cjs');
|
|
17
|
+
const { ensureEpic, MINT_AUTHORITY_NEW_EPIC_UI } = require('../lib/epicMint.cjs');
|
|
18
|
+
const { appendTurn } = require('../promptSessionTranscript.cjs');
|
|
19
|
+
const config = require('../config.cjs');
|
|
20
|
+
|
|
21
|
+
const tmpDirs = [];
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
while (tmpDirs.length) {
|
|
24
|
+
const d = tmpDirs.pop();
|
|
25
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async function mkCwd() {
|
|
30
|
+
const cwd = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-epiccontextdigest-cwd-'));
|
|
31
|
+
config.addAllowedRoot(cwd);
|
|
32
|
+
tmpDirs.push(cwd);
|
|
33
|
+
return cwd;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('normal digest: goalText followed by recent turns', async () => {
|
|
37
|
+
const cwd = await mkCwd();
|
|
38
|
+
const { epicId } = await ensureEpic(cwd, { goalText: 'Fix the flaky login test', status: 'proposed', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
|
|
39
|
+
await appendTurn(cwd, epicId, { role: 'user', text: 'Please investigate the flaky test.' });
|
|
40
|
+
await appendTurn(cwd, epicId, { role: 'assistant', text: 'Found the race condition, fixing now.' });
|
|
41
|
+
|
|
42
|
+
const digest = await buildContextDigest({ cwd, epicId });
|
|
43
|
+
expect(digest).toContain('Fix the flaky login test');
|
|
44
|
+
expect(digest).toContain('[user] Please investigate the flaky test.');
|
|
45
|
+
expect(digest).toContain('[assistant] Found the race condition, fixing now.');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('missing epicId returns empty string', async () => {
|
|
49
|
+
const cwd = await mkCwd();
|
|
50
|
+
expect(await buildContextDigest({ cwd, epicId: '' })).toBe('');
|
|
51
|
+
expect(await buildContextDigest({ cwd })).toBe('');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('epicId absent from active-index.json returns empty string', async () => {
|
|
55
|
+
const cwd = await mkCwd();
|
|
56
|
+
const digest = await buildContextDigest({ cwd, epicId: 'psess-does-not-exist' });
|
|
57
|
+
expect(digest).toBe('');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('empty transcript still returns goalText only, no throw', async () => {
|
|
61
|
+
const cwd = await mkCwd();
|
|
62
|
+
const { epicId } = await ensureEpic(cwd, { goalText: 'Empty transcript epic', status: 'proposed', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
|
|
63
|
+
|
|
64
|
+
const digest = await buildContextDigest({ cwd, epicId });
|
|
65
|
+
expect(digest).toBe('Empty transcript epic');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('maxChars truncation drops the oldest turns first, never a mid-turn slice', async () => {
|
|
69
|
+
const cwd = await mkCwd();
|
|
70
|
+
const { epicId } = await ensureEpic(cwd, { goalText: 'Truncation epic', status: 'proposed', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
|
|
71
|
+
await appendTurn(cwd, epicId, { role: 'user', text: 'oldest turn text goes here' });
|
|
72
|
+
await appendTurn(cwd, epicId, { role: 'assistant', text: 'middle turn text goes here' });
|
|
73
|
+
await appendTurn(cwd, epicId, { role: 'user', text: 'newest turn text goes here' });
|
|
74
|
+
|
|
75
|
+
// Small enough that only the newest turn (plus goalText) can fit.
|
|
76
|
+
const digest = await buildContextDigest({ cwd, epicId, maxChars: 80 });
|
|
77
|
+
|
|
78
|
+
expect(digest.length).toBeLessThanOrEqual(80);
|
|
79
|
+
expect(digest).toContain('newest turn text goes here');
|
|
80
|
+
expect(digest).not.toContain('oldest turn text goes here');
|
|
81
|
+
expect(digest).not.toContain('middle turn text goes here');
|
|
82
|
+
// No turn line appears partially — every included "[role] " line is whole.
|
|
83
|
+
for (const line of digest.split('\n')) {
|
|
84
|
+
if (line.startsWith('[')) {
|
|
85
|
+
expect(line).toMatch(/^\[(user|assistant)\] .+$/);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('maxChars smaller than goalText alone clamps the header itself, never exceeding the bound', async () => {
|
|
91
|
+
const cwd = await mkCwd();
|
|
92
|
+
const longGoal = 'This is a deliberately long goalText that on its own already exceeds the small maxChars bound used below';
|
|
93
|
+
const { epicId } = await ensureEpic(cwd, { goalText: longGoal, status: 'proposed', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
|
|
94
|
+
await appendTurn(cwd, epicId, { role: 'user', text: 'a turn that cannot possibly fit either' });
|
|
95
|
+
|
|
96
|
+
const digest = await buildContextDigest({ cwd, epicId, maxChars: 20 });
|
|
97
|
+
|
|
98
|
+
expect(digest.length).toBe(20);
|
|
99
|
+
expect(digest).toBe(longGoal.slice(0, 20));
|
|
100
|
+
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* epicMint.test.cjs — unit tests for ensureEpic's
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* PromptTicket.id).
|
|
2
|
+
* epicMint.test.cjs — unit tests for ensureEpic's two behaviors: JOIN (every
|
|
3
|
+
* automated caller, by explicit epicId) and MINT (the New Epic UI alone, via
|
|
4
|
+
* mintAuthority — epicMint.cjs's SINGLE-CREATOR LAW).
|
|
6
5
|
*
|
|
7
6
|
* Run: timeout 120 npx vitest run src/main/__tests__/epicMint.test.cjs
|
|
8
7
|
*/
|
|
@@ -14,7 +13,7 @@ const fs = require('node:fs');
|
|
|
14
13
|
const fsp = require('node:fs/promises');
|
|
15
14
|
const os = require('node:os');
|
|
16
15
|
const path = require('node:path');
|
|
17
|
-
const { ensureEpic, removeEpic, readActiveIndex,
|
|
16
|
+
const { ensureEpic, removeEpic, readActiveIndex, MINT_AUTHORITY_NEW_EPIC_UI } = require('../lib/epicMint.cjs');
|
|
18
17
|
|
|
19
18
|
const tmpDirs = [];
|
|
20
19
|
afterEach(async () => {
|
|
@@ -30,297 +29,256 @@ async function mkCwd() {
|
|
|
30
29
|
return cwd;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
/** The New Epic UI's call shape — the only caller allowed to mint. */
|
|
33
|
+
function mint(cwd, opts = {}) {
|
|
34
|
+
return ensureEpic(cwd, { mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI, ...opts });
|
|
35
|
+
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function writeIndex(cwd, index) {
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(cwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json'),
|
|
40
|
+
JSON.stringify(index, null, 2),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
function readAuditRecordsForCwd(auditLog, cwd) {
|
|
45
|
+
if (!fs.existsSync(auditLog.AUDIT_LOG_PATH)) return [];
|
|
46
|
+
return fs.readFileSync(auditLog.AUDIT_LOG_PATH, 'utf8')
|
|
47
|
+
.split('\n')
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map((line) => {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(line);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.filter((record) => record && record.cwd === cwd);
|
|
57
|
+
}
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
expect(index.sessions[notAPromptSessionId]).toBeUndefined();
|
|
44
|
-
expect(index.sessions[result.epicId]).toBeDefined();
|
|
45
|
-
expect(Object.keys(index.sessions)).toHaveLength(1);
|
|
46
|
-
});
|
|
59
|
+
// ─── SINGLE-CREATOR LAW ──────────────────────────────────────────────────────
|
|
47
60
|
|
|
48
|
-
test('ensureEpic
|
|
61
|
+
test('ensureEpic refuses to mint without mintAuthority, and appends an epic_mint_refused audit event', async () => {
|
|
49
62
|
const cwd = await mkCwd();
|
|
63
|
+
const auditLog = require('../lib/auditLog.cjs');
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const second = await ensureEpic(cwd, { goalText: 'a different PRD title', epicId: first.epicId });
|
|
55
|
-
|
|
56
|
-
expect(second.created).toBe(false);
|
|
57
|
-
expect(second.epicId).toBe(first.epicId);
|
|
58
|
-
expect(second.prdDir).toBe(first.prdDir);
|
|
65
|
+
await expect(ensureEpic(cwd, { goalText: 'agent wants work done' }))
|
|
66
|
+
.rejects.toThrow(/no open Epic to join/);
|
|
59
67
|
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(0);
|
|
69
|
+
const records = readAuditRecordsForCwd(auditLog, cwd);
|
|
70
|
+
expect(records).toHaveLength(1);
|
|
71
|
+
expect(records[0].kind).toBe('epic_mint_refused');
|
|
62
72
|
});
|
|
63
73
|
|
|
64
|
-
test('
|
|
74
|
+
test('ensureEpic refuses to mint when mintAuthority is a wrong/guessed value', async () => {
|
|
65
75
|
const cwd = await mkCwd();
|
|
66
|
-
const minted = await ensureEpic(cwd, { goalText: 'to be rolled back' });
|
|
67
|
-
expect(readActiveIndex(cwd).sessions[minted.epicId]).toBeDefined();
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
await expect(ensureEpic(cwd, { goalText: 'sneaky', mintAuthority: 'agent' }))
|
|
78
|
+
.rejects.toThrow(/no open Epic to join/);
|
|
70
79
|
|
|
71
|
-
expect(
|
|
72
|
-
const index = readActiveIndex(cwd);
|
|
73
|
-
expect(index.sessions[minted.epicId]).toBeUndefined();
|
|
74
|
-
expect(index.events[minted.epicId]).toBeUndefined();
|
|
80
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(0);
|
|
75
81
|
});
|
|
76
82
|
|
|
77
|
-
test('
|
|
83
|
+
test('ensureEpic mints when the New Epic UI presents mintAuthority', async () => {
|
|
78
84
|
const cwd = await mkCwd();
|
|
79
|
-
await ensureEpic(cwd, { goalText: 'unrelated epic' });
|
|
80
|
-
|
|
81
|
-
const removed = await removeEpic(cwd, 'nonexistent-epic-id');
|
|
82
|
-
|
|
83
|
-
expect(removed).toBe(false);
|
|
84
|
-
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(1);
|
|
85
|
-
});
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
// "__proto__"/"constructor" returns a truthy Object.prototype member even
|
|
89
|
-
// when no such Epic was ever written — without an own-property check, this
|
|
90
|
-
// would make mintIfMissing:false's "join an existing Epic" gate falsely
|
|
91
|
-
// report a match, defeating the restriction that a PRD write may only join
|
|
92
|
-
// a real, already-approved Epic.
|
|
93
|
-
for (const pollutedKey of ['__proto__', 'constructor', 'toString', 'hasOwnProperty']) {
|
|
94
|
-
test(`ensureEpic refuses to "join" the prototype-chain key "${pollutedKey}" (mintIfMissing:false)`, async () => {
|
|
95
|
-
const cwd = await mkCwd();
|
|
96
|
-
|
|
97
|
-
await expect(
|
|
98
|
-
ensureEpic(cwd, { epicId: pollutedKey, mintIfMissing: false }),
|
|
99
|
-
).rejects.toThrow(/no existing Epic found/);
|
|
86
|
+
const minted = await mint(cwd, { goalText: 'human-created epic' });
|
|
100
87
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
test('removeEpic refuses to report success for the prototype-chain key "__proto__"', async () => {
|
|
107
|
-
const cwd = await mkCwd();
|
|
108
|
-
const removed = await removeEpic(cwd, '__proto__');
|
|
109
|
-
expect(removed).toBe(false);
|
|
88
|
+
expect(minted.created).toBe(true);
|
|
89
|
+
const index = readActiveIndex(cwd);
|
|
90
|
+
expect(index.sessions[minted.epicId].status).toBe('proposed');
|
|
91
|
+
expect(index.events[minted.epicId]).toHaveLength(1);
|
|
110
92
|
});
|
|
111
93
|
|
|
112
|
-
test('
|
|
94
|
+
test('a mint never joins a near-identical open Epic — the human asked for a new one', async () => {
|
|
113
95
|
const cwd = await mkCwd();
|
|
114
|
-
const source = { producer: 'rca-hook', prdSlug: 'x', runId: 'y' };
|
|
115
96
|
|
|
116
|
-
const
|
|
97
|
+
const first = await mint(cwd, { goalText: 'Fix the login button color' });
|
|
98
|
+
const second = await mint(cwd, { goalText: 'Fix login button colour issue' });
|
|
117
99
|
|
|
118
|
-
|
|
119
|
-
expect(
|
|
100
|
+
expect(second.created).toBe(true);
|
|
101
|
+
expect(second.epicId).not.toBe(first.epicId);
|
|
102
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(2);
|
|
120
103
|
});
|
|
121
104
|
|
|
122
|
-
|
|
105
|
+
// ─── JOIN ────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
test('ensureEpic joins the existing Epic when explicit epicId equals its promptSessionId', async () => {
|
|
123
108
|
const cwd = await mkCwd();
|
|
124
109
|
|
|
125
|
-
const
|
|
110
|
+
const first = await mint(cwd, { goalText: 'initial epic goal' });
|
|
111
|
+
const second = await ensureEpic(cwd, { goalText: 'a different PRD title', epicId: first.epicId });
|
|
126
112
|
|
|
127
|
-
|
|
128
|
-
expect(
|
|
113
|
+
expect(second.created).toBe(false);
|
|
114
|
+
expect(second.epicId).toBe(first.epicId);
|
|
115
|
+
expect(second.prdDir).toBe(first.prdDir);
|
|
116
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(1);
|
|
129
117
|
});
|
|
130
118
|
|
|
131
|
-
test('ensureEpic joins an
|
|
119
|
+
test('ensureEpic joins an already-active Epic via explicit epicId', async () => {
|
|
132
120
|
const cwd = await mkCwd();
|
|
121
|
+
const first = await mint(cwd, { goalText: 'already running epic' });
|
|
122
|
+
const index = readActiveIndex(cwd);
|
|
123
|
+
index.sessions[first.epicId].status = 'active';
|
|
124
|
+
writeIndex(cwd, index);
|
|
133
125
|
|
|
134
|
-
const
|
|
135
|
-
expect(first.created).toBe(true);
|
|
136
|
-
|
|
137
|
-
const second = await ensureEpic(cwd, { goalText: 'Fix login button colour issue' });
|
|
126
|
+
const second = await ensureEpic(cwd, { goalText: 'unrelated text', epicId: first.epicId });
|
|
138
127
|
|
|
139
128
|
expect(second.created).toBe(false);
|
|
140
129
|
expect(second.epicId).toBe(first.epicId);
|
|
141
|
-
|
|
142
|
-
const index = readActiveIndex(cwd);
|
|
143
|
-
expect(Object.keys(index.sessions)).toHaveLength(1);
|
|
144
130
|
});
|
|
145
131
|
|
|
146
|
-
test('ensureEpic
|
|
132
|
+
test('ensureEpic refuses to join a completed Epic and does NOT mint a replacement', async () => {
|
|
147
133
|
const cwd = await mkCwd();
|
|
134
|
+
const first = await mint(cwd, { goalText: 'completed epic' });
|
|
135
|
+
const index = readActiveIndex(cwd);
|
|
136
|
+
index.sessions[first.epicId].status = 'completed';
|
|
137
|
+
writeIndex(cwd, index);
|
|
138
|
+
const auditLog = require('../lib/auditLog.cjs');
|
|
148
139
|
|
|
149
|
-
|
|
150
|
-
|
|
140
|
+
await expect(ensureEpic(cwd, { goalText: 'follow-up work', epicId: first.epicId }))
|
|
141
|
+
.rejects.toThrow(/no open Epic to join/);
|
|
151
142
|
|
|
152
|
-
|
|
143
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(1);
|
|
144
|
+
const refusal = readAuditRecordsForCwd(auditLog, cwd).find((r) => r.kind === 'epic_mint_refused');
|
|
145
|
+
expect(refusal).toBeDefined();
|
|
146
|
+
});
|
|
153
147
|
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
test('ensureEpic refuses an unknown explicit epicId rather than minting a sibling', async () => {
|
|
149
|
+
const cwd = await mkCwd();
|
|
156
150
|
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
await expect(ensureEpic(cwd, { goalText: 'do the thing', epicId: 'ticket-abc123' }))
|
|
152
|
+
.rejects.toThrow(/no open Epic to join/);
|
|
153
|
+
|
|
154
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(0);
|
|
159
155
|
});
|
|
160
156
|
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
// A plain object's bracket lookup with a prototype-chain key like
|
|
158
|
+
// "__proto__"/"constructor" returns a truthy Object.prototype member even
|
|
159
|
+
// when no such Epic was ever written — without an own-property check, this
|
|
160
|
+
// would make the join gate falsely report a match.
|
|
161
|
+
for (const pollutedKey of ['__proto__', 'constructor', 'toString', 'hasOwnProperty']) {
|
|
162
|
+
test(`ensureEpic refuses to "join" the prototype-chain key "${pollutedKey}"`, async () => {
|
|
163
|
+
const cwd = await mkCwd();
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
expect(first.created).toBe(true);
|
|
165
|
+
await expect(ensureEpic(cwd, { epicId: pollutedKey })).rejects.toThrow(/no open Epic to join/);
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
goalText: 'Completely unrelated topic about database migrations',
|
|
169
|
-
preferEpicId: first.epicId,
|
|
167
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(0);
|
|
170
168
|
});
|
|
169
|
+
}
|
|
171
170
|
|
|
172
|
-
|
|
173
|
-
});
|
|
171
|
+
// ─── removeEpic ──────────────────────────────────────────────────────────────
|
|
174
172
|
|
|
175
|
-
test('
|
|
173
|
+
test('removeEpic deletes a minted Epic from both sessions and events maps', async () => {
|
|
176
174
|
const cwd = await mkCwd();
|
|
175
|
+
const minted = await mint(cwd, { goalText: 'to be rolled back' });
|
|
177
176
|
|
|
178
|
-
const
|
|
179
|
-
const index = readActiveIndex(cwd);
|
|
180
|
-
index.sessions[first.epicId].status = 'completed';
|
|
181
|
-
fs.writeFileSync(
|
|
182
|
-
path.join(cwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json'),
|
|
183
|
-
JSON.stringify(index, null, 2),
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// No other open Epic shares tokens with the completed one, so the
|
|
187
|
-
// fall-through similarity check finds nothing rather than joining the
|
|
188
|
-
// dead (completed) preferEpicId Epic.
|
|
189
|
-
const joined = findJoinableEpic(cwd, { goalText: 'Fix login button colour issue', preferEpicId: first.epicId });
|
|
177
|
+
const removed = await removeEpic(cwd, minted.epicId);
|
|
190
178
|
|
|
191
|
-
expect(
|
|
179
|
+
expect(removed).toBe(true);
|
|
180
|
+
const index = readActiveIndex(cwd);
|
|
181
|
+
expect(index.sessions[minted.epicId]).toBeUndefined();
|
|
182
|
+
expect(index.events[minted.epicId]).toBeUndefined();
|
|
192
183
|
});
|
|
193
184
|
|
|
194
|
-
test('
|
|
185
|
+
test('removeEpic is a no-op (returns false) for an unknown epicId', async () => {
|
|
195
186
|
const cwd = await mkCwd();
|
|
196
|
-
|
|
187
|
+
await mint(cwd, { goalText: 'unrelated epic' });
|
|
197
188
|
|
|
198
|
-
|
|
189
|
+
expect(await removeEpic(cwd, 'nonexistent-epic-id')).toBe(false);
|
|
190
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(1);
|
|
191
|
+
});
|
|
199
192
|
|
|
200
|
-
|
|
193
|
+
test('removeEpic refuses to report success for the prototype-chain key "__proto__"', async () => {
|
|
194
|
+
const cwd = await mkCwd();
|
|
195
|
+
expect(await removeEpic(cwd, '__proto__')).toBe(false);
|
|
201
196
|
});
|
|
202
197
|
|
|
203
|
-
|
|
198
|
+
// ─── written record ──────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
test('ensureEpic persists the passed source onto the written active-index.json session record', async () => {
|
|
204
201
|
const cwd = await mkCwd();
|
|
202
|
+
const source = { producer: 'new-epic-ui' };
|
|
205
203
|
|
|
206
|
-
const minted = await
|
|
204
|
+
const minted = await mint(cwd, { goalText: 'epic with source', source });
|
|
207
205
|
|
|
208
|
-
|
|
209
|
-
expect(index.sessions[minted.epicId].status).toBe('proposed');
|
|
206
|
+
expect(readActiveIndex(cwd).sessions[minted.epicId].source).toEqual(source);
|
|
210
207
|
});
|
|
211
208
|
|
|
212
|
-
test('ensureEpic
|
|
209
|
+
test('ensureEpic omits source from the written record when not passed', async () => {
|
|
213
210
|
const cwd = await mkCwd();
|
|
214
|
-
const auditLog = require('../lib/auditLog.cjs');
|
|
215
211
|
|
|
216
|
-
await
|
|
217
|
-
ensureEpic(cwd, { goalText: 'should never mint active', status: 'active' }),
|
|
218
|
-
).rejects.toThrow(/born 'proposed'/);
|
|
212
|
+
const minted = await mint(cwd, { goalText: 'sourceless epic' });
|
|
219
213
|
|
|
220
|
-
|
|
221
|
-
expect(Object.keys(index.sessions)).toHaveLength(0);
|
|
222
|
-
|
|
223
|
-
const records = fs.readFileSync(auditLog.AUDIT_LOG_PATH, 'utf8')
|
|
224
|
-
.split('\n')
|
|
225
|
-
.filter(Boolean)
|
|
226
|
-
.map((line) => JSON.parse(line))
|
|
227
|
-
.filter((r) => r.cwd === cwd);
|
|
228
|
-
expect(records).toHaveLength(1);
|
|
229
|
-
expect(records[0].kind).toBe('epic_mint_refused');
|
|
230
|
-
expect(records[0].status).toBe('active');
|
|
214
|
+
expect('source' in readActiveIndex(cwd).sessions[minted.epicId]).toBe(false);
|
|
231
215
|
});
|
|
232
216
|
|
|
233
|
-
test('ensureEpic
|
|
217
|
+
test('ensureEpic mints successfully for a real-world call shape carrying tag, openingPrompt, and source', async () => {
|
|
234
218
|
const cwd = await mkCwd();
|
|
235
|
-
const first = await ensureEpic(cwd, { goalText: 'already running epic' });
|
|
236
|
-
const index = readActiveIndex(cwd);
|
|
237
|
-
index.sessions[first.epicId].status = 'active';
|
|
238
|
-
fs.writeFileSync(
|
|
239
|
-
path.join(cwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json'),
|
|
240
|
-
JSON.stringify(index, null, 2),
|
|
241
|
-
);
|
|
242
219
|
|
|
243
|
-
const
|
|
220
|
+
const minted = await mint(cwd, {
|
|
221
|
+
goalText: 'Fix the merge validation',
|
|
222
|
+
tag: 'bug',
|
|
223
|
+
openingPrompt: 'full opening prompt text',
|
|
224
|
+
source: { producer: 'new-epic-ui' },
|
|
225
|
+
});
|
|
244
226
|
|
|
245
|
-
|
|
246
|
-
expect(
|
|
227
|
+
const session = readActiveIndex(cwd).sessions[minted.epicId];
|
|
228
|
+
expect(session.tag).toBe('bug');
|
|
229
|
+
expect(session.openingPrompt).toBe('full opening prompt text');
|
|
230
|
+
expect(session.source).toEqual({ producer: 'new-epic-ui' });
|
|
247
231
|
});
|
|
248
232
|
|
|
249
|
-
|
|
233
|
+
// ─── BORN-PROPOSED LAW ───────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
test('ensureEpic mints with status "proposed" by default', async () => {
|
|
250
236
|
const cwd = await mkCwd();
|
|
251
|
-
const first = await ensureEpic(cwd, { goalText: 'completed epic' });
|
|
252
|
-
const index = readActiveIndex(cwd);
|
|
253
|
-
index.sessions[first.epicId].status = 'completed';
|
|
254
|
-
fs.writeFileSync(
|
|
255
|
-
path.join(cwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json'),
|
|
256
|
-
JSON.stringify(index, null, 2),
|
|
257
|
-
);
|
|
258
237
|
|
|
259
|
-
const
|
|
238
|
+
const minted = await mint(cwd, { goalText: 'default status epic' });
|
|
260
239
|
|
|
261
|
-
expect(
|
|
262
|
-
expect(second.epicId).not.toBe(first.epicId);
|
|
263
|
-
const after = readActiveIndex(cwd);
|
|
264
|
-
expect(after.sessions[second.epicId].status).toBe('proposed');
|
|
240
|
+
expect(readActiveIndex(cwd).sessions[minted.epicId].status).toBe('proposed');
|
|
265
241
|
});
|
|
266
242
|
|
|
267
|
-
|
|
268
|
-
if (!fs.existsSync(auditLog.AUDIT_LOG_PATH)) return [];
|
|
269
|
-
return fs.readFileSync(auditLog.AUDIT_LOG_PATH, 'utf8')
|
|
270
|
-
.split('\n')
|
|
271
|
-
.filter(Boolean)
|
|
272
|
-
.map((line) => {
|
|
273
|
-
try {
|
|
274
|
-
return JSON.parse(line);
|
|
275
|
-
} catch {
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
})
|
|
279
|
-
.filter((record) => record && record.cwd === cwd);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
test('mintIfMissing:false refusal appends an epic_mint_refused audit event', async () => {
|
|
243
|
+
test('ensureEpic throws when a mint is requested with status "active", and appends an audit event', async () => {
|
|
283
244
|
const cwd = await mkCwd();
|
|
284
245
|
const auditLog = require('../lib/auditLog.cjs');
|
|
285
246
|
|
|
286
|
-
await expect(
|
|
247
|
+
await expect(mint(cwd, { goalText: 'should never mint active', status: 'active' }))
|
|
248
|
+
.rejects.toThrow(/born 'proposed'/);
|
|
287
249
|
|
|
250
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(0);
|
|
288
251
|
const records = readAuditRecordsForCwd(auditLog, cwd);
|
|
289
252
|
expect(records).toHaveLength(1);
|
|
290
253
|
expect(records[0].kind).toBe('epic_mint_refused');
|
|
291
|
-
expect(records[0].
|
|
254
|
+
expect(records[0].status).toBe('active');
|
|
292
255
|
});
|
|
293
256
|
|
|
294
|
-
test('
|
|
257
|
+
test('ensureEpic refuses the mint and appends an epic_mint_refused audit event when the constructed session object fails schema validation', async () => {
|
|
295
258
|
const cwd = await mkCwd();
|
|
296
|
-
const first = await ensureEpic(cwd, { goalText: 'to be completed' });
|
|
297
|
-
const index = readActiveIndex(cwd);
|
|
298
|
-
index.sessions[first.epicId].status = 'completed';
|
|
299
|
-
fs.writeFileSync(
|
|
300
|
-
path.join(cwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json'),
|
|
301
|
-
JSON.stringify(index, null, 2),
|
|
302
|
-
);
|
|
303
259
|
const auditLog = require('../lib/auditLog.cjs');
|
|
260
|
+
const schema = require('../lib/promptSessionSchema.cjs');
|
|
261
|
+
|
|
262
|
+
// The real construction in ensureEpic is hardcoded and always produces a
|
|
263
|
+
// valid PromptSession, so to exercise the fail-closed wiring we simulate
|
|
264
|
+
// a corrupted construction by monkeypatching the schema module's exported
|
|
265
|
+
// validator in place — epicMint.cjs reads `schema.assertValidPromptSession`
|
|
266
|
+
// off the same exports object at call time (property lookup, not a
|
|
267
|
+
// destructured copy), so mutating it here is visible to ensureEpic.
|
|
268
|
+
const original = schema.assertValidPromptSession;
|
|
269
|
+
schema.assertValidPromptSession = () => {
|
|
270
|
+
throw new Error('assertValidPromptSession: invalid PromptSession shape — forced test failure');
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
await expect(mint(cwd, { goalText: 'should be refused by schema validation' }))
|
|
274
|
+
.rejects.toThrow(/forced test failure/);
|
|
275
|
+
} finally {
|
|
276
|
+
schema.assertValidPromptSession = original;
|
|
277
|
+
}
|
|
304
278
|
|
|
305
|
-
|
|
306
|
-
|
|
279
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(0);
|
|
307
280
|
const records = readAuditRecordsForCwd(auditLog, cwd);
|
|
308
|
-
|
|
309
|
-
expect(
|
|
310
|
-
expect(
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test('ensureEpic with forceNewEpic:true mints even when a near-identical open Epic exists', async () => {
|
|
314
|
-
const cwd = await mkCwd();
|
|
315
|
-
|
|
316
|
-
const first = await ensureEpic(cwd, { goalText: 'Fix the login button color' });
|
|
317
|
-
expect(first.created).toBe(true);
|
|
318
|
-
|
|
319
|
-
const second = await ensureEpic(cwd, { goalText: 'Fix login button colour issue', forceNewEpic: true });
|
|
320
|
-
|
|
321
|
-
expect(second.created).toBe(true);
|
|
322
|
-
expect(second.epicId).not.toBe(first.epicId);
|
|
323
|
-
|
|
324
|
-
const index = readActiveIndex(cwd);
|
|
325
|
-
expect(Object.keys(index.sessions)).toHaveLength(2);
|
|
281
|
+
expect(records).toHaveLength(1);
|
|
282
|
+
expect(records[0].kind).toBe('epic_mint_refused');
|
|
283
|
+
expect(records[0].reason).toMatch(/forced test failure/);
|
|
326
284
|
});
|