@monotykamary/pi-vcc 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/__tests__/helpers.ts +0 -135
- package/__tests__/root-causes.test.ts +0 -858
- package/demo.gif +0 -0
- package/knip.json +0 -4
- package/scripts/audit-sessions.ts +0 -88
- package/scripts/benchmark-real-sessions.ts +0 -25
- package/scripts/compare-before-after.ts +0 -36
- package/scripts/dump-branch-output.ts +0 -20
- package/stress-debug.ts +0 -202
- package/stress-degradation-curve.ts +0 -251
- package/tests/australian-government.test.ts +0 -479
- package/tests/before-compact-hook.test.ts +0 -185
- package/tests/before-compact.test.ts +0 -227
- package/tests/breadcrumbs.test.ts +0 -312
- package/tests/brief.test.ts +0 -215
- package/tests/build-sections.test.ts +0 -59
- package/tests/compile.test.ts +0 -89
- package/tests/content.test.ts +0 -66
- package/tests/deep-extraction.test.ts +0 -322
- package/tests/extract-goals.test.ts +0 -86
- package/tests/extract-preferences.test.ts +0 -30
- package/tests/filter-noise.test.ts +0 -61
- package/tests/fixtures.ts +0 -61
- package/tests/format-recall.test.ts +0 -87
- package/tests/format.test.ts +0 -73
- package/tests/government-domain.test.ts +0 -542
- package/tests/lineage.test.ts +0 -33
- package/tests/load-messages.test.ts +0 -49
- package/tests/model-threshold-hook.test.ts +0 -311
- package/tests/model-threshold.test.ts +0 -223
- package/tests/normalize.test.ts +0 -104
- package/tests/proactive-threshold.test.ts +0 -490
- package/tests/real-sessions.test.ts +0 -38
- package/tests/recall-expand.test.ts +0 -15
- package/tests/recall-scope.test.ts +0 -32
- package/tests/recall-tool-scope.test.ts +0 -67
- package/tests/render-entries.test.ts +0 -92
- package/tests/report.test.ts +0 -58
- package/tests/sanitize.test.ts +0 -24
- package/tests/search-entries.test.ts +0 -233
- package/tests/stress-100-compactions.test.ts +0 -373
- package/tests/support/load-session.ts +0 -23
- package/tests/support/real-sessions.ts +0 -51
- package/tests/threshold-integration.test.ts +0 -335
|
@@ -1,479 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { mkdtempSync, writeFileSync, rmSync } from "fs";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import { searchEntries } from "../src/core/search-entries";
|
|
6
|
-
import { formatRecallOutput } from "../src/core/format-recall";
|
|
7
|
-
import { registerRecallTool } from "../src/tools/recall";
|
|
8
|
-
import type { RenderedEntry } from "../src/core/render-entries";
|
|
9
|
-
import type { Message } from "@earendil-works/pi-ai";
|
|
10
|
-
import type { SearchHit } from "../src/core/search-entries";
|
|
11
|
-
|
|
12
|
-
// ── Australian government session fixture ──
|
|
13
|
-
// Uses British/Commonwealth spelling and Australian government frameworks:
|
|
14
|
-
// ISM (Information Security Manual), PSPF (Protective Security Policy Framework),
|
|
15
|
-
// IRAP (Information Security Registered Assessors Program), Essential Eight,
|
|
16
|
-
// ASD (Australian Signals Directorate), DTA (Digital Transformation Agency),
|
|
17
|
-
// Privacy Act 1988, Australian Privacy Principles (APPs)
|
|
18
|
-
|
|
19
|
-
const ausEntries: RenderedEntry[] = [
|
|
20
|
-
{ index: 0, role: "user", summary: "Update the ISM compliance documentation for the new Essential Eight controls" },
|
|
21
|
-
{ index: 1, role: "assistant", summary: "Reviewing the ASD Information Security Manual and Essential Eight maturity model for compliance alignment.", files: ["security/ism-compliance-2027.md"] },
|
|
22
|
-
{ index: 2, role: "tool_result", summary: "[Read] security/ism-compliance-2027.md" },
|
|
23
|
-
{ index: 3, role: "assistant", summary: "The current ISM compliance documentation covers the Essential Eight at Maturity Level 2. I'll update for Level 3 requirements." },
|
|
24
|
-
{ index: 4, role: "user", summary: "Ensure the authorisation controls meet PSPF requirements" },
|
|
25
|
-
{ index: 5, role: "assistant", summary: "The PSPF requires multi-factor authorisation for all privileged access. Updating authorisation policy to align with PSPF Policy 9.", files: ["security/authorisation-policy.md"] },
|
|
26
|
-
{ index: 6, role: "tool_result", summary: "[Edit] security/authorisation-policy.md" },
|
|
27
|
-
{ index: 7, role: "user", summary: "Organise the IRAP assessment for the cloud service" },
|
|
28
|
-
{ index: 8, role: "assistant", summary: "Scheduling IRAP assessment with an ASD-certified assessor for the cloud service. The programme includes documentation review and testing phases." },
|
|
29
|
-
{ index: 9, role: "user", summary: "Check the Privacy Act 1988 compliance for the new data handling procedures" },
|
|
30
|
-
{ index: 10, role: "assistant", summary: "Cross-referencing data handling procedures against Australian Privacy Principles under the Privacy Act 1988. APP 6 and APP 8 require authorisation for overseas disclosure." },
|
|
31
|
-
{ index: 11, role: "user", summary: "Update the protective security policy with the new risk management framework" },
|
|
32
|
-
{ index: 12, role: "assistant", summary: "Updating protective security policy to include the new risk management programme per PSPF Policy 1 and the ANAO guidance.", files: ["security/protective-policy.md"] },
|
|
33
|
-
{ index: 13, role: "tool_result", summary: "[Edit] security/protective-policy.md" },
|
|
34
|
-
{ index: 14, role: "user", summary: "Review the DTA digital service standard compliance" },
|
|
35
|
-
{ index: 15, role: "assistant", summary: "The Digital Transformation Agency service standard requires accessibility, privacy impact assessment, and user-centred design. All 13 criteria are being assessed." },
|
|
36
|
-
{ index: 16, role: "user", summary: "Analyse the cyber security incident response procedures" },
|
|
37
|
-
{ index: 17, role: "assistant", summary: "Analysing incident response procedures against the ASD Cyber Security Centre guidance. The current procedures cover reporting, containment, and recovery. Defence in depth measures are documented." },
|
|
38
|
-
{ index: 18, role: "user", summary: "Submit the security clearance pool documentation for review" },
|
|
39
|
-
{ index: 19, role: "assistant", summary: "Security clearance pool documentation has been submitted for formal review. NV1 and NV2 clearance records are current through the AGSVA programme." },
|
|
40
|
-
{ index: 20, role: "user", summary: "Catalogue the information management framework references" },
|
|
41
|
-
{ index: 21, role: "assistant", summary: "Cataloguing information management references across the National Archives of Australia, Records Authorities, and the Digital Continuity 2025 programme." },
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const ausMessages: Message[] = [
|
|
45
|
-
{ role: "user", content: "Update the ISM compliance documentation for the new Essential Eight controls" } as any,
|
|
46
|
-
{ role: "assistant", content: [{ type: "text", text: "Reviewing the ASD Information Security Manual and Essential Eight maturity model for compliance alignment." }] } as any,
|
|
47
|
-
{ role: "toolResult", content: [{ type: "text", text: "ISM compliance doc: Essential Eight controls at Maturity Level 2" }], toolName: "Read", isError: false } as any,
|
|
48
|
-
{ role: "assistant", content: [{ type: "text", text: "The current ISM compliance documentation covers the Essential Eight at Maturity Level 2. I'll update for Level 3 requirements." }] } as any,
|
|
49
|
-
{ role: "user", content: "Ensure the authorisation controls meet PSPF requirements" } as any,
|
|
50
|
-
{ role: "assistant", content: [{ type: "text", text: "The PSPF requires multi-factor authorisation for all privileged access. Updating authorisation policy to align with PSPF Policy 9." }] } as any,
|
|
51
|
-
{ role: "toolResult", content: [{ type: "text", text: "File edited: updated authorisation section per PSPF Policy 9" }], toolName: "Edit", isError: false } as any,
|
|
52
|
-
{ role: "user", content: "Organise the IRAP assessment for the cloud service" } as any,
|
|
53
|
-
{ role: "assistant", content: [{ type: "text", text: "Scheduling IRAP assessment with an ASD-certified assessor for the cloud service. The programme includes documentation review and testing phases." }] } as any,
|
|
54
|
-
{ role: "user", content: "Check the Privacy Act 1988 compliance for the new data handling procedures" } as any,
|
|
55
|
-
{ role: "assistant", content: [{ type: "text", text: "Cross-referencing data handling procedures against Australian Privacy Principles under the Privacy Act 1988. APP 6 and APP 8 require authorisation for overseas disclosure." }] } as any,
|
|
56
|
-
{ role: "user", content: "Update the protective security policy with the new risk management framework" } as any,
|
|
57
|
-
{ role: "assistant", content: [{ type: "text", text: "Updating protective security policy to include the new risk management programme per PSPF Policy 1 and the ANAO guidance." }] } as any,
|
|
58
|
-
{ role: "toolResult", content: [{ type: "text", text: "File edited: added risk management programme section" }], toolName: "Edit", isError: false } as any,
|
|
59
|
-
{ role: "user", content: "Review the DTA digital service standard compliance" } as any,
|
|
60
|
-
{ role: "assistant", content: [{ type: "text", text: "The Digital Transformation Agency service standard requires accessibility, privacy impact assessment, and user-centred design. All 13 criteria are being assessed." }] } as any,
|
|
61
|
-
{ role: "user", content: "Analyse the cyber security incident response procedures" } as any,
|
|
62
|
-
{ role: "assistant", content: [{ type: "text", text: "Analysing incident response procedures against the ASD Cyber Security Centre guidance. The current procedures cover reporting, containment, and recovery. Defence in depth measures are documented." }] } as any,
|
|
63
|
-
{ role: "user", content: "Submit the security clearance pool documentation for review" } as any,
|
|
64
|
-
{ role: "assistant", content: [{ type: "text", text: "Security clearance pool documentation has been submitted for formal review. NV1 and NV2 clearance records are current through the AGSVA programme." }] } as any,
|
|
65
|
-
{ role: "user", content: "Catalogue the information management framework references" } as any,
|
|
66
|
-
{ role: "assistant", content: [{ type: "text", text: "Cataloguing information management references across the National Archives of Australia, Records Authorities, and the Digital Continuity 2025 programme." }] } as any,
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
// ── Helpers ──
|
|
70
|
-
|
|
71
|
-
const register = () => {
|
|
72
|
-
let tool: any;
|
|
73
|
-
registerRecallTool({ registerTool: (t: any) => { tool = t; } } as any);
|
|
74
|
-
return tool;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const makeAusSession = () => {
|
|
78
|
-
const dir = mkdtempSync(join(tmpdir(), "pi-vcc-aus-gov-"));
|
|
79
|
-
const file = join(dir, "session.jsonl");
|
|
80
|
-
const lines = ausMessages.map((msg, i) =>
|
|
81
|
-
JSON.stringify({ type: "message", id: `m${i}`, message: msg })
|
|
82
|
-
);
|
|
83
|
-
writeFileSync(file, lines.join("\n") + "\n", "utf8");
|
|
84
|
-
return { dir, file };
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const invoke = async (tool: any, file: string, params: Record<string, unknown>, branchIds?: string[]) => {
|
|
88
|
-
const ids = branchIds ?? ausMessages.map((_, i) => `m${i}`);
|
|
89
|
-
const result = await tool.execute("tool-call", params, undefined, undefined, {
|
|
90
|
-
sessionManager: {
|
|
91
|
-
getSessionFile: () => file,
|
|
92
|
-
getBranch: () => ids.map((id) => ({ id })),
|
|
93
|
-
getEntries: () => ids.map((id) => ({ id })),
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
return result.content[0].text as string;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// ── Tests ──
|
|
100
|
-
|
|
101
|
-
describe("Australian government: spelling variant search", () => {
|
|
102
|
-
it("US 'authorization' finds British 'authorisation'", () => {
|
|
103
|
-
const r = searchEntries(ausEntries, ausMessages, "authorization");
|
|
104
|
-
expect(r.length).toBeGreaterThanOrEqual(2);
|
|
105
|
-
// Should match entries #4 and #5 (authorisation controls and PSPF)
|
|
106
|
-
expect(r.some((h) => h.index === 4 || h.index === 5)).toBe(true);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("British 'authorisation' also finds British 'authorisation'", () => {
|
|
110
|
-
const r = searchEntries(ausEntries, ausMessages, "authorisation");
|
|
111
|
-
expect(r.length).toBeGreaterThanOrEqual(2);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("US 'organize' finds British 'organise'", () => {
|
|
115
|
-
const entries: RenderedEntry[] = [
|
|
116
|
-
{ index: 0, role: "user", summary: "Organise the security review" },
|
|
117
|
-
{ index: 1, role: "assistant", summary: "Arranging the security review schedule" },
|
|
118
|
-
];
|
|
119
|
-
const msgs: Message[] = entries.map((e) => ({
|
|
120
|
-
role: e.role === "user" ? "user" : "assistant" as any,
|
|
121
|
-
content: e.summary,
|
|
122
|
-
} as any));
|
|
123
|
-
|
|
124
|
-
const r = searchEntries(entries, msgs, "organize");
|
|
125
|
-
expect(r).toHaveLength(1);
|
|
126
|
-
expect(r[0].index).toBe(0);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("US 'analyze' finds British 'analyse'", () => {
|
|
130
|
-
// Entry #16 has "Analyse", entry #17 has "Analysing"
|
|
131
|
-
const r = searchEntries(ausEntries, ausMessages, "analyze");
|
|
132
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
133
|
-
expect(r.some((h) => h.index === 16 || h.index === 17)).toBe(true);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("US 'program' finds British 'programme'", () => {
|
|
137
|
-
// Entries #8, #12, #19, #21 use "programme"
|
|
138
|
-
const r = searchEntries(ausEntries, ausMessages, "program");
|
|
139
|
-
expect(r.length).toBeGreaterThanOrEqual(2);
|
|
140
|
-
expect(r.some((h) => [8, 12, 19, 21].includes(h.index))).toBe(true);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("US 'defense' finds British 'defence'", () => {
|
|
144
|
-
// Entry #17 mentions "Defence in depth"
|
|
145
|
-
const r = searchEntries(ausEntries, ausMessages, "defense");
|
|
146
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("US 'color' finds British 'colour' in government context", () => {
|
|
150
|
-
const entries: RenderedEntry[] = [
|
|
151
|
-
{ index: 0, role: "user", summary: "Update the colour coding for security classifications" },
|
|
152
|
-
{ index: 1, role: "assistant", summary: "Updating colour coding: TOP SECRET (red), SECRET (blue), PROTECTED (green)" },
|
|
153
|
-
];
|
|
154
|
-
const msgs: Message[] = entries.map((e) => ({
|
|
155
|
-
role: e.role === "user" ? "user" : "assistant" as any,
|
|
156
|
-
content: e.summary,
|
|
157
|
-
} as any));
|
|
158
|
-
|
|
159
|
-
const r = searchEntries(entries, msgs, "color");
|
|
160
|
-
expect(r).toHaveLength(2);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("US 'catalog' finds British 'catalogue'", () => {
|
|
164
|
-
// Entry #20 has "Catalogue"
|
|
165
|
-
const r = searchEntries(ausEntries, ausMessages, "catalog");
|
|
166
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
167
|
-
expect(r.some((h) => h.index === 20 || h.index === 21)).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("US 'license' finds British 'licence' in government context", () => {
|
|
171
|
-
const entries: RenderedEntry[] = [
|
|
172
|
-
{ index: 0, role: "user", summary: "Check the software licence compliance" },
|
|
173
|
-
{ index: 1, role: "assistant", summary: "All software licences are current and compliant." },
|
|
174
|
-
];
|
|
175
|
-
const msgs: Message[] = entries.map((e) => ({
|
|
176
|
-
role: e.role === "user" ? "user" : "assistant" as any,
|
|
177
|
-
content: e.summary,
|
|
178
|
-
} as any));
|
|
179
|
-
|
|
180
|
-
// US spelling "license" should match British "licence"
|
|
181
|
-
const r = searchEntries(entries, msgs, "license");
|
|
182
|
-
expect(r).toHaveLength(2);
|
|
183
|
-
|
|
184
|
-
// British "licence" should also work
|
|
185
|
-
const r2 = searchEntries(entries, msgs, "licence");
|
|
186
|
-
expect(r2).toHaveLength(2);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("multi-term British spelling query crosses US/British boundary", () => {
|
|
190
|
-
// Query uses US spellings, data uses British
|
|
191
|
-
const r = searchEntries(ausEntries, ausMessages, "authorization PSPF compliance");
|
|
192
|
-
// 3 meaningful terms → MIN_TERM_MATCH_FOR_MULTITERM=2
|
|
193
|
-
// "authorization" matches "authorisation", "PSPF" is specific, "compliance" is common
|
|
194
|
-
expect(r.length).toBeGreaterThan(0);
|
|
195
|
-
expect(r.some((h) => h.index === 4 || h.index === 5)).toBe(true);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("spelling expansion does not affect regex queries", () => {
|
|
199
|
-
// Regex queries (containing metacharacters) are passed through as-is;
|
|
200
|
-
// spelling expansion only applies to natural language terms.
|
|
201
|
-
const entries: RenderedEntry[] = [
|
|
202
|
-
{ index: 0, role: "user", summary: "authorisation required" },
|
|
203
|
-
{ index: 1, role: "user", summary: "authorization pending" },
|
|
204
|
-
];
|
|
205
|
-
const msgs: Message[] = entries.map((e) => ({
|
|
206
|
-
role: "user" as any, content: e.summary,
|
|
207
|
-
} as any));
|
|
208
|
-
|
|
209
|
-
// Natural language query (no metacharacters) → expansion applied → finds both
|
|
210
|
-
const rNl = searchEntries(entries, msgs, "authorisation");
|
|
211
|
-
expect(rNl).toHaveLength(2);
|
|
212
|
-
|
|
213
|
-
// Regex query (| metacharacter) → NO expansion → both match via explicit alternation
|
|
214
|
-
const rRegex = searchEntries(entries, msgs, "authorisation|authorization");
|
|
215
|
-
expect(rRegex).toHaveLength(2);
|
|
216
|
-
|
|
217
|
-
// Regex query matching only British spelling → should NOT auto-expand
|
|
218
|
-
// Using [is]ation as a regex class to match only "isation" literal
|
|
219
|
-
const rRegexOnlyIse = searchEntries(entries, msgs, "authoris[ai]tion");
|
|
220
|
-
expect(rRegexOnlyIse).toHaveLength(1);
|
|
221
|
-
expect(rRegexOnlyIse[0].index).toBe(0); // matches British only
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe("Australian government: searchEntries", () => {
|
|
226
|
-
it("high-overlap Australian government vocabulary stays bounded", () => {
|
|
227
|
-
// "security policy compliance review" — all common in Australian gov sessions
|
|
228
|
-
const r = searchEntries(ausEntries, ausMessages, "security policy compliance review");
|
|
229
|
-
expect(r.length).toBeLessThan(ausEntries.length);
|
|
230
|
-
expect(r.length).toBeGreaterThan(0);
|
|
231
|
-
// With 4 meaningful terms → ≥2 matches required
|
|
232
|
-
for (const hit of r) {
|
|
233
|
-
expect(hit.matchCount).toBeGreaterThanOrEqual(2);
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("specific Australian framework term returns narrow results", () => {
|
|
238
|
-
// "IRAP" is specific to Australian government, appears once
|
|
239
|
-
const r = searchEntries(ausEntries, ausMessages, "IRAP");
|
|
240
|
-
expect(r.length).toBeLessThanOrEqual(3);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it("Essential Eight query returns targeted results", () => {
|
|
244
|
-
const r = searchEntries(ausEntries, ausMessages, "Essential Eight");
|
|
245
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
246
|
-
expect(r.some((h) => h.index === 0 || h.index === 1)).toBe(true);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("Australian Privacy Act specific search", () => {
|
|
250
|
-
const r = searchEntries(ausEntries, ausMessages, "Privacy Act 1988");
|
|
251
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
252
|
-
expect(r.some((h) => h.index === 9 || h.index === 10)).toBe(true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("PSPF policy reference search", () => {
|
|
256
|
-
const r = searchEntries(ausEntries, ausMessages, "PSPF Policy");
|
|
257
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
258
|
-
// PSPF Policy 9 is in #5, PSPF Policy 1 is in #12
|
|
259
|
-
expect(r.some((h) => [5, 12].includes(h.index))).toBe(true);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("AGSVA clearance query returns targeted results", () => {
|
|
263
|
-
const r = searchEntries(ausEntries, ausMessages, "AGSVA");
|
|
264
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
265
|
-
expect(r.some((h) => h.index === 19)).toBe(true);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("DTA digital service standard search", () => {
|
|
269
|
-
const r = searchEntries(ausEntries, ausMessages, "Digital Transformation Agency");
|
|
270
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
271
|
-
expect(r.some((h) => h.index === 15)).toBe(true);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("National Archives of Australia reference search", () => {
|
|
275
|
-
const r = searchEntries(ausEntries, ausMessages, "National Archives");
|
|
276
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
277
|
-
expect(r.some((h) => h.index === 21)).toBe(true);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("ANAO audit reference search", () => {
|
|
281
|
-
const r = searchEntries(ausEntries, ausMessages, "ANAO");
|
|
282
|
-
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
283
|
-
expect(r.some((h) => h.index === 12)).toBe(true);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("Australian government session: BM25 ranks specific framework hits highest", () => {
|
|
287
|
-
// A query with both specific (ASD) and generic (security) terms
|
|
288
|
-
const r = searchEntries(ausEntries, ausMessages, "ASD security guidance");
|
|
289
|
-
// 3 terms → ≥2 required. "ASD" is rare (high IDF), "security" is common (low IDF).
|
|
290
|
-
// Top result should prioritise the ASD-specific entries.
|
|
291
|
-
expect(r.length).toBeGreaterThan(0);
|
|
292
|
-
// First result should be an ASD-heavy entry
|
|
293
|
-
expect(r[0].matchCount).toBeGreaterThanOrEqual(2);
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
describe("Australian government: vcc_recall integration", () => {
|
|
298
|
-
it("US-spelled query finds British content through integration", async () => {
|
|
299
|
-
const { dir, file } = makeAusSession();
|
|
300
|
-
try {
|
|
301
|
-
const tool = register();
|
|
302
|
-
|
|
303
|
-
// Model searching with "authorization" against British "authorisation"
|
|
304
|
-
const result = await invoke(tool, file, { query: "authorization PSPF" });
|
|
305
|
-
expect(result).toContain("matches");
|
|
306
|
-
// Should have found the British-spelled entries
|
|
307
|
-
expect(result).toMatch(/authorisation|PSPF/i);
|
|
308
|
-
} finally {
|
|
309
|
-
rmSync(dir, { recursive: true, force: true });
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it("Australian framework term returns paginated results", async () => {
|
|
314
|
-
const { dir, file } = makeAusSession();
|
|
315
|
-
try {
|
|
316
|
-
const tool = register();
|
|
317
|
-
|
|
318
|
-
const result = await invoke(tool, file, { query: "security compliance policy review" });
|
|
319
|
-
// Should show results, not dump the whole session
|
|
320
|
-
expect(result).toMatch(/matches/);
|
|
321
|
-
} finally {
|
|
322
|
-
rmSync(dir, { recursive: true, force: true });
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("IRAP search returns narrow results through tool", async () => {
|
|
327
|
-
const { dir, file } = makeAusSession();
|
|
328
|
-
try {
|
|
329
|
-
const tool = register();
|
|
330
|
-
|
|
331
|
-
const result = await invoke(tool, file, { query: "IRAP assessment" });
|
|
332
|
-
expect(result).toContain("matches");
|
|
333
|
-
expect(result).toMatch(/IRAP/);
|
|
334
|
-
} finally {
|
|
335
|
-
rmSync(dir, { recursive: true, force: true });
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it("Essential Eight search finds ASD framework entries", async () => {
|
|
340
|
-
const { dir, file } = makeAusSession();
|
|
341
|
-
try {
|
|
342
|
-
const tool = register();
|
|
343
|
-
|
|
344
|
-
const result = await invoke(tool, file, { query: "Essential Eight controls" });
|
|
345
|
-
expect(result).toContain("matches");
|
|
346
|
-
expect(result).toMatch(/Essential Eight|ISM/i);
|
|
347
|
-
} finally {
|
|
348
|
-
rmSync(dir, { recursive: true, force: true });
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it("search for 'organise' also finds entries via British spelling", async () => {
|
|
353
|
-
const { dir, file } = makeAusSession();
|
|
354
|
-
try {
|
|
355
|
-
const tool = register();
|
|
356
|
-
|
|
357
|
-
// Both spellings should find #7/#8 (organise/organize)
|
|
358
|
-
const result = await invoke(tool, file, { query: "organize IRAP" });
|
|
359
|
-
expect(result).toMatch(/matches|IRAP/);
|
|
360
|
-
} finally {
|
|
361
|
-
rmSync(dir, { recursive: true, force: true });
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
describe("Australian government: stress scenarios", () => {
|
|
367
|
-
it("defence portfolio session with Commonwealth spelling", () => {
|
|
368
|
-
const entries: RenderedEntry[] = [
|
|
369
|
-
{ index: 0, role: "user", summary: "Review the Defence procurement authorisation framework" },
|
|
370
|
-
{ index: 1, role: "assistant", summary: "Analysing the Defence procurement authorisation framework against the Commonwealth Procurement Rules." },
|
|
371
|
-
{ index: 2, role: "user", summary: "Check behaviour against the Defence behaviour and values framework" },
|
|
372
|
-
{ index: 3, role: "assistant", summary: "Behaviour standards are set by the Defence Values and Behaviour Framework. All personnel must adhere." },
|
|
373
|
-
{ index: 4, role: "user", summary: "Update the catalogue of defence equipment authorisations" },
|
|
374
|
-
{ index: 5, role: "assistant", summary: "Cataloguing defence equipment authorisations per the Defence and Industry Programme. Honour system requirements apply." },
|
|
375
|
-
{ index: 6, role: "user", summary: "Organise the offshore licence compliance review" },
|
|
376
|
-
{ index: 7, role: "assistant", summary: "Organising offshore licence compliance review against the Foreign Investment Review Board requirements and Defence Trade Controls Act." },
|
|
377
|
-
];
|
|
378
|
-
const msgs: Message[] = entries.map((e) => ({
|
|
379
|
-
role: e.role === "user" ? "user" : "assistant" as any,
|
|
380
|
-
content: e.summary,
|
|
381
|
-
} as any));
|
|
382
|
-
|
|
383
|
-
// US spelling → British hits
|
|
384
|
-
expect(searchEntries(entries, msgs, "authorization").length).toBeGreaterThanOrEqual(2);
|
|
385
|
-
expect(searchEntries(entries, msgs, "defense").length).toBeGreaterThanOrEqual(2);
|
|
386
|
-
expect(searchEntries(entries, msgs, "behavior").length).toBeGreaterThanOrEqual(2);
|
|
387
|
-
expect(searchEntries(entries, msgs, "catalog").length).toBeGreaterThanOrEqual(2);
|
|
388
|
-
expect(searchEntries(entries, msgs, "organize").length).toBeGreaterThanOrEqual(1);
|
|
389
|
-
expect(searchEntries(entries, msgs, "license").length).toBeGreaterThanOrEqual(1);
|
|
390
|
-
|
|
391
|
-
// Mixed spelling query: terms from both conventions
|
|
392
|
-
const r = searchEntries(entries, msgs, "authorization defense behavior");
|
|
393
|
-
// 3 terms → requires ≥2. "authorization" matches authorisation,
|
|
394
|
-
// "defense" matches defence, "behavior" matches behaviour.
|
|
395
|
-
expect(r.length).toBeGreaterThan(0);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it("state government session with mixed US/British spelling", () => {
|
|
399
|
-
// Government workers often mix spellings: "authorisation" in policy
|
|
400
|
-
// documents but "authorization" in technical contexts (OAuth, etc.)
|
|
401
|
-
const entries: RenderedEntry[] = [
|
|
402
|
-
{ index: 0, role: "user", summary: "Check the OAuth authorization server configuration" },
|
|
403
|
-
{ index: 1, role: "assistant", summary: "The OAuth authorization server is configured per standard. Authorisation for API access follows PSPF guidelines." },
|
|
404
|
-
{ index: 2, role: "user", summary: "Update the data classification programme documentation" },
|
|
405
|
-
{ index: 3, role: "assistant", summary: "Updating the data classification program per ISM guidance. Both programme and program references are retained for compatibility." },
|
|
406
|
-
];
|
|
407
|
-
const msgs: Message[] = entries.map((e) => ({
|
|
408
|
-
role: e.role === "user" ? "user" : "assistant" as any,
|
|
409
|
-
content: e.summary,
|
|
410
|
-
} as any));
|
|
411
|
-
|
|
412
|
-
// Either spelling should find the relevant entries
|
|
413
|
-
const fromUs = searchEntries(entries, msgs, "authorization");
|
|
414
|
-
const fromBr = searchEntries(entries, msgs, "authorisation");
|
|
415
|
-
// Both should find entries mentioning authorization/authorisation
|
|
416
|
-
expect(fromUs.length).toBeGreaterThanOrEqual(1);
|
|
417
|
-
expect(fromBr.length).toBeGreaterThanOrEqual(1);
|
|
418
|
-
|
|
419
|
-
// programme/program cross-match
|
|
420
|
-
const programUs = searchEntries(entries, msgs, "program");
|
|
421
|
-
expect(programUs.length).toBeGreaterThanOrEqual(2);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("large Australian government session stays bounded", () => {
|
|
425
|
-
// Simulate a 150-entry Australian government session with heavy
|
|
426
|
-
// domain vocabulary overlap and Commonwealth spelling
|
|
427
|
-
const bigEntries: RenderedEntry[] = Array.from({ length: 150 }, (_, i) => ({
|
|
428
|
-
index: i,
|
|
429
|
-
role: (i % 2 === 0 ? "user" : "assistant") as any,
|
|
430
|
-
summary: i % 2 === 0
|
|
431
|
-
? `Review security policy compliance authorisation for department ${Math.floor(i / 2)}`
|
|
432
|
-
: `Processed defence programme policy compliance review for section ${Math.floor(i / 2)}`,
|
|
433
|
-
}));
|
|
434
|
-
const bigMsgs: Message[] = bigEntries.map((e) => ({
|
|
435
|
-
role: e.role === "user" ? "user" : "assistant" as any,
|
|
436
|
-
content: e.summary,
|
|
437
|
-
} as any));
|
|
438
|
-
|
|
439
|
-
// US-spelled query against British-spelled data
|
|
440
|
-
const rUs = searchEntries(bigEntries, bigMsgs, "authorization defense program");
|
|
441
|
-
expect(rUs.length).toBeLessThanOrEqual(50); // MAX_SEARCH_RESULTS
|
|
442
|
-
|
|
443
|
-
// British-spelled query
|
|
444
|
-
const rBr = searchEntries(bigEntries, bigMsgs, "authorisation defence programme");
|
|
445
|
-
expect(rBr.length).toBeLessThanOrEqual(50);
|
|
446
|
-
|
|
447
|
-
// Both should return similar counts (US/British should be transparent)
|
|
448
|
-
expect(Math.abs(rUs.length - rBr.length)).toBeLessThanOrEqual(5);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("ASD Essential Eight maturity model search", () => {
|
|
452
|
-
const entries: RenderedEntry[] = [
|
|
453
|
-
{ index: 0, role: "user", summary: "Assess Essential Eight Maturity Level 3 compliance" },
|
|
454
|
-
{ index: 1, role: "assistant", summary: "Assessing Essential Eight Maturity Level 3: application control, patch management, and multi-factor authentication requirements per ASD guidance." },
|
|
455
|
-
{ index: 2, role: "user", summary: "What are the gaps at Maturity Level 2?" },
|
|
456
|
-
{ index: 3, role: "assistant", summary: "At Maturity Level 2 gaps include: application control partial coverage, Microsoft Office macro configuration, and operating system patch authorisation delays." },
|
|
457
|
-
{ index: 4, role: "user", summary: "Organise the remediation programme" },
|
|
458
|
-
{ index: 5, role: "assistant", summary: "Organising the Essential Eight remediation programme. Priority: macro configuration and patch management authorisation workflows." },
|
|
459
|
-
];
|
|
460
|
-
const msgs: Message[] = entries.map((e) => ({
|
|
461
|
-
role: e.role === "user" ? "user" : "assistant" as any,
|
|
462
|
-
content: e.summary,
|
|
463
|
-
} as any));
|
|
464
|
-
|
|
465
|
-
// US spelling "authorization" matching British "authorisation"
|
|
466
|
-
const r = searchEntries(entries, msgs, "Essential Eight authorization");
|
|
467
|
-
// "Essential" and "Eight" are specific, "authorization" matches "authorisation"
|
|
468
|
-
expect(r.length).toBeGreaterThan(0);
|
|
469
|
-
|
|
470
|
-
// Specific framework search
|
|
471
|
-
const lvl3 = searchEntries(entries, msgs, "Maturity Level 3");
|
|
472
|
-
expect(lvl3.length).toBeGreaterThanOrEqual(1);
|
|
473
|
-
expect(lvl3.some((h) => h.index === 0 || h.index === 1)).toBe(true);
|
|
474
|
-
|
|
475
|
-
// "organize" (US) matches "organise" (British) in entry #4/#5
|
|
476
|
-
const org = searchEntries(entries, msgs, "organize remediation");
|
|
477
|
-
expect(org.length).toBeGreaterThanOrEqual(1);
|
|
478
|
-
});
|
|
479
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "bun:test";
|
|
2
|
-
import { existsSync, unlinkSync, writeFileSync, readFileSync, mkdtempSync, rmSync } from "fs";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import { registerBeforeCompactHook, PI_VCC_COMPACT_INSTRUCTION } from "../src/hooks/before-compact";
|
|
6
|
-
|
|
7
|
-
let tmpDir: string;
|
|
8
|
-
let CONFIG_PATH: string;
|
|
9
|
-
const DEBUG_PATH = "/tmp/pi-vcc-debug.json";
|
|
10
|
-
|
|
11
|
-
beforeAll(() => {
|
|
12
|
-
tmpDir = mkdtempSync(join(tmpdir(), "pi-vcc-test-"));
|
|
13
|
-
CONFIG_PATH = join(tmpDir, "pi-vcc-config.json");
|
|
14
|
-
process.env.PI_VCC_CONFIG_PATH = CONFIG_PATH;
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterAll(() => {
|
|
18
|
-
delete process.env.PI_VCC_CONFIG_PATH;
|
|
19
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// Minimal ExtensionAPI stub: capture handler + provide ctx with mocked ui.notify
|
|
23
|
-
function createMockPi() {
|
|
24
|
-
let handler: ((event: any, ctx: any) => any) | undefined;
|
|
25
|
-
const notifyCalls: Array<{ msg: string; level: string }> = [];
|
|
26
|
-
const ctx = {
|
|
27
|
-
hasUI: true,
|
|
28
|
-
ui: {
|
|
29
|
-
notify: (msg: string, level: string) => {
|
|
30
|
-
notifyCalls.push({ msg, level });
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
return {
|
|
35
|
-
pi: {
|
|
36
|
-
on: (eventName: string, h: (e: any, c: any) => any) => {
|
|
37
|
-
if (eventName === "session_before_compact") handler = h;
|
|
38
|
-
},
|
|
39
|
-
} as any,
|
|
40
|
-
invoke: (event: any) => handler!(event, ctx),
|
|
41
|
-
notifyCalls,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function setConfig(cfg: Record<string, unknown>) {
|
|
46
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(cfg));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function makeEvent(branchEntries: any[], customInstructions?: string) {
|
|
50
|
-
return {
|
|
51
|
-
type: "session_before_compact",
|
|
52
|
-
customInstructions,
|
|
53
|
-
branchEntries,
|
|
54
|
-
preparation: {
|
|
55
|
-
previousSummary: undefined,
|
|
56
|
-
fileOps: { read: [], written: [], edited: [] },
|
|
57
|
-
tokensBefore: 1000,
|
|
58
|
-
},
|
|
59
|
-
signal: new AbortController().signal,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const msg = (id: string, role: "user" | "assistant" | "toolResult", content = "x") => ({
|
|
64
|
-
id,
|
|
65
|
-
type: "message",
|
|
66
|
-
message: { role, content },
|
|
67
|
-
});
|
|
68
|
-
const comp = (id: string, firstKeptEntryId?: string) => ({ id, type: "compaction", firstKeptEntryId });
|
|
69
|
-
|
|
70
|
-
describe("registerBeforeCompactHook: cancel paths", () => {
|
|
71
|
-
beforeEach(() => {
|
|
72
|
-
if (existsSync(DEBUG_PATH)) unlinkSync(DEBUG_PATH);
|
|
73
|
-
});
|
|
74
|
-
afterEach(() => {
|
|
75
|
-
if (existsSync(CONFIG_PATH)) unlinkSync(CONFIG_PATH);
|
|
76
|
-
if (existsSync(DEBUG_PATH)) unlinkSync(DEBUG_PATH);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("/pi-vcc with too few live messages cancels and notifies warning", () => {
|
|
80
|
-
setConfig({ debug: false, overrideDefaultCompaction: false });
|
|
81
|
-
const { pi, invoke, notifyCalls } = createMockPi();
|
|
82
|
-
registerBeforeCompactHook(pi);
|
|
83
|
-
|
|
84
|
-
const entries = [msg("m1", "user"), msg("m2", "assistant")];
|
|
85
|
-
expect(invoke(makeEvent(entries, PI_VCC_COMPACT_INSTRUCTION))).toEqual({ cancel: true });
|
|
86
|
-
expect(notifyCalls).toHaveLength(1);
|
|
87
|
-
expect(notifyCalls[0].level).toBe("warning");
|
|
88
|
-
expect(notifyCalls[0].msg).toContain("Too few messages");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("/pi-vcc with no user message compacts all instead of cancelling", () => {
|
|
92
|
-
setConfig({ debug: false, overrideDefaultCompaction: false });
|
|
93
|
-
const { pi, invoke, notifyCalls } = createMockPi();
|
|
94
|
-
registerBeforeCompactHook(pi);
|
|
95
|
-
|
|
96
|
-
const entries = [msg("m1", "assistant"), msg("m2", "assistant"), msg("m3", "assistant")];
|
|
97
|
-
const result = invoke(makeEvent(entries, PI_VCC_COMPACT_INSTRUCTION));
|
|
98
|
-
// No longer cancels — compacts all to recover from context overflow
|
|
99
|
-
expect(result.cancel).toBeUndefined();
|
|
100
|
-
expect(result.compaction).toBeDefined();
|
|
101
|
-
expect(result.compaction.firstKeptEntryId).toBe("");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("/compact with override=true cancels and notifies (NEW: was silent before)", () => {
|
|
105
|
-
setConfig({ debug: false, overrideDefaultCompaction: true });
|
|
106
|
-
const { pi, invoke, notifyCalls } = createMockPi();
|
|
107
|
-
registerBeforeCompactHook(pi);
|
|
108
|
-
|
|
109
|
-
const entries = [msg("m1", "user"), msg("m2", "assistant")];
|
|
110
|
-
expect(invoke(makeEvent(entries, undefined))).toEqual({ cancel: true });
|
|
111
|
-
expect(notifyCalls).toHaveLength(1);
|
|
112
|
-
expect(notifyCalls[0].level).toBe("warning");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("/compact with override=false short-circuits (no notify, returns undefined)", () => {
|
|
116
|
-
setConfig({ debug: false, overrideDefaultCompaction: false });
|
|
117
|
-
const { pi, invoke, notifyCalls } = createMockPi();
|
|
118
|
-
registerBeforeCompactHook(pi);
|
|
119
|
-
|
|
120
|
-
const entries = [msg("m1", "user"), msg("m2", "assistant")];
|
|
121
|
-
expect(invoke(makeEvent(entries, undefined))).toBeUndefined();
|
|
122
|
-
expect(notifyCalls).toHaveLength(0);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("debug:true writes metrics-only snapshot on cancel with no content leakage", () => {
|
|
126
|
-
setConfig({ debug: true, overrideDefaultCompaction: false });
|
|
127
|
-
const { pi, invoke } = createMockPi();
|
|
128
|
-
registerBeforeCompactHook(pi);
|
|
129
|
-
|
|
130
|
-
// Use too_few_live_messages cancel path to test content leakage
|
|
131
|
-
const entries = [
|
|
132
|
-
msg("m1", "user", "SECRET_TOKEN_abc123"),
|
|
133
|
-
msg("m2", "assistant", "sensitive response"),
|
|
134
|
-
];
|
|
135
|
-
expect(invoke(makeEvent(entries, PI_VCC_COMPACT_INSTRUCTION))).toEqual({ cancel: true });
|
|
136
|
-
|
|
137
|
-
expect(existsSync(DEBUG_PATH)).toBe(true);
|
|
138
|
-
const snapshot = JSON.parse(readFileSync(DEBUG_PATH, "utf-8"));
|
|
139
|
-
expect(snapshot.cancelled).toBe(true);
|
|
140
|
-
expect(snapshot.reason).toBe("too_few_live_messages");
|
|
141
|
-
|
|
142
|
-
// No content leakage
|
|
143
|
-
const serialized = JSON.stringify(snapshot);
|
|
144
|
-
expect(serialized).not.toContain("SECRET_TOKEN_abc123");
|
|
145
|
-
expect(serialized).not.toContain("sensitive response");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("debug:false does NOT write snapshot", () => {
|
|
149
|
-
setConfig({ debug: false, overrideDefaultCompaction: false });
|
|
150
|
-
const { pi, invoke } = createMockPi();
|
|
151
|
-
registerBeforeCompactHook(pi);
|
|
152
|
-
const entries = [msg("m1", "user"), msg("m2", "assistant")];
|
|
153
|
-
expect(invoke(makeEvent(entries, PI_VCC_COMPACT_INSTRUCTION))).toEqual({ cancel: true });
|
|
154
|
-
expect(existsSync(DEBUG_PATH)).toBe(false);
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe("registerBeforeCompactHook: compact-all path", () => {
|
|
159
|
-
beforeEach(() => {
|
|
160
|
-
if (existsSync(DEBUG_PATH)) unlinkSync(DEBUG_PATH);
|
|
161
|
-
});
|
|
162
|
-
afterEach(() => {
|
|
163
|
-
if (existsSync(CONFIG_PATH)) unlinkSync(CONFIG_PATH);
|
|
164
|
-
if (existsSync(DEBUG_PATH)) unlinkSync(DEBUG_PATH);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("single-user + autonomous tail → cuts at mid-cycle boundary", () => {
|
|
168
|
-
setConfig({ debug: false, overrideDefaultCompaction: false });
|
|
169
|
-
const { pi, invoke, notifyCalls } = createMockPi();
|
|
170
|
-
registerBeforeCompactHook(pi);
|
|
171
|
-
|
|
172
|
-
const entries = [
|
|
173
|
-
msg("m1", "user", "go"),
|
|
174
|
-
msg("m2", "assistant", "calling tool"),
|
|
175
|
-
msg("m3", "toolResult", "result"),
|
|
176
|
-
msg("m4", "assistant", "done"),
|
|
177
|
-
];
|
|
178
|
-
const result = invoke(makeEvent(entries, PI_VCC_COMPACT_INSTRUCTION));
|
|
179
|
-
expect(result.compaction).toBeDefined();
|
|
180
|
-
// Single user at idx 0, completed cycle m2→m3 ends at idx 2 (midpoint=2)
|
|
181
|
-
// Cut after m3, keep from m4 onward
|
|
182
|
-
expect(result.compaction.firstKeptEntryId).toBe("m4");
|
|
183
|
-
expect(notifyCalls).toHaveLength(0); // no cancel notify on success
|
|
184
|
-
});
|
|
185
|
-
});
|