@tryinget/pi-agent-vent 0.2.2 → 0.3.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/docs/engineering.local.md +1 -1
- package/package.json +9 -10
- package/policy/engineering-lane.json +11 -5
- package/scripts/release-check.sh +19 -77
- package/scripts/release-smoke.sh +7 -3
- package/scripts/validate-structure.mjs +4 -3
- package/scripts/validate-structure.sh +0 -2
- package/tests/vent-store-curation.test.js +427 -0
- package/tests/vent-store-diagnostics.test.js +148 -0
- package/tests/vent-store-lifecycle.test.js +245 -0
- package/tests/vent-store-records.test.js +304 -0
- package/tests/vent-store-retention.test.js +606 -0
- package/tests/vent-store-review-outcomes.test.js +378 -0
- package/scripts/docs-list.sh +0 -55
- package/tests/vent-store.test.js +0 -2015
package/tests/vent-store.test.js
DELETED
|
@@ -1,2015 +0,0 @@
|
|
|
1
|
-
// ---
|
|
2
|
-
// summary: tests agent vent persistence, redaction, review projections, and retention safety
|
|
3
|
-
// read_when:
|
|
4
|
-
// - modifying vent-store records, filters, curation, exports, backups, or filesystem guards
|
|
5
|
-
// ---
|
|
6
|
-
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { createHash } from "node:crypto";
|
|
9
|
-
import fs from "node:fs";
|
|
10
|
-
import os from "node:os";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
import {
|
|
14
|
-
appendCurationEvent,
|
|
15
|
-
appendReviewEvent,
|
|
16
|
-
appendVentRecord,
|
|
17
|
-
archiveRecurrenceGroup,
|
|
18
|
-
assertCanCurateRecurrence,
|
|
19
|
-
buildEscalationDraft,
|
|
20
|
-
buildFacetSummary,
|
|
21
|
-
buildLifecycleSnapshot,
|
|
22
|
-
buildRecurrenceKey,
|
|
23
|
-
buildRetentionCandidates,
|
|
24
|
-
buildRetentionHistory,
|
|
25
|
-
buildRetentionPreview,
|
|
26
|
-
buildReviewComparison,
|
|
27
|
-
buildReviewDetail,
|
|
28
|
-
buildReviewOutcomes,
|
|
29
|
-
createCurationEvent,
|
|
30
|
-
createReviewEvent,
|
|
31
|
-
createVentRecord,
|
|
32
|
-
DRAFT_TARGETS,
|
|
33
|
-
defaultBackupDir,
|
|
34
|
-
defaultCurationPath,
|
|
35
|
-
defaultRetentionPath,
|
|
36
|
-
defaultReviewPath,
|
|
37
|
-
defaultStorePath,
|
|
38
|
-
formatExportJson,
|
|
39
|
-
formatExportMarkdown,
|
|
40
|
-
formatFacetSummary,
|
|
41
|
-
formatLifecycleStats,
|
|
42
|
-
formatRetentionCandidates,
|
|
43
|
-
formatRetentionHistory,
|
|
44
|
-
formatReviewComparison,
|
|
45
|
-
formatReviewDetail,
|
|
46
|
-
formatReviewOutcomes,
|
|
47
|
-
formatReviewQueue,
|
|
48
|
-
formatSummary,
|
|
49
|
-
loadDiagnosticState,
|
|
50
|
-
MAX_JSONL_FILE_BYTES,
|
|
51
|
-
MAX_JSONL_LINE_BYTES,
|
|
52
|
-
readCurationEvents,
|
|
53
|
-
readRetentionEvents,
|
|
54
|
-
readReviewEvents,
|
|
55
|
-
readVentRecords,
|
|
56
|
-
redactSensitiveText,
|
|
57
|
-
resolveRecurrenceGroup,
|
|
58
|
-
restoreRetentionBackup,
|
|
59
|
-
summarizeRecords,
|
|
60
|
-
summarizeReviewQueue,
|
|
61
|
-
} from "../src/vent-store.js";
|
|
62
|
-
|
|
63
|
-
test("default store and lifecycle paths honor PI_AGENT_VENT_DIR", () => {
|
|
64
|
-
assert.equal(
|
|
65
|
-
defaultStorePath({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
66
|
-
"/tmp/private-vents/vents.jsonl",
|
|
67
|
-
);
|
|
68
|
-
assert.equal(
|
|
69
|
-
defaultReviewPath({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
70
|
-
"/tmp/private-vents/review-events.jsonl",
|
|
71
|
-
);
|
|
72
|
-
assert.equal(
|
|
73
|
-
defaultCurationPath({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
74
|
-
"/tmp/private-vents/curation-events.jsonl",
|
|
75
|
-
);
|
|
76
|
-
assert.equal(
|
|
77
|
-
defaultRetentionPath({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
78
|
-
"/tmp/private-vents/retention-events.jsonl",
|
|
79
|
-
);
|
|
80
|
-
assert.equal(
|
|
81
|
-
defaultBackupDir({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
82
|
-
"/tmp/private-vents/backups",
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("createVentRecord minimizes, redacts, and derives recurrence key", () => {
|
|
87
|
-
const record = createVentRecord(
|
|
88
|
-
{
|
|
89
|
-
category: "tool-failure",
|
|
90
|
-
severity: "high",
|
|
91
|
-
summary: "Bash retry keeps failing with token=abc123",
|
|
92
|
-
frustration: "Same brittle flow; Authorization: Bearer abcdefghijklmnop should not leak.",
|
|
93
|
-
tags: ["Tool Failure", "runtime"],
|
|
94
|
-
tool: "bash token=abc123",
|
|
95
|
-
packageName: "@tryinget/pi-agent-vent secret=hidden",
|
|
96
|
-
},
|
|
97
|
-
{ id: "vent-1", now: "2026-05-21T00:00:00.000Z", cwd: "/repo", sessionFile: "session.jsonl" },
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
assert.equal(record.schemaVersion, 1);
|
|
101
|
-
assert.equal(record.category, "tool_failure");
|
|
102
|
-
assert.equal(record.severity, "high");
|
|
103
|
-
assert.equal(record.recurrenceKey, "tool_failure:bash-retry-keeps-failing-with-token-redacted");
|
|
104
|
-
assert.equal(record.tool, "bash-token-redacted");
|
|
105
|
-
assert.equal(record.packageName, "tryinget-pi-agent-vent-secret-redacted");
|
|
106
|
-
assert.equal(record.privacy.redacted, true);
|
|
107
|
-
assert.match(record.summary, /token=\[REDACTED\]/);
|
|
108
|
-
assert.match(record.frustration, /Bearer \[REDACTED\]/);
|
|
109
|
-
assert.deepEqual(record.tags, ["tool-failure", "runtime"]);
|
|
110
|
-
|
|
111
|
-
const tagOnlySecret = createVentRecord({
|
|
112
|
-
summary: "Tag-only secret must update privacy metadata",
|
|
113
|
-
tags: ["token=abc123"],
|
|
114
|
-
});
|
|
115
|
-
assert.deepEqual(tagOnlySecret.tags, ["token-redacted"]);
|
|
116
|
-
assert.equal(tagOnlySecret.privacy.redacted, true);
|
|
117
|
-
assert.deepEqual(tagOnlySecret.privacy.redactionPatterns, ["assigned_secret"]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("category aliases normalize at record and review-filter boundaries", () => {
|
|
121
|
-
const workflow = createVentRecord({
|
|
122
|
-
summary: "Workflow alias should not degrade",
|
|
123
|
-
category: "workflow_friction",
|
|
124
|
-
recurrenceKey: "workflow alias",
|
|
125
|
-
});
|
|
126
|
-
const missing = createVentRecord({
|
|
127
|
-
summary: "Missing affordance alias should preserve meaning",
|
|
128
|
-
category: "missing_affordance",
|
|
129
|
-
});
|
|
130
|
-
const alignment = createVentRecord({
|
|
131
|
-
summary: "Context alignment candidate should remain callable",
|
|
132
|
-
category: "context_alignment",
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
assert.equal(workflow.category, "workflow");
|
|
136
|
-
assert.equal(workflow.recurrenceKey, "workflow:workflow-alias");
|
|
137
|
-
assert.equal(missing.category, "missing_capability");
|
|
138
|
-
assert.match(missing.recurrenceKey, /^missing_capability:/);
|
|
139
|
-
assert.equal(alignment.category, "other");
|
|
140
|
-
assert.match(alignment.recurrenceKey, /^other:/);
|
|
141
|
-
|
|
142
|
-
const queue = summarizeReviewQueue([workflow, missing, alignment], [], {
|
|
143
|
-
filters: { category: "workflow_friction" },
|
|
144
|
-
});
|
|
145
|
-
assert.equal(queue.filters.category, "workflow");
|
|
146
|
-
assert.equal(queue.matchingGroupCount, 1);
|
|
147
|
-
assert.equal(queue.items[0].recurrenceKey, workflow.recurrenceKey);
|
|
148
|
-
|
|
149
|
-
const alignmentQueue = summarizeReviewQueue([workflow, missing, alignment], [], {
|
|
150
|
-
filters: { category: "context_alignment" },
|
|
151
|
-
});
|
|
152
|
-
assert.equal(alignmentQueue.filters.category, "other");
|
|
153
|
-
assert.equal(alignmentQueue.matchingGroupCount, 1);
|
|
154
|
-
assert.equal(alignmentQueue.items[0].recurrenceKey, alignment.recurrenceKey);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("explicit recurrence keys are redacted before slugging", () => {
|
|
158
|
-
const record = createVentRecord({
|
|
159
|
-
summary: "Explicit key should not leak",
|
|
160
|
-
category: "bug",
|
|
161
|
-
recurrenceKey: "reload token=abc123",
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
assert.equal(record.recurrenceKey, "bug:reload-token-redacted");
|
|
165
|
-
assert.doesNotMatch(record.recurrenceKey, /abc123/);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("redactSensitiveText catches common secret shapes", () => {
|
|
169
|
-
const result = redactSensitiveText(
|
|
170
|
-
"api_key=secret sk-abcdefghijklmnopqrstuvwxyz ghp_abcdefghijklmnopqrstuvwxyz",
|
|
171
|
-
);
|
|
172
|
-
assert.equal(result.redacted, true);
|
|
173
|
-
assert.match(result.text, /api_key=\[REDACTED\]/);
|
|
174
|
-
assert.match(result.text, /sk-\[REDACTED\]/);
|
|
175
|
-
assert.match(result.text, /gh\*_REDACTED/);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("append/read JSONL tolerates malformed old lines", () => {
|
|
179
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-"));
|
|
180
|
-
const filePath = path.join(dir, "vents.jsonl");
|
|
181
|
-
fs.writeFileSync(filePath, "{not-json}\n", "utf8");
|
|
182
|
-
const record = createVentRecord(
|
|
183
|
-
{ summary: "Repeated docs mismatch", category: "documentation" },
|
|
184
|
-
{ id: "v1" },
|
|
185
|
-
);
|
|
186
|
-
appendVentRecord(filePath, record);
|
|
187
|
-
|
|
188
|
-
const result = readVentRecords(filePath);
|
|
189
|
-
assert.equal(result.malformedLines, 1);
|
|
190
|
-
assert.equal(result.records.length, 1);
|
|
191
|
-
assert.equal(result.records[0].id, "v1");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("summary groups recurrence candidates without declaring incidents", () => {
|
|
195
|
-
const records = [
|
|
196
|
-
createVentRecord(
|
|
197
|
-
{
|
|
198
|
-
summary: "Reload drops registered tool",
|
|
199
|
-
category: "bug",
|
|
200
|
-
severity: "medium",
|
|
201
|
-
recurrenceKey: "reload-tools",
|
|
202
|
-
},
|
|
203
|
-
{ id: "a", now: "2026-05-21T00:00:00.000Z" },
|
|
204
|
-
),
|
|
205
|
-
createVentRecord(
|
|
206
|
-
{
|
|
207
|
-
summary: "Reload drops registered tool again",
|
|
208
|
-
category: "bug",
|
|
209
|
-
severity: "high",
|
|
210
|
-
recurrenceKey: "reload-tools",
|
|
211
|
-
},
|
|
212
|
-
{ id: "b", now: "2026-05-21T00:01:00.000Z" },
|
|
213
|
-
),
|
|
214
|
-
];
|
|
215
|
-
|
|
216
|
-
assert.equal(
|
|
217
|
-
buildRecurrenceKey({ category: "bug", recurrenceKey: "reload-tools" }),
|
|
218
|
-
"bug:reload-tools",
|
|
219
|
-
);
|
|
220
|
-
const summary = summarizeRecords(records);
|
|
221
|
-
assert.equal(summary.totalRecords, 2);
|
|
222
|
-
assert.equal(summary.candidateIncidentCount, 1);
|
|
223
|
-
assert.equal(summary.groups[0].candidateIncident, true);
|
|
224
|
-
assert.match(formatSummary(summary), /candidate incident/);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test("review events are append-only local state projections", () => {
|
|
228
|
-
const records = [
|
|
229
|
-
createVentRecord(
|
|
230
|
-
{
|
|
231
|
-
summary: "Reload drops registered tool",
|
|
232
|
-
category: "bug",
|
|
233
|
-
severity: "high",
|
|
234
|
-
recurrenceKey: "reload-tools",
|
|
235
|
-
},
|
|
236
|
-
{ id: "a", now: "2026-05-21T00:00:00.000Z" },
|
|
237
|
-
),
|
|
238
|
-
];
|
|
239
|
-
const acknowledged = createReviewEvent(
|
|
240
|
-
{
|
|
241
|
-
recurrenceKey: "bug:reload-tools",
|
|
242
|
-
state: "acknowledged",
|
|
243
|
-
note: "Operator saw token=abc123 and will inspect later",
|
|
244
|
-
},
|
|
245
|
-
{ id: "r1", now: "2026-05-21T00:02:00.000Z" },
|
|
246
|
-
);
|
|
247
|
-
const reset = createReviewEvent(
|
|
248
|
-
{ recurrenceKey: "bug:reload-tools", state: "new" },
|
|
249
|
-
{ id: "r2", now: "2026-05-21T00:03:00.000Z" },
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
assert.equal(acknowledged.privacy.redacted, true);
|
|
253
|
-
assert.match(acknowledged.note, /token=\[REDACTED\]/);
|
|
254
|
-
|
|
255
|
-
const queue = summarizeReviewQueue(records, [acknowledged, reset]);
|
|
256
|
-
assert.equal(queue.queueCount, 1);
|
|
257
|
-
assert.equal(queue.items[0].reviewState, "new");
|
|
258
|
-
assert.match(formatReviewQueue(queue), /local diagnostic state only/);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test("review events fail closed on invalid state and clamp huge notes", () => {
|
|
262
|
-
assert.throws(
|
|
263
|
-
() => createReviewEvent({ recurrenceKey: "bug:bad-state", state: "resolved" }),
|
|
264
|
-
/invalid agent_vent review state/,
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const event = createReviewEvent({
|
|
268
|
-
recurrenceKey: "bug:huge-note",
|
|
269
|
-
state: "acknowledged",
|
|
270
|
-
note: "x".repeat(5000),
|
|
271
|
-
});
|
|
272
|
-
assert.equal(event.note.length, 1200);
|
|
273
|
-
assert.match(event.note, /…$/);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test("dismissed recurrence groups leave the default active review queue", () => {
|
|
277
|
-
const records = [
|
|
278
|
-
createVentRecord(
|
|
279
|
-
{ summary: "Noisy one-off", category: "workflow", recurrenceKey: "noisy-one-off" },
|
|
280
|
-
{ id: "v1" },
|
|
281
|
-
),
|
|
282
|
-
];
|
|
283
|
-
const event = createReviewEvent({ recurrenceKey: "workflow:noisy-one-off", state: "dismissed" });
|
|
284
|
-
|
|
285
|
-
assert.equal(summarizeReviewQueue(records, [event]).queueCount, 0);
|
|
286
|
-
assert.equal(summarizeReviewQueue(records, [event], { state: "dismissed" }).queueCount, 1);
|
|
287
|
-
assert.equal(summarizeReviewQueue(records, [event], { state: "all" }).queueCount, 1);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test("review JSONL tolerates malformed lines and records latest state", () => {
|
|
291
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-review-"));
|
|
292
|
-
const filePath = path.join(dir, "review-events.jsonl");
|
|
293
|
-
fs.writeFileSync(filePath, "{not-json}\n", "utf8");
|
|
294
|
-
appendReviewEvent(
|
|
295
|
-
filePath,
|
|
296
|
-
createReviewEvent({ recurrenceKey: "bug:reload-tools", state: "acknowledged" }, { id: "r1" }),
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
const result = readReviewEvents(filePath);
|
|
300
|
-
assert.equal(result.malformedLines, 1);
|
|
301
|
-
assert.equal(result.events.length, 1);
|
|
302
|
-
assert.equal(result.events[0].state, "acknowledged");
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test("JSONL store files fail closed when replaced by symlinks", () => {
|
|
306
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-symlink-"));
|
|
307
|
-
const target = path.join(dir, "target.jsonl");
|
|
308
|
-
const link = path.join(dir, "vents.jsonl");
|
|
309
|
-
fs.writeFileSync(target, "", "utf8");
|
|
310
|
-
fs.symlinkSync(target, link);
|
|
311
|
-
|
|
312
|
-
assert.throws(
|
|
313
|
-
() => appendVentRecord(link, createVentRecord({ summary: "Symlink escape", category: "bug" })),
|
|
314
|
-
/must not be a symlink/,
|
|
315
|
-
);
|
|
316
|
-
assert.throws(() => readVentRecords(link), /must not be a symlink|ELOOP/);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("JSONL write permission failures propagate instead of pretending success", (context) => {
|
|
320
|
-
if (process.getuid?.() === 0) {
|
|
321
|
-
context.skip("root can bypass directory write permissions");
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-permission-"));
|
|
325
|
-
const filePath = path.join(dir, "vents.jsonl");
|
|
326
|
-
fs.chmodSync(dir, 0o500);
|
|
327
|
-
try {
|
|
328
|
-
assert.throws(
|
|
329
|
-
() => appendVentRecord(filePath, createVentRecord({ summary: "Permission failure" })),
|
|
330
|
-
/EACCES|EPERM/,
|
|
331
|
-
);
|
|
332
|
-
} finally {
|
|
333
|
-
fs.chmodSync(dir, 0o700);
|
|
334
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("stale vent-store lock files are removed before append", () => {
|
|
339
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-stale-lock-"));
|
|
340
|
-
const filePath = path.join(dir, "vents.jsonl");
|
|
341
|
-
const lockPath = `${filePath}.lock`;
|
|
342
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
343
|
-
fs.writeFileSync(lockPath, "stale\n", "utf8");
|
|
344
|
-
const old = new Date(Date.now() - 60_000);
|
|
345
|
-
fs.utimesSync(lockPath, old, old);
|
|
346
|
-
appendVentRecord(filePath, createVentRecord({ summary: "Stale lock clears" }, { id: "v1" }));
|
|
347
|
-
|
|
348
|
-
assert.equal(readVentRecords(filePath).records.length, 1);
|
|
349
|
-
assert.equal(fs.existsSync(lockPath), false);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
test("curation events merge recurrence groups without rewriting source records", () => {
|
|
353
|
-
const first = createVentRecord(
|
|
354
|
-
{ summary: "Reload drops tool", category: "bug", recurrenceKey: "reload-tool-a" },
|
|
355
|
-
{ id: "v1" },
|
|
356
|
-
);
|
|
357
|
-
const second = createVentRecord(
|
|
358
|
-
{ summary: "Reload loses tool", category: "bug", recurrenceKey: "reload-tool-b" },
|
|
359
|
-
{ id: "v2" },
|
|
360
|
-
);
|
|
361
|
-
const records = [first, second];
|
|
362
|
-
const curation = createCurationEvent({
|
|
363
|
-
action: "merge",
|
|
364
|
-
sourceRecurrenceKey: second.recurrenceKey,
|
|
365
|
-
targetRecurrenceKey: first.recurrenceKey,
|
|
366
|
-
note: "same bug token=abc123",
|
|
367
|
-
});
|
|
368
|
-
assert.equal(curation.privacy.redacted, true);
|
|
369
|
-
assert.match(curation.note, /token=\[REDACTED\]/);
|
|
370
|
-
|
|
371
|
-
const summary = summarizeRecords(records, { curationEvents: [curation] });
|
|
372
|
-
assert.equal(summary.groupCount, 1);
|
|
373
|
-
assert.equal(summary.groups[0].count, 2);
|
|
374
|
-
assert.equal(summary.groups[0].recurrenceKey, first.recurrenceKey);
|
|
375
|
-
assert.equal(second.recurrenceKey, "bug:reload-tool-b");
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
test("curation fails closed for unknown source, self merge, and cycles", () => {
|
|
379
|
-
const records = [
|
|
380
|
-
createVentRecord({ summary: "Alpha", category: "bug", recurrenceKey: "alpha" }, { id: "v1" }),
|
|
381
|
-
createVentRecord({ summary: "Beta", category: "bug", recurrenceKey: "beta" }, { id: "v2" }),
|
|
382
|
-
];
|
|
383
|
-
assert.throws(
|
|
384
|
-
() =>
|
|
385
|
-
assertCanCurateRecurrence(records, [], {
|
|
386
|
-
action: "merge",
|
|
387
|
-
sourceRecurrenceKey: "bug:nope",
|
|
388
|
-
targetRecurrenceKey: "bug:alpha",
|
|
389
|
-
}),
|
|
390
|
-
/unknown recurrence group/,
|
|
391
|
-
);
|
|
392
|
-
assert.throws(
|
|
393
|
-
() =>
|
|
394
|
-
createCurationEvent({
|
|
395
|
-
action: "merge",
|
|
396
|
-
sourceRecurrenceKey: "bug:alpha",
|
|
397
|
-
targetRecurrenceKey: "bug:alpha",
|
|
398
|
-
}),
|
|
399
|
-
/source and target must differ/,
|
|
400
|
-
);
|
|
401
|
-
const first = createCurationEvent({
|
|
402
|
-
action: "merge",
|
|
403
|
-
sourceRecurrenceKey: "bug:alpha",
|
|
404
|
-
targetRecurrenceKey: "bug:beta",
|
|
405
|
-
});
|
|
406
|
-
assert.throws(
|
|
407
|
-
() =>
|
|
408
|
-
assertCanCurateRecurrence(records, [first], {
|
|
409
|
-
action: "merge",
|
|
410
|
-
sourceRecurrenceKey: "bug:beta",
|
|
411
|
-
targetRecurrenceKey: "bug:alpha",
|
|
412
|
-
}),
|
|
413
|
-
/cycle detected/,
|
|
414
|
-
);
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
test("review state follows curated recurrence key", () => {
|
|
418
|
-
const first = createVentRecord(
|
|
419
|
-
{ summary: "Primary", category: "workflow", recurrenceKey: "primary" },
|
|
420
|
-
{ id: "v1" },
|
|
421
|
-
);
|
|
422
|
-
const second = createVentRecord(
|
|
423
|
-
{ summary: "Duplicate", category: "workflow", recurrenceKey: "duplicate" },
|
|
424
|
-
{ id: "v2" },
|
|
425
|
-
);
|
|
426
|
-
const curation = createCurationEvent({
|
|
427
|
-
action: "merge",
|
|
428
|
-
sourceRecurrenceKey: second.recurrenceKey,
|
|
429
|
-
targetRecurrenceKey: first.recurrenceKey,
|
|
430
|
-
});
|
|
431
|
-
const review = createReviewEvent({ recurrenceKey: second.recurrenceKey, state: "acknowledged" });
|
|
432
|
-
|
|
433
|
-
const resolved = resolveRecurrenceGroup([first, second], second.recurrenceKey, [curation]);
|
|
434
|
-
assert.equal(resolved.recurrenceKey, first.recurrenceKey);
|
|
435
|
-
assert.equal(resolved.resolvedThroughCuration, true);
|
|
436
|
-
const queue = summarizeReviewQueue([first, second], [review], { curationEvents: [curation] });
|
|
437
|
-
assert.equal(queue.groupCount, 1);
|
|
438
|
-
assert.equal(queue.items[0].reviewState, "acknowledged");
|
|
439
|
-
assert.equal(queue.items[0].recurrenceKey, first.recurrenceKey);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
test("facet summary counts local categories tags tools packages and review states", () => {
|
|
443
|
-
const first = createVentRecord(
|
|
444
|
-
{
|
|
445
|
-
summary: "Facet primary",
|
|
446
|
-
category: "tool_failure",
|
|
447
|
-
severity: "high",
|
|
448
|
-
recurrenceKey: "facet-primary",
|
|
449
|
-
tags: ["Reload", "Runtime"],
|
|
450
|
-
tool: "pi reload",
|
|
451
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
452
|
-
},
|
|
453
|
-
{ id: "v1" },
|
|
454
|
-
);
|
|
455
|
-
const second = createVentRecord(
|
|
456
|
-
{
|
|
457
|
-
summary: "Facet duplicate token=abc123",
|
|
458
|
-
category: "workflow",
|
|
459
|
-
recurrenceKey: "facet-dupe",
|
|
460
|
-
tags: ["Reload"],
|
|
461
|
-
tool: "pi reload",
|
|
462
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
463
|
-
},
|
|
464
|
-
{ id: "v2" },
|
|
465
|
-
);
|
|
466
|
-
const curation = createCurationEvent({
|
|
467
|
-
action: "merge",
|
|
468
|
-
sourceRecurrenceKey: second.recurrenceKey,
|
|
469
|
-
targetRecurrenceKey: first.recurrenceKey,
|
|
470
|
-
});
|
|
471
|
-
const review = createReviewEvent({ recurrenceKey: first.recurrenceKey, state: "acknowledged" });
|
|
472
|
-
|
|
473
|
-
const summary = buildFacetSummary({
|
|
474
|
-
records: [first, second],
|
|
475
|
-
reviewEvents: [review],
|
|
476
|
-
curationEvents: [curation],
|
|
477
|
-
limit: 1,
|
|
478
|
-
});
|
|
479
|
-
assert.equal(summary.totalRecords, 2);
|
|
480
|
-
assert.equal(summary.groupCount, 1);
|
|
481
|
-
assert.deepEqual(summary.records.tools, [{ name: "pi-reload", count: 2 }]);
|
|
482
|
-
assert.deepEqual(summary.records.packages, [{ name: "tryinget-pi-agent-vent", count: 2 }]);
|
|
483
|
-
assert.deepEqual(summary.groups.reviewStates, [{ name: "acknowledged", count: 1 }]);
|
|
484
|
-
const text = formatFacetSummary(summary);
|
|
485
|
-
assert.match(text, /not owner routing/);
|
|
486
|
-
assert.match(text, /tools: pi-reload=2/);
|
|
487
|
-
assert.doesNotMatch(text, /abc123/);
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
test("review queue filters by local facets without owner-routing claims", () => {
|
|
491
|
-
const reload = createVentRecord(
|
|
492
|
-
{
|
|
493
|
-
summary: "Reload tool fails",
|
|
494
|
-
category: "tool_failure",
|
|
495
|
-
recurrenceKey: "reload-tool",
|
|
496
|
-
tags: ["Review Flow", "Reload"],
|
|
497
|
-
tool: "pi reload",
|
|
498
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
499
|
-
},
|
|
500
|
-
{ id: "v1" },
|
|
501
|
-
);
|
|
502
|
-
const docs = createVentRecord(
|
|
503
|
-
{
|
|
504
|
-
summary: "Docs stale",
|
|
505
|
-
category: "documentation",
|
|
506
|
-
recurrenceKey: "docs-stale",
|
|
507
|
-
tags: ["Docs"],
|
|
508
|
-
tool: "docs-list",
|
|
509
|
-
packageName: "@tryinget/other-package",
|
|
510
|
-
},
|
|
511
|
-
{ id: "v2" },
|
|
512
|
-
);
|
|
513
|
-
|
|
514
|
-
assert.throws(
|
|
515
|
-
() => summarizeReviewQueue([reload, docs], [], { filters: { category: "bgu" } }),
|
|
516
|
-
/invalid agent_vent review filter category/,
|
|
517
|
-
);
|
|
518
|
-
|
|
519
|
-
const queue = summarizeReviewQueue([reload, docs], [], {
|
|
520
|
-
filters: {
|
|
521
|
-
category: "tool-failure",
|
|
522
|
-
tags: ["review flow"],
|
|
523
|
-
tool: "pi reload token=abc123",
|
|
524
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
525
|
-
},
|
|
526
|
-
});
|
|
527
|
-
assert.equal(queue.groupCount, 2);
|
|
528
|
-
assert.equal(queue.matchingGroupCount, 0);
|
|
529
|
-
assert.equal(queue.queueCount, 0);
|
|
530
|
-
assert.equal(queue.filters.tool, "pi-reload-token-redacted");
|
|
531
|
-
const noMatchText = formatReviewQueue(queue);
|
|
532
|
-
assert.match(noMatchText, /not owner routing/);
|
|
533
|
-
assert.match(noMatchText, /token-redacted/);
|
|
534
|
-
assert.doesNotMatch(noMatchText, /abc123/);
|
|
535
|
-
|
|
536
|
-
const matchingQueue = summarizeReviewQueue([reload, docs], [], {
|
|
537
|
-
filters: {
|
|
538
|
-
category: "tool-failure",
|
|
539
|
-
tags: ["review flow", "reload"],
|
|
540
|
-
tool: "pi reload",
|
|
541
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
542
|
-
},
|
|
543
|
-
});
|
|
544
|
-
assert.equal(matchingQueue.matchingGroupCount, 1);
|
|
545
|
-
assert.equal(matchingQueue.queueCount, 1);
|
|
546
|
-
assert.equal(matchingQueue.items[0].recurrenceKey, reload.recurrenceKey);
|
|
547
|
-
assert.equal(matchingQueue.filters.tool, "pi-reload");
|
|
548
|
-
assert.deepEqual(matchingQueue.filters.tags, ["review-flow", "reload"]);
|
|
549
|
-
const matchingText = formatReviewQueue(matchingQueue);
|
|
550
|
-
assert.match(
|
|
551
|
-
matchingText,
|
|
552
|
-
/Filters: category=tool_failure; tool=pi-reload; package=tryinget-pi-agent-vent; tags=review-flow,reload/,
|
|
553
|
-
);
|
|
554
|
-
assert.match(matchingText, /Filter note:/);
|
|
555
|
-
assert.doesNotMatch(matchingText, /owner assignment was created/);
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
test("review queue facet filters are bounded for huge caller-controlled input", () => {
|
|
559
|
-
const record = createVentRecord({
|
|
560
|
-
summary: "Huge filter should not leak",
|
|
561
|
-
category: "workflow",
|
|
562
|
-
recurrenceKey: "huge-filter",
|
|
563
|
-
tags: ["safe"],
|
|
564
|
-
tool: "small-tool",
|
|
565
|
-
});
|
|
566
|
-
const queue = summarizeReviewQueue([record], [], {
|
|
567
|
-
filters: {
|
|
568
|
-
tool: `${"x".repeat(5000)} token=abc123`,
|
|
569
|
-
tags: Array.from({ length: 20 }, (_, index) => `tag-${index}`),
|
|
570
|
-
},
|
|
571
|
-
});
|
|
572
|
-
assert.equal(queue.queueCount, 0);
|
|
573
|
-
assert.ok(queue.filters.tool.length <= 80);
|
|
574
|
-
assert.equal(queue.filters.tags.length, 12);
|
|
575
|
-
const text = formatReviewQueue(queue);
|
|
576
|
-
assert.doesNotMatch(text, /abc123/);
|
|
577
|
-
assert.match(text, /not owner routing/);
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
test("review detail expands curated groups with bounded redacted samples", () => {
|
|
581
|
-
const first = createVentRecord(
|
|
582
|
-
{
|
|
583
|
-
summary: "Primary reload failure",
|
|
584
|
-
category: "tool_failure",
|
|
585
|
-
severity: "high",
|
|
586
|
-
recurrenceKey: "reload-primary",
|
|
587
|
-
evidence: "Bearer abcdefghijklmnop should redact",
|
|
588
|
-
tags: ["Pi Tool", "Reload"],
|
|
589
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
590
|
-
},
|
|
591
|
-
{ id: "v1", now: "2026-05-21T00:00:00.000Z" },
|
|
592
|
-
);
|
|
593
|
-
const second = createVentRecord(
|
|
594
|
-
{
|
|
595
|
-
summary: "Duplicate reload wording",
|
|
596
|
-
category: "workflow",
|
|
597
|
-
recurrenceKey: "reload-dupe",
|
|
598
|
-
reproduction: "Run reload with token=abc123 in shell",
|
|
599
|
-
tags: ["Reload"],
|
|
600
|
-
},
|
|
601
|
-
{ id: "v2", now: "2026-05-21T00:01:00.000Z" },
|
|
602
|
-
);
|
|
603
|
-
const curation = createCurationEvent({
|
|
604
|
-
action: "merge",
|
|
605
|
-
sourceRecurrenceKey: second.recurrenceKey,
|
|
606
|
-
targetRecurrenceKey: first.recurrenceKey,
|
|
607
|
-
});
|
|
608
|
-
const review = createReviewEvent({ recurrenceKey: second.recurrenceKey, state: "acknowledged" });
|
|
609
|
-
|
|
610
|
-
const detail = buildReviewDetail({
|
|
611
|
-
recurrenceKey: second.recurrenceKey,
|
|
612
|
-
records: [first, second],
|
|
613
|
-
reviewEvents: [review],
|
|
614
|
-
curationEvents: [curation],
|
|
615
|
-
limit: 999,
|
|
616
|
-
});
|
|
617
|
-
assert.equal(detail.recurrenceKey, first.recurrenceKey);
|
|
618
|
-
assert.equal(detail.group.count, 2);
|
|
619
|
-
assert.equal(detail.group.reviewState, "acknowledged");
|
|
620
|
-
assert.equal(detail.group.decisionPosture.state, "reviewed_retention_ready");
|
|
621
|
-
assert.deepEqual(detail.group.categories, ["tool_failure", "workflow"]);
|
|
622
|
-
assert.deepEqual(detail.group.tags, ["pi-tool", "reload"]);
|
|
623
|
-
assert.equal(detail.samples.length, 2);
|
|
624
|
-
const text = formatReviewDetail(detail);
|
|
625
|
-
assert.match(text, /Requested key resolved through local curation/);
|
|
626
|
-
assert.match(text, /Decision posture: reviewed_retention_ready/);
|
|
627
|
-
assert.match(
|
|
628
|
-
text,
|
|
629
|
-
/not resolution, assignment, evidence, publication, issue status, task truth, or incident state/,
|
|
630
|
-
);
|
|
631
|
-
assert.match(text, /Human review hints:/);
|
|
632
|
-
assert.match(text, /incident_review draft may help a human decide/);
|
|
633
|
-
assert.match(text, /maintainer_note draft may help package\/tool maintainers/);
|
|
634
|
-
assert.match(text, /github_issue draft may help/);
|
|
635
|
-
assert.match(text, /ak_task draft may help/);
|
|
636
|
-
assert.match(text, /not owner routing, assignment, filing, task creation, incident declaration/);
|
|
637
|
-
assert.match(text, /Local next actions:/);
|
|
638
|
-
assert.match(text, /draft github_issue/);
|
|
639
|
-
assert.match(text, /draft ak_task/);
|
|
640
|
-
assert.match(text, /draft incident_review/);
|
|
641
|
-
assert.match(text, /draft maintainer_note/);
|
|
642
|
-
assert.match(text, /retention preview/);
|
|
643
|
-
assert.match(
|
|
644
|
-
text,
|
|
645
|
-
/do not file, create, declare, assign, record evidence, publish, or mutate owner systems/,
|
|
646
|
-
);
|
|
647
|
-
assert.match(text, /Bearer \[REDACTED\]/);
|
|
648
|
-
assert.match(text, /token=\[REDACTED\]/);
|
|
649
|
-
assert.match(text, /No AK task, GitHub issue, incident, evidence/);
|
|
650
|
-
assert.doesNotMatch(text, /filed|declared|owner was assigned/);
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
test("review output quotes generated commands for legacy recurrence keys", () => {
|
|
654
|
-
const legacy = {
|
|
655
|
-
...createVentRecord({ summary: "Legacy key with spaces", category: "bug" }),
|
|
656
|
-
recurrenceKey: 'bug:legacy key "quoted"',
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
const queueText = formatReviewQueue(summarizeReviewQueue([legacy], []));
|
|
660
|
-
assert.match(queueText, /review show "bug:legacy key \\"quoted\\""/);
|
|
661
|
-
assert.match(queueText, /review set acknowledged "bug:legacy key \\"quoted\\""/);
|
|
662
|
-
assert.match(queueText, /draft github_issue "bug:legacy key \\"quoted\\""/);
|
|
663
|
-
|
|
664
|
-
const detailText = formatReviewDetail(
|
|
665
|
-
buildReviewDetail({ recurrenceKey: legacy.recurrenceKey, records: [legacy] }),
|
|
666
|
-
);
|
|
667
|
-
assert.match(detailText, /review set dismissed "bug:legacy key \\"quoted\\""/);
|
|
668
|
-
assert.match(detailText, /draft maintainer_note "bug:legacy key \\"quoted\\""/);
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
test("review queue guidance hints remain advisory and authority-safe", () => {
|
|
672
|
-
const incident = createVentRecord({
|
|
673
|
-
summary: "Critical package bug",
|
|
674
|
-
category: "bug",
|
|
675
|
-
severity: "critical",
|
|
676
|
-
recurrenceKey: "critical-package-bug",
|
|
677
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
678
|
-
});
|
|
679
|
-
const workflow = createVentRecord({
|
|
680
|
-
summary: "Workflow docs unclear",
|
|
681
|
-
category: "documentation",
|
|
682
|
-
recurrenceKey: "workflow-docs",
|
|
683
|
-
});
|
|
684
|
-
const toolOnly = createVentRecord({
|
|
685
|
-
summary: "Tool-only reload issue",
|
|
686
|
-
category: "workflow",
|
|
687
|
-
recurrenceKey: "tool-only",
|
|
688
|
-
tool: "pi reload",
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
const queue = summarizeReviewQueue([incident, workflow, toolOnly], []);
|
|
692
|
-
assert.equal(queue.items[0].decisionPosture.priority, "human_review_candidate");
|
|
693
|
-
assert.equal(queue.items[0].decisionPosture.state, "needs_local_review");
|
|
694
|
-
const text = formatReviewQueue(queue);
|
|
695
|
-
assert.match(text, /decision posture: needs_local_review; priority=human_review_candidate/);
|
|
696
|
-
assert.match(text, /No local review decision has been recorded/);
|
|
697
|
-
assert.match(text, /incident_review draft may help a human decide/);
|
|
698
|
-
assert.match(text, /maintainer_note draft may help package\/tool maintainers/);
|
|
699
|
-
assert.match(text, /github_issue draft may help/);
|
|
700
|
-
assert.match(text, /ak_task draft may help/);
|
|
701
|
-
assert.match(text, /Hints are local diagnostics only/);
|
|
702
|
-
assert.doesNotMatch(
|
|
703
|
-
text,
|
|
704
|
-
/GitHub issue was created|AK task was created|incident was declared|owner was assigned|was filed/,
|
|
705
|
-
);
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
test("review queue next actions are state-aware and authority-safe", () => {
|
|
709
|
-
const record = createVentRecord({
|
|
710
|
-
summary: "State-aware next action",
|
|
711
|
-
category: "workflow",
|
|
712
|
-
recurrenceKey: "state-aware",
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
const newQueueText = formatReviewQueue(summarizeReviewQueue([record], []));
|
|
716
|
-
assert.match(newQueueText, /review first before local retention archive/);
|
|
717
|
-
assert.match(newQueueText, /draft ak_task/);
|
|
718
|
-
assert.match(
|
|
719
|
-
newQueueText,
|
|
720
|
-
/do not file, create, declare, assign, record evidence, publish, or mutate owner systems/,
|
|
721
|
-
);
|
|
722
|
-
assert.doesNotMatch(newQueueText, /optional local lifecycle: \/agent_vent retention preview/);
|
|
723
|
-
|
|
724
|
-
const reviewedQueueText = formatReviewQueue(
|
|
725
|
-
summarizeReviewQueue(
|
|
726
|
-
[record],
|
|
727
|
-
[createReviewEvent({ recurrenceKey: record.recurrenceKey, state: "acknowledged" })],
|
|
728
|
-
),
|
|
729
|
-
);
|
|
730
|
-
assert.match(reviewedQueueText, /decision posture: reviewed_retention_ready/);
|
|
731
|
-
assert.match(reviewedQueueText, /optional local lifecycle: \/agent_vent retention preview/);
|
|
732
|
-
assert.match(reviewedQueueText, /draft maintainer_note/);
|
|
733
|
-
assert.doesNotMatch(
|
|
734
|
-
reviewedQueueText,
|
|
735
|
-
/GitHub issue was created|AK task was created|incident was declared|was filed/,
|
|
736
|
-
);
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
test("review outcomes bucket post-review follow-up without authority drift", () => {
|
|
740
|
-
const fresh = createVentRecord({
|
|
741
|
-
summary: "Fresh outcome needs review",
|
|
742
|
-
category: "workflow",
|
|
743
|
-
recurrenceKey: "fresh outcome",
|
|
744
|
-
tags: ["outcome"],
|
|
745
|
-
});
|
|
746
|
-
const acknowledged = createVentRecord({
|
|
747
|
-
summary: "Acknowledged package issue",
|
|
748
|
-
category: "tool_failure",
|
|
749
|
-
recurrenceKey: "acknowledged outcome",
|
|
750
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
751
|
-
tags: ["outcome"],
|
|
752
|
-
});
|
|
753
|
-
const dismissed = createVentRecord({
|
|
754
|
-
summary: "Dismissed noisy group",
|
|
755
|
-
category: "documentation",
|
|
756
|
-
recurrenceKey: "dismissed outcome",
|
|
757
|
-
tags: ["outcome"],
|
|
758
|
-
});
|
|
759
|
-
const drafted = createVentRecord({
|
|
760
|
-
summary: "Escalation draft prepared",
|
|
761
|
-
category: "bug",
|
|
762
|
-
recurrenceKey: "drafted outcome",
|
|
763
|
-
severity: "high",
|
|
764
|
-
tags: ["outcome"],
|
|
765
|
-
});
|
|
766
|
-
const reviews = [
|
|
767
|
-
createReviewEvent({
|
|
768
|
-
recurrenceKey: acknowledged.recurrenceKey,
|
|
769
|
-
state: "acknowledged",
|
|
770
|
-
note: "seen token=abc123 locally",
|
|
771
|
-
}),
|
|
772
|
-
createReviewEvent({ recurrenceKey: dismissed.recurrenceKey, state: "dismissed" }),
|
|
773
|
-
createReviewEvent({ recurrenceKey: drafted.recurrenceKey, state: "escalation_drafted" }),
|
|
774
|
-
];
|
|
775
|
-
|
|
776
|
-
const outcomes = buildReviewOutcomes({
|
|
777
|
-
records: [fresh, acknowledged, dismissed, drafted],
|
|
778
|
-
reviewEvents: reviews,
|
|
779
|
-
filters: { tags: ["outcome"] },
|
|
780
|
-
limit: 10,
|
|
781
|
-
});
|
|
782
|
-
assert.equal(outcomes.counts.new, 1);
|
|
783
|
-
assert.equal(outcomes.counts.acknowledged, 1);
|
|
784
|
-
assert.equal(outcomes.counts.dismissed, 1);
|
|
785
|
-
assert.equal(outcomes.counts.escalation_drafted, 1);
|
|
786
|
-
assert.equal(outcomes.limitPerBucket, 10);
|
|
787
|
-
assert.deepEqual(outcomes.filters.tags, ["outcome"]);
|
|
788
|
-
const boundedFilter = buildReviewOutcomes({
|
|
789
|
-
records: [fresh],
|
|
790
|
-
reviewEvents: [],
|
|
791
|
-
filters: { tags: ["x".repeat(1000)] },
|
|
792
|
-
});
|
|
793
|
-
assert.deepEqual(boundedFilter.filters.tags, ["x".repeat(80)]);
|
|
794
|
-
assert.equal(boundedFilter.matchingGroupCount, 0);
|
|
795
|
-
|
|
796
|
-
const text = formatReviewOutcomes(outcomes);
|
|
797
|
-
assert.match(text, /Agent vent review outcomes/);
|
|
798
|
-
assert.match(text, /showing up to 10 group\(s\) per state bucket/);
|
|
799
|
-
assert.match(text, /new: 1 group/);
|
|
800
|
-
assert.match(text, /acknowledged: 1 group/);
|
|
801
|
-
assert.match(text, /dismissed: 1 group/);
|
|
802
|
-
assert.match(text, /escalation_drafted: 1 group/);
|
|
803
|
-
assert.match(text, /decision posture: needs_local_review/);
|
|
804
|
-
assert.match(text, /decision posture: dismissed_retention_ready/);
|
|
805
|
-
assert.match(text, /decision posture: draft_recorded_owner_external/);
|
|
806
|
-
assert.match(text, /seen token=\[REDACTED\] locally/);
|
|
807
|
-
assert.match(text, /retention waits for local review/);
|
|
808
|
-
assert.match(text, /optional local lifecycle: \/agent_vent retention preview/);
|
|
809
|
-
assert.match(
|
|
810
|
-
text,
|
|
811
|
-
/export this outcome bucket: \/agent_vent export markdown acknowledged tag=outcome/,
|
|
812
|
-
);
|
|
813
|
-
assert.match(text, /Local diagnostic labels only; not owner routing/);
|
|
814
|
-
assert.match(text, /No AK task, GitHub issue, incident, evidence, telemetry/);
|
|
815
|
-
assert.doesNotMatch(
|
|
816
|
-
text,
|
|
817
|
-
/GitHub issue was created|AK task was created|incident was declared|owner was assigned|resolved externally|was filed/,
|
|
818
|
-
);
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
test("review comparison contrasts state buckets without authority drift", () => {
|
|
822
|
-
const fresh = createVentRecord({
|
|
823
|
-
summary: "Fresh compare needs review token=abc123",
|
|
824
|
-
category: "workflow",
|
|
825
|
-
recurrenceKey: "fresh compare",
|
|
826
|
-
tags: ["Compare"],
|
|
827
|
-
});
|
|
828
|
-
const acknowledged = createVentRecord({
|
|
829
|
-
summary: "Acknowledged compare issue",
|
|
830
|
-
category: "tool_failure",
|
|
831
|
-
severity: "high",
|
|
832
|
-
recurrenceKey: "ack compare",
|
|
833
|
-
tags: ["Compare"],
|
|
834
|
-
tool: "pi reload",
|
|
835
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
836
|
-
});
|
|
837
|
-
const dismissed = createVentRecord({
|
|
838
|
-
summary: "Dismissed compare issue",
|
|
839
|
-
category: "documentation",
|
|
840
|
-
recurrenceKey: "dismiss compare",
|
|
841
|
-
tags: ["Compare"],
|
|
842
|
-
});
|
|
843
|
-
const drafted = createVentRecord({
|
|
844
|
-
summary: "Drafted compare critical issue",
|
|
845
|
-
category: "bug",
|
|
846
|
-
severity: "critical",
|
|
847
|
-
recurrenceKey: "draft compare",
|
|
848
|
-
tags: ["Compare"],
|
|
849
|
-
});
|
|
850
|
-
const reviews = [
|
|
851
|
-
createReviewEvent({ recurrenceKey: acknowledged.recurrenceKey, state: "acknowledged" }),
|
|
852
|
-
createReviewEvent({ recurrenceKey: dismissed.recurrenceKey, state: "dismissed" }),
|
|
853
|
-
createReviewEvent({ recurrenceKey: drafted.recurrenceKey, state: "escalation_drafted" }),
|
|
854
|
-
];
|
|
855
|
-
|
|
856
|
-
const comparison = buildReviewComparison({
|
|
857
|
-
records: [fresh, acknowledged, dismissed, drafted],
|
|
858
|
-
reviewEvents: reviews,
|
|
859
|
-
filters: { tags: ["compare"], tool: `${"x".repeat(5000)} token=abc123` },
|
|
860
|
-
limit: 1000,
|
|
861
|
-
});
|
|
862
|
-
assert.equal(comparison.limitPerState, 100);
|
|
863
|
-
assert.equal(comparison.matchingGroupCount, 0);
|
|
864
|
-
assert.ok(comparison.filters.tool.length <= 80);
|
|
865
|
-
|
|
866
|
-
const unfiltered = buildReviewComparison({
|
|
867
|
-
records: [fresh, acknowledged, dismissed, drafted],
|
|
868
|
-
reviewEvents: reviews,
|
|
869
|
-
filters: { tags: ["compare"] },
|
|
870
|
-
limit: 2,
|
|
871
|
-
});
|
|
872
|
-
assert.equal(unfiltered.totals.new.groups, 1);
|
|
873
|
-
assert.equal(unfiltered.totals.acknowledged.candidateIncidents, 0);
|
|
874
|
-
assert.equal(unfiltered.totals.escalation_drafted.candidateIncidents, 1);
|
|
875
|
-
assert.equal(unfiltered.totals.escalation_drafted.criticalGroups, 1);
|
|
876
|
-
assert.deepEqual(unfiltered.filters.tags, ["compare"]);
|
|
877
|
-
const text = formatReviewComparison(unfiltered);
|
|
878
|
-
assert.match(text, /Agent vent review comparison/);
|
|
879
|
-
assert.match(text, /State totals:/);
|
|
880
|
-
assert.match(text, /export bucket: \/agent_vent export markdown acknowledged tag=compare/);
|
|
881
|
-
assert.match(text, /outcomes: \/agent_vent outcomes acknowledged tag=compare/);
|
|
882
|
-
assert.match(
|
|
883
|
-
text,
|
|
884
|
-
/retention planning: \/agent_vent retention candidates acknowledged tag=compare/,
|
|
885
|
-
);
|
|
886
|
-
assert.match(text, /choose local outcome: \/agent_vent review set acknowledged/);
|
|
887
|
-
assert.match(text, /intentionally emits no archive or restore confirmation tokens/);
|
|
888
|
-
assert.match(text, /No AK task, GitHub issue, incident, evidence, telemetry/);
|
|
889
|
-
assert.match(text, /token=\[REDACTED\]/);
|
|
890
|
-
assert.doesNotMatch(
|
|
891
|
-
text,
|
|
892
|
-
/archive:[a-f0-9]|restore:[a-f0-9]|GitHub issue was created|AK task was created|incident was declared|owner was assigned|resolved externally|was filed/,
|
|
893
|
-
);
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
test("review outcomes limit is explicit per state bucket", () => {
|
|
897
|
-
const states = ["new", "acknowledged", "dismissed", "escalation_drafted"];
|
|
898
|
-
const records = [];
|
|
899
|
-
const reviews = [];
|
|
900
|
-
for (const state of states) {
|
|
901
|
-
const record = createVentRecord({
|
|
902
|
-
summary: `${state} grouped`,
|
|
903
|
-
recurrenceKey: `${state}-group`,
|
|
904
|
-
});
|
|
905
|
-
records.push(record);
|
|
906
|
-
if (state !== "new") {
|
|
907
|
-
reviews.push(createReviewEvent({ recurrenceKey: record.recurrenceKey, state }));
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
const outcomes = buildReviewOutcomes({ records, reviewEvents: reviews, state: "all", limit: 1 });
|
|
912
|
-
assert.equal(outcomes.limitPerBucket, 1);
|
|
913
|
-
assert.deepEqual(
|
|
914
|
-
outcomes.buckets.map((bucket) => `${bucket.state}:${bucket.items.length}`),
|
|
915
|
-
["new:1", "acknowledged:1", "dismissed:1", "escalation_drafted:1"],
|
|
916
|
-
);
|
|
917
|
-
assert.match(formatReviewOutcomes(outcomes), /showing up to 1 group\(s\) per state bucket/);
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
test("review outcomes fail closed for invalid states and category filters", () => {
|
|
921
|
-
assert.throws(
|
|
922
|
-
() => buildReviewOutcomes({ state: "resolved", records: [], reviewEvents: [] }),
|
|
923
|
-
/invalid agent_vent review state/,
|
|
924
|
-
);
|
|
925
|
-
assert.throws(
|
|
926
|
-
() => buildReviewOutcomes({ filters: { category: "../../etc/passwd" } }),
|
|
927
|
-
/invalid agent_vent review filter category/,
|
|
928
|
-
);
|
|
929
|
-
const emptyText = formatReviewOutcomes(buildReviewOutcomes({ records: [], reviewEvents: [] }));
|
|
930
|
-
assert.match(emptyText, /No agent vent records found/);
|
|
931
|
-
assert.match(emptyText, /Boundary:/);
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
test("review detail fails closed for unknown groups and redacts legacy JSONL", () => {
|
|
935
|
-
assert.throws(
|
|
936
|
-
() => buildReviewDetail({ recurrenceKey: "bug:missing", records: [] }),
|
|
937
|
-
/cannot inspect unknown recurrence group/,
|
|
938
|
-
);
|
|
939
|
-
|
|
940
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-review-detail-"));
|
|
941
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
942
|
-
fs.writeFileSync(
|
|
943
|
-
storePath,
|
|
944
|
-
`${JSON.stringify({
|
|
945
|
-
schemaVersion: 1,
|
|
946
|
-
id: "legacy",
|
|
947
|
-
createdAt: "2026-05-21T00:00:00.000Z",
|
|
948
|
-
category: "bug",
|
|
949
|
-
severity: "high",
|
|
950
|
-
recurrenceKey: "bug:legacy-secret",
|
|
951
|
-
summary: "Legacy token=abc123 should redact",
|
|
952
|
-
evidence: "Authorization Bearer abcdefghijklmnop",
|
|
953
|
-
})}\n`,
|
|
954
|
-
"utf8",
|
|
955
|
-
);
|
|
956
|
-
|
|
957
|
-
const records = readVentRecords(storePath).records;
|
|
958
|
-
const text = formatReviewDetail(
|
|
959
|
-
buildReviewDetail({ recurrenceKey: "bug:legacy-secret", records, limit: 1000 }),
|
|
960
|
-
);
|
|
961
|
-
assert.doesNotMatch(text, /abc123|abcdefghijklmnop/);
|
|
962
|
-
assert.match(text, /token=\[REDACTED\]/);
|
|
963
|
-
assert.match(text, /Bearer \[REDACTED\]/);
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
test("curation JSONL tolerates malformed lines and symlink stores fail closed", () => {
|
|
967
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-curation-"));
|
|
968
|
-
const filePath = path.join(dir, "curation-events.jsonl");
|
|
969
|
-
fs.writeFileSync(filePath, "{not-json}\n", "utf8");
|
|
970
|
-
appendCurationEvent(
|
|
971
|
-
filePath,
|
|
972
|
-
createCurationEvent({
|
|
973
|
-
action: "rename",
|
|
974
|
-
sourceRecurrenceKey: "bug:old",
|
|
975
|
-
targetRecurrenceKey: "bug:new",
|
|
976
|
-
}),
|
|
977
|
-
);
|
|
978
|
-
const result = readCurationEvents(filePath);
|
|
979
|
-
assert.equal(result.malformedLines, 1);
|
|
980
|
-
assert.equal(result.events.length, 1);
|
|
981
|
-
|
|
982
|
-
const target = path.join(dir, "target.jsonl");
|
|
983
|
-
const link = path.join(dir, "curation-link.jsonl");
|
|
984
|
-
fs.writeFileSync(target, "", "utf8");
|
|
985
|
-
fs.symlinkSync(target, link);
|
|
986
|
-
assert.throws(() => appendCurationEvent(link, result.events[0]), /must not be a symlink/);
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
test("draft-only escalation text is bounded and authority-safe", () => {
|
|
990
|
-
const records = Array.from({ length: 8 }, (_, index) =>
|
|
991
|
-
createVentRecord(
|
|
992
|
-
{
|
|
993
|
-
summary: `Draft sample ${index}`,
|
|
994
|
-
evidence: `Pointer ${index}`,
|
|
995
|
-
category: "bug",
|
|
996
|
-
severity: index === 7 ? "high" : "medium",
|
|
997
|
-
recurrenceKey: "draft-target",
|
|
998
|
-
tool: index % 2 === 0 ? "pi reload token=abc123" : undefined,
|
|
999
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
1000
|
-
tags: ["Draft Flow"],
|
|
1001
|
-
},
|
|
1002
|
-
{ id: `v${index}`, now: `2026-05-21T00:0${index}:00.000Z` },
|
|
1003
|
-
),
|
|
1004
|
-
);
|
|
1005
|
-
const review = createReviewEvent({
|
|
1006
|
-
recurrenceKey: "bug:draft-target",
|
|
1007
|
-
state: "acknowledged",
|
|
1008
|
-
note: "reviewed token=abc123",
|
|
1009
|
-
});
|
|
1010
|
-
const draft = buildEscalationDraft({
|
|
1011
|
-
target: "github_issue",
|
|
1012
|
-
recurrenceKey: "bug:draft-target",
|
|
1013
|
-
records,
|
|
1014
|
-
reviewEvents: [review],
|
|
1015
|
-
limit: 3,
|
|
1016
|
-
now: "2026-05-21T00:10:00.000Z",
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
assert.equal(draft.samples.length, 3);
|
|
1020
|
-
assert.equal(draft.group.reviewState, "acknowledged");
|
|
1021
|
-
assert.match(draft.text, /Draft-only GitHub issue text/);
|
|
1022
|
-
assert.match(draft.text, /No AK task, GitHub issue, incident, evidence/);
|
|
1023
|
-
assert.match(draft.text, /Local diagnostic facets \(not owner routing\)/);
|
|
1024
|
-
assert.match(draft.text, /Tools: pi-reload-token-redacted/);
|
|
1025
|
-
assert.match(draft.text, /Packages: tryinget-pi-agent-vent/);
|
|
1026
|
-
assert.match(draft.text, /token=\[REDACTED\]/);
|
|
1027
|
-
assert.doesNotMatch(draft.text, /abc123|was created|was filed|was declared/);
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
test("draft-only escalation supports all targets and curation projections", () => {
|
|
1031
|
-
assert.deepEqual(DRAFT_TARGETS, [
|
|
1032
|
-
"github_issue",
|
|
1033
|
-
"ak_task",
|
|
1034
|
-
"incident_review",
|
|
1035
|
-
"maintainer_note",
|
|
1036
|
-
]);
|
|
1037
|
-
const first = createVentRecord(
|
|
1038
|
-
{
|
|
1039
|
-
summary: "Primary draft",
|
|
1040
|
-
category: "workflow",
|
|
1041
|
-
recurrenceKey: "draft-primary",
|
|
1042
|
-
tool: "pi reload",
|
|
1043
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
1044
|
-
},
|
|
1045
|
-
{ id: "v1" },
|
|
1046
|
-
);
|
|
1047
|
-
const second = createVentRecord(
|
|
1048
|
-
{
|
|
1049
|
-
summary: "Duplicate draft",
|
|
1050
|
-
category: "workflow",
|
|
1051
|
-
recurrenceKey: "draft-dupe",
|
|
1052
|
-
tool: "pi toolbox",
|
|
1053
|
-
packageName: "@tryinget/pi-toolbox-discovery",
|
|
1054
|
-
},
|
|
1055
|
-
{ id: "v2" },
|
|
1056
|
-
);
|
|
1057
|
-
const curation = createCurationEvent({
|
|
1058
|
-
action: "merge",
|
|
1059
|
-
sourceRecurrenceKey: second.recurrenceKey,
|
|
1060
|
-
targetRecurrenceKey: first.recurrenceKey,
|
|
1061
|
-
});
|
|
1062
|
-
|
|
1063
|
-
for (const target of DRAFT_TARGETS) {
|
|
1064
|
-
const draft = buildEscalationDraft({
|
|
1065
|
-
target,
|
|
1066
|
-
recurrenceKey: second.recurrenceKey,
|
|
1067
|
-
records: [first, second],
|
|
1068
|
-
curationEvents: [curation],
|
|
1069
|
-
});
|
|
1070
|
-
assert.equal(draft.recurrenceKey, first.recurrenceKey);
|
|
1071
|
-
assert.equal(draft.group.count, 2);
|
|
1072
|
-
assert.deepEqual(draft.group.tools, ["pi-reload", "pi-toolbox"]);
|
|
1073
|
-
assert.match(draft.text, /Draft-only/);
|
|
1074
|
-
assert.match(draft.text, /Local facets are hints, not owner routing/);
|
|
1075
|
-
}
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
test("draft-only escalation fails closed for unknown group and invalid target", () => {
|
|
1079
|
-
const records = [createVentRecord({ summary: "Known", category: "bug" }, { id: "v1" })];
|
|
1080
|
-
assert.throws(
|
|
1081
|
-
() => buildEscalationDraft({ target: "github_issue", recurrenceKey: "bug:missing", records }),
|
|
1082
|
-
/unknown recurrence group/,
|
|
1083
|
-
);
|
|
1084
|
-
assert.throws(
|
|
1085
|
-
() =>
|
|
1086
|
-
buildEscalationDraft({ target: "email", recurrenceKey: records[0].recurrenceKey, records }),
|
|
1087
|
-
/invalid agent_vent draft target/,
|
|
1088
|
-
);
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
test("diagnostic state membrane redacts legacy valid JSONL before draft/export/compare", () => {
|
|
1092
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-legacy-secret-"));
|
|
1093
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1094
|
-
fs.writeFileSync(
|
|
1095
|
-
storePath,
|
|
1096
|
-
`${JSON.stringify({
|
|
1097
|
-
id: "legacy-1",
|
|
1098
|
-
createdAt: "2026-05-21T00:00:00.000Z",
|
|
1099
|
-
category: "bug",
|
|
1100
|
-
severity: "high",
|
|
1101
|
-
recurrenceKey: "bug:legacy-secret",
|
|
1102
|
-
summary: "Legacy unredacted payload",
|
|
1103
|
-
evidence: "password=hunter2",
|
|
1104
|
-
reproduction: "Bearer abcdefghijklmnop",
|
|
1105
|
-
tool: "tool token=abc123",
|
|
1106
|
-
tags: ["secret=tagged"],
|
|
1107
|
-
privacy: {
|
|
1108
|
-
classification: "local-diagnostic-user-data",
|
|
1109
|
-
redacted: false,
|
|
1110
|
-
redactionPatterns: [],
|
|
1111
|
-
},
|
|
1112
|
-
})}\n`,
|
|
1113
|
-
"utf8",
|
|
1114
|
-
);
|
|
1115
|
-
const state = loadDiagnosticState({ storePath, reviewPath: path.join(dir, "review.jsonl") });
|
|
1116
|
-
const draft = buildEscalationDraft({
|
|
1117
|
-
target: "github_issue",
|
|
1118
|
-
recurrenceKey: "bug:legacy-secret",
|
|
1119
|
-
records: state.records,
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
assert.equal(state.records[0].privacy.redacted, true);
|
|
1123
|
-
assert.deepEqual(state.records[0].tags, ["secret-redacted"]);
|
|
1124
|
-
assert.equal(state.records[0].tool, "tool-token-redacted");
|
|
1125
|
-
const comparisonText = formatReviewComparison(
|
|
1126
|
-
buildReviewComparison({ records: state.records, filters: { tool: "tool token=abc123" } }),
|
|
1127
|
-
);
|
|
1128
|
-
|
|
1129
|
-
assert.match(draft.text, /password=\[REDACTED\]/);
|
|
1130
|
-
assert.match(draft.text, /Bearer \[REDACTED\]/);
|
|
1131
|
-
assert.match(comparisonText, /tool-token-redacted/);
|
|
1132
|
-
assert.doesNotMatch(`${draft.text}\n${comparisonText}`, /hunter2|abcdefghijklmnop|abc123/);
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
test("draft lookup is not constrained by display limits", () => {
|
|
1136
|
-
const records = [
|
|
1137
|
-
createVentRecord(
|
|
1138
|
-
{ summary: "Low-ranked target", category: "documentation", recurrenceKey: "target-old" },
|
|
1139
|
-
{ id: "target", now: "2026-01-01T00:00:00.000Z" },
|
|
1140
|
-
),
|
|
1141
|
-
...Array.from({ length: 120 }, (_, index) =>
|
|
1142
|
-
createVentRecord(
|
|
1143
|
-
{
|
|
1144
|
-
summary: `Higher ranked group ${index}`,
|
|
1145
|
-
category: "documentation",
|
|
1146
|
-
recurrenceKey: `higher-${index}`,
|
|
1147
|
-
severity: "critical",
|
|
1148
|
-
},
|
|
1149
|
-
{ id: `v-${index}`, now: `2026-02-01T00:00:${String(index % 60).padStart(2, "0")}.000Z` },
|
|
1150
|
-
),
|
|
1151
|
-
),
|
|
1152
|
-
];
|
|
1153
|
-
|
|
1154
|
-
const draft = buildEscalationDraft({
|
|
1155
|
-
target: "github_issue",
|
|
1156
|
-
recurrenceKey: "documentation:target-old",
|
|
1157
|
-
records,
|
|
1158
|
-
});
|
|
1159
|
-
assert.equal(draft.recurrenceKey, "documentation:target-old");
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
test("diagnostic state quarantines semantic curation cycles", () => {
|
|
1163
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-curation-cycle-"));
|
|
1164
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1165
|
-
const curationPath = path.join(dir, "curation-events.jsonl");
|
|
1166
|
-
const a = createVentRecord({ summary: "A", category: "bug", recurrenceKey: "a" });
|
|
1167
|
-
const b = createVentRecord({ summary: "B", category: "bug", recurrenceKey: "b" });
|
|
1168
|
-
appendVentRecord(storePath, a);
|
|
1169
|
-
appendVentRecord(storePath, b);
|
|
1170
|
-
fs.writeFileSync(
|
|
1171
|
-
curationPath,
|
|
1172
|
-
`${JSON.stringify({ eventType: "recurrence_curation", action: "merge", sourceRecurrenceKey: a.recurrenceKey, targetRecurrenceKey: b.recurrenceKey })}\n${JSON.stringify({ eventType: "recurrence_curation", action: "merge", sourceRecurrenceKey: b.recurrenceKey, targetRecurrenceKey: a.recurrenceKey })}\n`,
|
|
1173
|
-
"utf8",
|
|
1174
|
-
);
|
|
1175
|
-
|
|
1176
|
-
const state = loadDiagnosticState({ storePath, curationPath });
|
|
1177
|
-
assert.equal(state.quarantinedCurationEvents, 1);
|
|
1178
|
-
assert.doesNotThrow(() =>
|
|
1179
|
-
summarizeRecords(state.records, { curationEvents: state.curationEvents }),
|
|
1180
|
-
);
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
|
-
test("curation remove is append-only rollback", () => {
|
|
1184
|
-
const records = [
|
|
1185
|
-
createVentRecord({ summary: "A", category: "bug", recurrenceKey: "a" }, { id: "a" }),
|
|
1186
|
-
createVentRecord({ summary: "B", category: "bug", recurrenceKey: "b" }, { id: "b" }),
|
|
1187
|
-
];
|
|
1188
|
-
const merge = createCurationEvent({
|
|
1189
|
-
action: "merge",
|
|
1190
|
-
sourceRecurrenceKey: "bug:a",
|
|
1191
|
-
targetRecurrenceKey: "bug:b",
|
|
1192
|
-
});
|
|
1193
|
-
assertCanCurateRecurrence(records, [merge], {
|
|
1194
|
-
action: "remove",
|
|
1195
|
-
sourceRecurrenceKey: "bug:a",
|
|
1196
|
-
});
|
|
1197
|
-
const remove = createCurationEvent({ action: "remove", sourceRecurrenceKey: "bug:a" });
|
|
1198
|
-
const summary = summarizeRecords(records, { curationEvents: [merge, remove] });
|
|
1199
|
-
assert.equal(summary.groupCount, 2);
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
test("diagnostic state membrane bounds file and line size", () => {
|
|
1203
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-size-"));
|
|
1204
|
-
const linePath = path.join(dir, "oversized-line.jsonl");
|
|
1205
|
-
fs.writeFileSync(linePath, `${"x".repeat(MAX_JSONL_LINE_BYTES + 1)}\n`, "utf8");
|
|
1206
|
-
const lineResult = readVentRecords(linePath);
|
|
1207
|
-
assert.equal(lineResult.oversizedLines, 1);
|
|
1208
|
-
assert.equal(lineResult.records.length, 0);
|
|
1209
|
-
|
|
1210
|
-
const filePath = path.join(dir, "oversized-file.jsonl");
|
|
1211
|
-
fs.writeFileSync(filePath, "x".repeat(MAX_JSONL_FILE_BYTES + 1), "utf8");
|
|
1212
|
-
assert.throws(() => readVentRecords(filePath), /store file is too large/);
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
test("lifecycle stats and exports are bounded local projections", () => {
|
|
1216
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-lifecycle-"));
|
|
1217
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1218
|
-
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
1219
|
-
const record = createVentRecord(
|
|
1220
|
-
{ summary: "Lifecycle export captures recurrence", category: "documentation" },
|
|
1221
|
-
{ id: "v1" },
|
|
1222
|
-
);
|
|
1223
|
-
const review = createReviewEvent({
|
|
1224
|
-
recurrenceKey: record.recurrenceKey,
|
|
1225
|
-
state: "escalation_drafted",
|
|
1226
|
-
note: "Draft locally only",
|
|
1227
|
-
});
|
|
1228
|
-
appendVentRecord(storePath, record);
|
|
1229
|
-
appendReviewEvent(reviewPath, review);
|
|
1230
|
-
|
|
1231
|
-
const snapshot = buildLifecycleSnapshot({
|
|
1232
|
-
records: readVentRecords(storePath).records,
|
|
1233
|
-
reviewEvents: readReviewEvents(reviewPath).events,
|
|
1234
|
-
storePath,
|
|
1235
|
-
reviewPath,
|
|
1236
|
-
now: "2026-05-21T00:00:00.000Z",
|
|
1237
|
-
});
|
|
1238
|
-
|
|
1239
|
-
assert.equal(snapshot.counts.vents, 1);
|
|
1240
|
-
assert.equal(snapshot.counts.reviewStates.escalation_drafted, 1);
|
|
1241
|
-
assert.equal(snapshot.files.vents.exists, true);
|
|
1242
|
-
assert.match(formatLifecycleStats(snapshot), /no AK task, GitHub issue, incident, evidence/);
|
|
1243
|
-
const markdown = formatExportMarkdown(snapshot);
|
|
1244
|
-
assert.match(markdown, /# Agent vent local diagnostic export/);
|
|
1245
|
-
assert.match(markdown, /Decision posture: draft_recorded_owner_external/);
|
|
1246
|
-
assert.match(
|
|
1247
|
-
markdown,
|
|
1248
|
-
/not resolution, assignment, evidence, publication, issue status, task truth, or incident state/,
|
|
1249
|
-
);
|
|
1250
|
-
assert.match(markdown, /## Safe local follow-up/);
|
|
1251
|
-
assert.match(markdown, /Optional local retention preview: \/agent_vent retention preview/);
|
|
1252
|
-
assert.match(markdown, /Draft-only handoff:/);
|
|
1253
|
-
assert.match(
|
|
1254
|
-
markdown,
|
|
1255
|
-
/They do not file, create, declare, assign, record evidence, publish, archive, restore/,
|
|
1256
|
-
);
|
|
1257
|
-
const json = JSON.parse(formatExportJson(snapshot));
|
|
1258
|
-
assert.equal(json.counts.vents, 1);
|
|
1259
|
-
assert.equal(json.nextActions[0].decisionPosture.state, "draft_recorded_owner_external");
|
|
1260
|
-
assert.equal(
|
|
1261
|
-
json.nextActions[0].retentionPreviewCommand,
|
|
1262
|
-
`/agent_vent retention preview ${record.recurrenceKey}`,
|
|
1263
|
-
);
|
|
1264
|
-
assert.doesNotMatch(
|
|
1265
|
-
JSON.stringify(json.nextActions),
|
|
1266
|
-
/archive:[a-f0-9]|restore:[a-f0-9]|owner was assigned/,
|
|
1267
|
-
);
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
|
-
test("lifecycle export can preserve facet scope without owner-routing claims", () => {
|
|
1271
|
-
const keep = createVentRecord({
|
|
1272
|
-
summary: "Keep export scoped token=abc123",
|
|
1273
|
-
category: "tool_failure",
|
|
1274
|
-
recurrenceKey: "export-keep",
|
|
1275
|
-
tags: ["Export"],
|
|
1276
|
-
tool: "pi reload",
|
|
1277
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
1278
|
-
});
|
|
1279
|
-
const omit = createVentRecord({
|
|
1280
|
-
summary: "Omit unrelated export group",
|
|
1281
|
-
category: "documentation",
|
|
1282
|
-
recurrenceKey: "export-omit",
|
|
1283
|
-
tags: ["Other"],
|
|
1284
|
-
});
|
|
1285
|
-
const mixedOut = createVentRecord({
|
|
1286
|
-
summary: "Omit mixed-group private payload",
|
|
1287
|
-
category: "tool_failure",
|
|
1288
|
-
recurrenceKey: "export-keep",
|
|
1289
|
-
tags: ["Other"],
|
|
1290
|
-
tool: "pi reload",
|
|
1291
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
1292
|
-
});
|
|
1293
|
-
const review = createReviewEvent({
|
|
1294
|
-
recurrenceKey: keep.recurrenceKey,
|
|
1295
|
-
state: "acknowledged",
|
|
1296
|
-
note: "review token=abc123 locally",
|
|
1297
|
-
});
|
|
1298
|
-
|
|
1299
|
-
const snapshot = buildLifecycleSnapshot({
|
|
1300
|
-
records: [keep, omit, mixedOut],
|
|
1301
|
-
reviewEvents: [review],
|
|
1302
|
-
filters: { tags: ["export"], tool: "pi reload", packageName: "@tryinget/pi-agent-vent" },
|
|
1303
|
-
state: "acknowledged",
|
|
1304
|
-
limit: 10,
|
|
1305
|
-
now: "2026-05-22T00:00:00.000Z",
|
|
1306
|
-
});
|
|
1307
|
-
|
|
1308
|
-
assert.equal(snapshot.scope.hasFilters, true);
|
|
1309
|
-
assert.equal(snapshot.scope.matchingGroups, 1);
|
|
1310
|
-
assert.equal(snapshot.counts.vents, 1);
|
|
1311
|
-
assert.equal(snapshot.counts.reviewStates.acknowledged, 1);
|
|
1312
|
-
assert.equal(snapshot.reviewQueue.items.length, 1);
|
|
1313
|
-
assert.equal(snapshot.reviewQueue.items[0].recurrenceKey, keep.recurrenceKey);
|
|
1314
|
-
assert.equal(snapshot.summary.groups[0].recurrenceKey, keep.recurrenceKey);
|
|
1315
|
-
const markdown = formatExportMarkdown(snapshot);
|
|
1316
|
-
assert.match(markdown, /Filters: tool=pi-reload; package=tryinget-pi-agent-vent; tags=export/);
|
|
1317
|
-
assert.match(markdown, /not owner routing or owner assignment/);
|
|
1318
|
-
assert.match(markdown, /token=\[REDACTED\]/);
|
|
1319
|
-
assert.match(markdown, /Safe local follow-up/);
|
|
1320
|
-
assert.match(
|
|
1321
|
-
markdown,
|
|
1322
|
-
/Optional local retention preview: \/agent_vent retention preview tool_failure:export-keep/,
|
|
1323
|
-
);
|
|
1324
|
-
assert.doesNotMatch(
|
|
1325
|
-
markdown,
|
|
1326
|
-
/Omit unrelated export group|Omit mixed-group private payload|owner was assigned|archive:[a-f0-9]/,
|
|
1327
|
-
);
|
|
1328
|
-
const json = JSON.parse(formatExportJson(snapshot));
|
|
1329
|
-
assert.equal(json.scope.filters.tool, "pi-reload");
|
|
1330
|
-
assert.equal(json.counts.vents, 1);
|
|
1331
|
-
assert.equal(
|
|
1332
|
-
json.nextActions[0].exportBucketCommand,
|
|
1333
|
-
"/agent_vent export markdown acknowledged tool=pi-reload package=tryinget-pi-agent-vent tag=export",
|
|
1334
|
-
);
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
test("lifecycle export state scope constrains counts and summaries", () => {
|
|
1338
|
-
const acknowledged = createVentRecord({
|
|
1339
|
-
summary: "Acknowledged export group",
|
|
1340
|
-
category: "bug",
|
|
1341
|
-
recurrenceKey: "ack-export",
|
|
1342
|
-
tags: ["export-state"],
|
|
1343
|
-
});
|
|
1344
|
-
const fresh = createVentRecord({
|
|
1345
|
-
summary: "Fresh export group should not appear",
|
|
1346
|
-
category: "bug",
|
|
1347
|
-
recurrenceKey: "fresh-export",
|
|
1348
|
-
tags: ["export-state"],
|
|
1349
|
-
});
|
|
1350
|
-
const review = createReviewEvent({
|
|
1351
|
-
recurrenceKey: acknowledged.recurrenceKey,
|
|
1352
|
-
state: "acknowledged",
|
|
1353
|
-
});
|
|
1354
|
-
|
|
1355
|
-
const snapshot = buildLifecycleSnapshot({
|
|
1356
|
-
records: [acknowledged, fresh],
|
|
1357
|
-
reviewEvents: [review],
|
|
1358
|
-
filters: { tags: ["export-state"] },
|
|
1359
|
-
state: "acknowledged",
|
|
1360
|
-
limit: 10,
|
|
1361
|
-
});
|
|
1362
|
-
|
|
1363
|
-
assert.equal(snapshot.scope.stateFilter, "acknowledged");
|
|
1364
|
-
assert.equal(snapshot.scope.facetMatchingGroups, 2);
|
|
1365
|
-
assert.equal(snapshot.scope.matchingGroups, 1);
|
|
1366
|
-
assert.equal(snapshot.counts.vents, 1);
|
|
1367
|
-
assert.equal(snapshot.counts.reviewStates.new, 0);
|
|
1368
|
-
assert.equal(snapshot.counts.reviewStates.acknowledged, 1);
|
|
1369
|
-
assert.equal(snapshot.summary.groups.length, 1);
|
|
1370
|
-
assert.equal(snapshot.reviewQueue.items.length, 1);
|
|
1371
|
-
const markdown = formatExportMarkdown(snapshot);
|
|
1372
|
-
assert.match(markdown, /Acknowledged export group/);
|
|
1373
|
-
assert.doesNotMatch(markdown, /Fresh export group should not appear/);
|
|
1374
|
-
});
|
|
1375
|
-
|
|
1376
|
-
test("lifecycle export follow-up commands quote legacy keys without authority claims", () => {
|
|
1377
|
-
const legacy = {
|
|
1378
|
-
...createVentRecord({ summary: "Legacy export key", category: "bug" }),
|
|
1379
|
-
recurrenceKey: 'bug:legacy key "quoted"',
|
|
1380
|
-
};
|
|
1381
|
-
const snapshot = buildLifecycleSnapshot({ records: [legacy], reviewEvents: [], limit: 5 });
|
|
1382
|
-
const markdown = formatExportMarkdown(snapshot);
|
|
1383
|
-
const json = JSON.parse(formatExportJson(snapshot));
|
|
1384
|
-
|
|
1385
|
-
assert.match(markdown, /\/agent_vent review show "bug:legacy key \\"quoted\\""/);
|
|
1386
|
-
assert.match(markdown, /Choose local review state:/);
|
|
1387
|
-
assert.doesNotMatch(markdown, /retention preview "bug:legacy key/);
|
|
1388
|
-
assert.match(markdown, /draft commands only generate local text|local diagnostics\/drafts only/);
|
|
1389
|
-
assert.equal(json.nextActions[0].reviewCommands.length, 3);
|
|
1390
|
-
assert.equal(json.nextActions[0].retentionPreviewCommand, undefined);
|
|
1391
|
-
assert.doesNotMatch(
|
|
1392
|
-
JSON.stringify(json.nextActions),
|
|
1393
|
-
/archive:[a-f0-9]|restore:[a-f0-9]|filed|owner was assigned/,
|
|
1394
|
-
);
|
|
1395
|
-
});
|
|
1396
|
-
|
|
1397
|
-
test("lifecycle export rejects empty facet filters", () => {
|
|
1398
|
-
assert.throws(
|
|
1399
|
-
() => buildLifecycleSnapshot({ records: [], filters: { tags: [""] } }),
|
|
1400
|
-
/invalid agent_vent review filter tag: empty value/,
|
|
1401
|
-
);
|
|
1402
|
-
assert.throws(
|
|
1403
|
-
() => buildLifecycleSnapshot({ records: [], filters: { tool: "" } }),
|
|
1404
|
-
/invalid agent_vent review filter tool: empty value/,
|
|
1405
|
-
);
|
|
1406
|
-
});
|
|
1407
|
-
|
|
1408
|
-
test("lifecycle stats report missing stores without creating files", () => {
|
|
1409
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-lifecycle-missing-"));
|
|
1410
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1411
|
-
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
1412
|
-
const snapshot = buildLifecycleSnapshot({ storePath, reviewPath });
|
|
1413
|
-
|
|
1414
|
-
assert.equal(snapshot.files.vents.exists, false);
|
|
1415
|
-
assert.equal(snapshot.files.reviewEvents.sizeBytes, 0);
|
|
1416
|
-
assert.equal(fs.existsSync(storePath), false);
|
|
1417
|
-
assert.match(formatLifecycleStats(snapshot), /vents: 0 record/);
|
|
1418
|
-
});
|
|
1419
|
-
|
|
1420
|
-
test("lifecycle stats count more groups than the display limit", () => {
|
|
1421
|
-
const records = Array.from({ length: 125 }, (_, index) =>
|
|
1422
|
-
createVentRecord(
|
|
1423
|
-
{
|
|
1424
|
-
summary: `Distinct lifecycle group ${index}`,
|
|
1425
|
-
category: "workflow",
|
|
1426
|
-
recurrenceKey: `distinct-${index}`,
|
|
1427
|
-
},
|
|
1428
|
-
{ id: `v-${index}` },
|
|
1429
|
-
),
|
|
1430
|
-
);
|
|
1431
|
-
const snapshot = buildLifecycleSnapshot({ records, limit: 5 });
|
|
1432
|
-
|
|
1433
|
-
assert.equal(snapshot.counts.recurrenceGroups, 125);
|
|
1434
|
-
assert.equal(snapshot.summary.groups.length, 5);
|
|
1435
|
-
assert.equal(snapshot.reviewQueue.items.length, 5);
|
|
1436
|
-
});
|
|
1437
|
-
|
|
1438
|
-
test("retention candidates list reviewed groups without tokens or mutation", () => {
|
|
1439
|
-
const fresh = createVentRecord({
|
|
1440
|
-
summary: "Fresh retention group",
|
|
1441
|
-
category: "workflow",
|
|
1442
|
-
recurrenceKey: "fresh-retention",
|
|
1443
|
-
});
|
|
1444
|
-
const acknowledged = createVentRecord({
|
|
1445
|
-
summary: "Acknowledged retention group token=abc123",
|
|
1446
|
-
category: "tool_failure",
|
|
1447
|
-
recurrenceKey: "ack-retention",
|
|
1448
|
-
tags: ["Retention"],
|
|
1449
|
-
tool: "pi reload",
|
|
1450
|
-
packageName: "@tryinget/pi-agent-vent",
|
|
1451
|
-
});
|
|
1452
|
-
const dismissed = createVentRecord({
|
|
1453
|
-
summary: "Dismissed retention group",
|
|
1454
|
-
category: "documentation",
|
|
1455
|
-
recurrenceKey: "dismissed-retention",
|
|
1456
|
-
tags: ["Retention"],
|
|
1457
|
-
});
|
|
1458
|
-
const reviews = [
|
|
1459
|
-
createReviewEvent({
|
|
1460
|
-
recurrenceKey: acknowledged.recurrenceKey,
|
|
1461
|
-
state: "acknowledged",
|
|
1462
|
-
note: "reviewed token=abc123 locally",
|
|
1463
|
-
}),
|
|
1464
|
-
createReviewEvent({ recurrenceKey: dismissed.recurrenceKey, state: "dismissed" }),
|
|
1465
|
-
];
|
|
1466
|
-
|
|
1467
|
-
const candidates = buildRetentionCandidates({
|
|
1468
|
-
records: [fresh, acknowledged, dismissed],
|
|
1469
|
-
reviewEvents: reviews,
|
|
1470
|
-
filters: { tags: ["retention"], tool: "pi reload" },
|
|
1471
|
-
limit: 10,
|
|
1472
|
-
now: "2026-05-22T00:00:00.000Z",
|
|
1473
|
-
});
|
|
1474
|
-
assert.equal(candidates.stateFilter, "reviewed");
|
|
1475
|
-
assert.equal(candidates.groupCount, 3);
|
|
1476
|
-
assert.equal(candidates.matchingGroupCount, 1);
|
|
1477
|
-
assert.equal(candidates.candidateCount, 1);
|
|
1478
|
-
assert.equal(candidates.items[0].reviewState, "acknowledged");
|
|
1479
|
-
assert.equal(candidates.filters.tool, "pi-reload");
|
|
1480
|
-
|
|
1481
|
-
const text = formatRetentionCandidates(candidates);
|
|
1482
|
-
assert.match(text, /Agent vent retention candidates/);
|
|
1483
|
-
assert.match(text, /read-only planning view/);
|
|
1484
|
-
assert.match(text, /preview to request archive token: \/agent_vent retention preview/);
|
|
1485
|
-
assert.match(text, /reviewed token=\[REDACTED\] locally/);
|
|
1486
|
-
assert.match(
|
|
1487
|
-
text,
|
|
1488
|
-
/export this outcome bucket: \/agent_vent export markdown acknowledged tool=pi-reload tag=retention/,
|
|
1489
|
-
);
|
|
1490
|
-
assert.match(text, /No archive, restore, AK task, GitHub issue, incident, evidence/);
|
|
1491
|
-
assert.match(text, /not owner routing/);
|
|
1492
|
-
assert.doesNotMatch(text, /archive:[a-f0-9]/);
|
|
1493
|
-
assert.doesNotMatch(text, /abc123|was archived|was filed|owner was assigned/);
|
|
1494
|
-
|
|
1495
|
-
const allCandidates = buildRetentionCandidates({
|
|
1496
|
-
records: [fresh, acknowledged],
|
|
1497
|
-
reviewEvents: reviews,
|
|
1498
|
-
state: "all",
|
|
1499
|
-
});
|
|
1500
|
-
assert.equal(allCandidates.candidateCount, 2);
|
|
1501
|
-
assert.match(formatRetentionCandidates(allCandidates), /review before archive/);
|
|
1502
|
-
assert.throws(
|
|
1503
|
-
() => buildRetentionCandidates({ records: [], reviewEvents: [], state: "resolved" }),
|
|
1504
|
-
/invalid agent_vent review state/,
|
|
1505
|
-
);
|
|
1506
|
-
assert.throws(
|
|
1507
|
-
() => buildRetentionCandidates({ filters: { category: "../../etc/passwd" } }),
|
|
1508
|
-
/invalid agent_vent review filter category/,
|
|
1509
|
-
);
|
|
1510
|
-
});
|
|
1511
|
-
|
|
1512
|
-
test("retention archive is confirmation-gated, backed up, receipted, and restorable", () => {
|
|
1513
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-retention-"));
|
|
1514
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1515
|
-
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
1516
|
-
const retentionPath = path.join(dir, "retention-events.jsonl");
|
|
1517
|
-
const backupDir = path.join(dir, "backups");
|
|
1518
|
-
const keep = createVentRecord(
|
|
1519
|
-
{ summary: "Keep active", category: "workflow", recurrenceKey: "keep" },
|
|
1520
|
-
{ id: "keep" },
|
|
1521
|
-
);
|
|
1522
|
-
const archive = createVentRecord(
|
|
1523
|
-
{ summary: "Archive reviewed", category: "workflow", recurrenceKey: "archive-me" },
|
|
1524
|
-
{ id: "archive" },
|
|
1525
|
-
);
|
|
1526
|
-
appendVentRecord(storePath, keep);
|
|
1527
|
-
appendVentRecord(storePath, archive);
|
|
1528
|
-
appendReviewEvent(
|
|
1529
|
-
reviewPath,
|
|
1530
|
-
createReviewEvent({
|
|
1531
|
-
recurrenceKey: archive.recurrenceKey,
|
|
1532
|
-
state: "dismissed",
|
|
1533
|
-
note: "reviewed token=abc123",
|
|
1534
|
-
}),
|
|
1535
|
-
);
|
|
1536
|
-
|
|
1537
|
-
const state = loadDiagnosticState({ storePath, reviewPath, retentionPath, backupDir });
|
|
1538
|
-
assert.throws(
|
|
1539
|
-
() =>
|
|
1540
|
-
buildRetentionPreview({
|
|
1541
|
-
recurrenceKey: archive.recurrenceKey,
|
|
1542
|
-
records: state.records,
|
|
1543
|
-
reviewEvents: state.reviewEvents,
|
|
1544
|
-
storeHash: state.ventsHash,
|
|
1545
|
-
}),
|
|
1546
|
-
/requires current store, review, and curation hashes/,
|
|
1547
|
-
);
|
|
1548
|
-
const preview = buildRetentionPreview({
|
|
1549
|
-
recurrenceKey: archive.recurrenceKey,
|
|
1550
|
-
records: state.records,
|
|
1551
|
-
reviewEvents: state.reviewEvents,
|
|
1552
|
-
storeHash: state.ventsHash,
|
|
1553
|
-
reviewHash: state.reviewEventsHash,
|
|
1554
|
-
curationHash: state.curationEventsHash,
|
|
1555
|
-
});
|
|
1556
|
-
assert.equal(preview.archivable, true);
|
|
1557
|
-
assert.equal(preview.archivedRecordCount, 1);
|
|
1558
|
-
assert.match(preview.confirmationToken, /^archive:/);
|
|
1559
|
-
assert.throws(
|
|
1560
|
-
() =>
|
|
1561
|
-
archiveRecurrenceGroup({
|
|
1562
|
-
storePath,
|
|
1563
|
-
reviewPath,
|
|
1564
|
-
retentionPath,
|
|
1565
|
-
backupDir,
|
|
1566
|
-
recurrenceKey: archive.recurrenceKey,
|
|
1567
|
-
confirmationToken: "archive:wrong",
|
|
1568
|
-
}),
|
|
1569
|
-
/requires exact confirmation token/,
|
|
1570
|
-
);
|
|
1571
|
-
|
|
1572
|
-
const result = archiveRecurrenceGroup({
|
|
1573
|
-
storePath,
|
|
1574
|
-
reviewPath,
|
|
1575
|
-
retentionPath,
|
|
1576
|
-
backupDir,
|
|
1577
|
-
recurrenceKey: archive.recurrenceKey,
|
|
1578
|
-
confirmationToken: preview.confirmationToken,
|
|
1579
|
-
note: "archive note password=hunter2",
|
|
1580
|
-
});
|
|
1581
|
-
assert.equal(readVentRecords(storePath).records.length, 1);
|
|
1582
|
-
assert.equal(readVentRecords(storePath).records[0].id, "keep");
|
|
1583
|
-
assert.equal(fs.existsSync(result.backupPath), true);
|
|
1584
|
-
assert.match(result.restoreConfirmationToken, /^restore:/);
|
|
1585
|
-
const retentionEvents = readRetentionEvents(retentionPath).events;
|
|
1586
|
-
assert.equal(retentionEvents.length, 1);
|
|
1587
|
-
assert.equal(retentionEvents[0].action, "archive");
|
|
1588
|
-
assert.match(retentionEvents[0].note, /password=\[REDACTED\]/);
|
|
1589
|
-
assert.match(result.boundary, /No AK task, GitHub issue, incident, evidence/);
|
|
1590
|
-
|
|
1591
|
-
const restore = restoreRetentionBackup({
|
|
1592
|
-
storePath,
|
|
1593
|
-
retentionPath,
|
|
1594
|
-
backupDir,
|
|
1595
|
-
backupPath: result.backupPath,
|
|
1596
|
-
confirmationToken: result.restoreConfirmationToken,
|
|
1597
|
-
});
|
|
1598
|
-
assert.equal(restore.restoredRecordCount, 1);
|
|
1599
|
-
assert.equal(readVentRecords(storePath).records.length, 2);
|
|
1600
|
-
assert.equal(readRetentionEvents(retentionPath).events.length, 2);
|
|
1601
|
-
});
|
|
1602
|
-
|
|
1603
|
-
test("retention history reconstructs restore candidates without trusting escaped receipt paths", () => {
|
|
1604
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent vent retention history-"));
|
|
1605
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1606
|
-
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
1607
|
-
const retentionPath = path.join(dir, "retention-events.jsonl");
|
|
1608
|
-
const backupDir = path.join(dir, "backups");
|
|
1609
|
-
const record = createVentRecord(
|
|
1610
|
-
{ summary: "History archive", category: "workflow", recurrenceKey: "history archive" },
|
|
1611
|
-
{ id: "history" },
|
|
1612
|
-
);
|
|
1613
|
-
appendVentRecord(storePath, record);
|
|
1614
|
-
appendReviewEvent(
|
|
1615
|
-
reviewPath,
|
|
1616
|
-
createReviewEvent({ recurrenceKey: record.recurrenceKey, state: "acknowledged" }),
|
|
1617
|
-
);
|
|
1618
|
-
|
|
1619
|
-
const state = loadDiagnosticState({ storePath, reviewPath, retentionPath, backupDir });
|
|
1620
|
-
const preview = buildRetentionPreview({
|
|
1621
|
-
recurrenceKey: record.recurrenceKey,
|
|
1622
|
-
records: state.records,
|
|
1623
|
-
reviewEvents: state.reviewEvents,
|
|
1624
|
-
curationEvents: state.curationEvents,
|
|
1625
|
-
storeHash: state.ventsHash,
|
|
1626
|
-
reviewHash: state.reviewEventsHash,
|
|
1627
|
-
curationHash: state.curationEventsHash,
|
|
1628
|
-
});
|
|
1629
|
-
const archived = archiveRecurrenceGroup({
|
|
1630
|
-
storePath,
|
|
1631
|
-
reviewPath,
|
|
1632
|
-
retentionPath,
|
|
1633
|
-
backupDir,
|
|
1634
|
-
recurrenceKey: record.recurrenceKey,
|
|
1635
|
-
confirmationToken: preview.confirmationToken,
|
|
1636
|
-
});
|
|
1637
|
-
const events = readRetentionEvents(retentionPath).events;
|
|
1638
|
-
const history = buildRetentionHistory({ retentionEvents: events, backupDir, limit: 5 });
|
|
1639
|
-
|
|
1640
|
-
assert.equal(history.totalEvents, 1);
|
|
1641
|
-
assert.match(history.items[0].restoreConfirmationToken, /^restore:/);
|
|
1642
|
-
assert.equal(history.items[0].restoreConfirmationToken, archived.restoreConfirmationToken);
|
|
1643
|
-
assert.match(history.items[0].restoreCommand, /retention restore/);
|
|
1644
|
-
assert.match(history.items[0].restoreCommand, /".*history-archive.*\.agent-vent-backup\.json"/);
|
|
1645
|
-
const text = formatRetentionHistory(history);
|
|
1646
|
-
assert.match(text, /read-only receipt projection/i);
|
|
1647
|
-
assert.match(text, /rollback candidate: \/agent_vent retention restore/);
|
|
1648
|
-
assert.match(text, /stale\/moved\/path-invalid backups fail closed/);
|
|
1649
|
-
assert.doesNotMatch(
|
|
1650
|
-
text,
|
|
1651
|
-
/AK task was created|GitHub issue was created|incident was declared|evidence was recorded|owner was assigned/,
|
|
1652
|
-
);
|
|
1653
|
-
|
|
1654
|
-
const escaped = buildRetentionHistory({
|
|
1655
|
-
retentionEvents: [
|
|
1656
|
-
{
|
|
1657
|
-
...events[0],
|
|
1658
|
-
id: "evil-receipt",
|
|
1659
|
-
backupPath: path.join(dir, "outside.agent-vent-backup.json"),
|
|
1660
|
-
},
|
|
1661
|
-
],
|
|
1662
|
-
backupDir,
|
|
1663
|
-
});
|
|
1664
|
-
assert.equal(escaped.items[0].restoreCommand, undefined);
|
|
1665
|
-
assert.match(
|
|
1666
|
-
escaped.items[0].restoreUnavailableReason,
|
|
1667
|
-
/outside the configured backup directory/,
|
|
1668
|
-
);
|
|
1669
|
-
});
|
|
1670
|
-
|
|
1671
|
-
test("retention archive removes only selected group records when legacy ids collide", () => {
|
|
1672
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-retention-duplicate-id-"));
|
|
1673
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1674
|
-
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
1675
|
-
const retentionPath = path.join(dir, "retention-events.jsonl");
|
|
1676
|
-
const backupDir = path.join(dir, "backups");
|
|
1677
|
-
const archive = createVentRecord(
|
|
1678
|
-
{ summary: "Archive duplicate id", category: "bug", recurrenceKey: "archive-duplicate" },
|
|
1679
|
-
{ id: "duplicate-id" },
|
|
1680
|
-
);
|
|
1681
|
-
const keep = createVentRecord(
|
|
1682
|
-
{ summary: "Keep duplicate id", category: "workflow", recurrenceKey: "keep-duplicate" },
|
|
1683
|
-
{ id: "duplicate-id" },
|
|
1684
|
-
);
|
|
1685
|
-
appendVentRecord(storePath, archive);
|
|
1686
|
-
appendVentRecord(storePath, keep);
|
|
1687
|
-
appendReviewEvent(
|
|
1688
|
-
reviewPath,
|
|
1689
|
-
createReviewEvent({ recurrenceKey: archive.recurrenceKey, state: "acknowledged" }),
|
|
1690
|
-
);
|
|
1691
|
-
|
|
1692
|
-
const state = loadDiagnosticState({ storePath, reviewPath, retentionPath, backupDir });
|
|
1693
|
-
const preview = buildRetentionPreview({
|
|
1694
|
-
recurrenceKey: archive.recurrenceKey,
|
|
1695
|
-
records: state.records,
|
|
1696
|
-
reviewEvents: state.reviewEvents,
|
|
1697
|
-
storeHash: state.ventsHash,
|
|
1698
|
-
reviewHash: state.reviewEventsHash,
|
|
1699
|
-
curationHash: state.curationEventsHash,
|
|
1700
|
-
});
|
|
1701
|
-
archiveRecurrenceGroup({
|
|
1702
|
-
storePath,
|
|
1703
|
-
reviewPath,
|
|
1704
|
-
retentionPath,
|
|
1705
|
-
backupDir,
|
|
1706
|
-
recurrenceKey: archive.recurrenceKey,
|
|
1707
|
-
confirmationToken: preview.confirmationToken,
|
|
1708
|
-
});
|
|
1709
|
-
|
|
1710
|
-
const remaining = readVentRecords(storePath).records;
|
|
1711
|
-
assert.equal(remaining.length, 1);
|
|
1712
|
-
assert.equal(remaining[0].recurrenceKey, keep.recurrenceKey);
|
|
1713
|
-
assert.equal(remaining[0].id, "duplicate-id");
|
|
1714
|
-
});
|
|
1715
|
-
|
|
1716
|
-
test("retention fails closed for unreviewed groups, path escape, and stale restore", () => {
|
|
1717
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-retention-negative-"));
|
|
1718
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1719
|
-
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
1720
|
-
const retentionPath = path.join(dir, "retention-events.jsonl");
|
|
1721
|
-
const backupDir = path.join(dir, "backups");
|
|
1722
|
-
const record = createVentRecord(
|
|
1723
|
-
{ summary: "Needs lifecycle", category: "bug", recurrenceKey: "needs-lifecycle" },
|
|
1724
|
-
{ id: "v1" },
|
|
1725
|
-
);
|
|
1726
|
-
appendVentRecord(storePath, record);
|
|
1727
|
-
const unreviewed = loadDiagnosticState({ storePath, reviewPath, retentionPath, backupDir });
|
|
1728
|
-
const preview = buildRetentionPreview({
|
|
1729
|
-
recurrenceKey: record.recurrenceKey,
|
|
1730
|
-
records: unreviewed.records,
|
|
1731
|
-
reviewEvents: unreviewed.reviewEvents,
|
|
1732
|
-
storeHash: unreviewed.ventsHash,
|
|
1733
|
-
reviewHash: unreviewed.reviewEventsHash,
|
|
1734
|
-
curationHash: unreviewed.curationEventsHash,
|
|
1735
|
-
});
|
|
1736
|
-
assert.equal(preview.archivable, false);
|
|
1737
|
-
assert.throws(
|
|
1738
|
-
() =>
|
|
1739
|
-
archiveRecurrenceGroup({
|
|
1740
|
-
storePath,
|
|
1741
|
-
reviewPath,
|
|
1742
|
-
retentionPath,
|
|
1743
|
-
backupDir,
|
|
1744
|
-
recurrenceKey: record.recurrenceKey,
|
|
1745
|
-
confirmationToken: preview.confirmationToken,
|
|
1746
|
-
}),
|
|
1747
|
-
/before local review/,
|
|
1748
|
-
);
|
|
1749
|
-
|
|
1750
|
-
appendReviewEvent(
|
|
1751
|
-
reviewPath,
|
|
1752
|
-
createReviewEvent({ recurrenceKey: record.recurrenceKey, state: "acknowledged" }),
|
|
1753
|
-
);
|
|
1754
|
-
assert.throws(
|
|
1755
|
-
() =>
|
|
1756
|
-
archiveRecurrenceGroup({
|
|
1757
|
-
storePath,
|
|
1758
|
-
reviewPath,
|
|
1759
|
-
retentionPath,
|
|
1760
|
-
backupDir,
|
|
1761
|
-
recurrenceKey: record.recurrenceKey,
|
|
1762
|
-
confirmationToken: preview.confirmationToken,
|
|
1763
|
-
}),
|
|
1764
|
-
/requires exact confirmation token/,
|
|
1765
|
-
);
|
|
1766
|
-
const reviewed = loadDiagnosticState({ storePath, reviewPath, retentionPath, backupDir });
|
|
1767
|
-
const token = buildRetentionPreview({
|
|
1768
|
-
recurrenceKey: record.recurrenceKey,
|
|
1769
|
-
records: reviewed.records,
|
|
1770
|
-
reviewEvents: reviewed.reviewEvents,
|
|
1771
|
-
storeHash: reviewed.ventsHash,
|
|
1772
|
-
reviewHash: reviewed.reviewEventsHash,
|
|
1773
|
-
curationHash: reviewed.curationEventsHash,
|
|
1774
|
-
}).confirmationToken;
|
|
1775
|
-
const archived = archiveRecurrenceGroup({
|
|
1776
|
-
storePath,
|
|
1777
|
-
reviewPath,
|
|
1778
|
-
retentionPath,
|
|
1779
|
-
backupDir,
|
|
1780
|
-
recurrenceKey: record.recurrenceKey,
|
|
1781
|
-
confirmationToken: token,
|
|
1782
|
-
});
|
|
1783
|
-
|
|
1784
|
-
assert.throws(
|
|
1785
|
-
() =>
|
|
1786
|
-
restoreRetentionBackup({
|
|
1787
|
-
storePath,
|
|
1788
|
-
retentionPath,
|
|
1789
|
-
backupDir,
|
|
1790
|
-
backupPath: path.join(dir, "outside.agent-vent-backup.json"),
|
|
1791
|
-
confirmationToken: archived.restoreConfirmationToken,
|
|
1792
|
-
}),
|
|
1793
|
-
/backup path must stay inside/,
|
|
1794
|
-
);
|
|
1795
|
-
appendVentRecord(
|
|
1796
|
-
storePath,
|
|
1797
|
-
createVentRecord({ summary: "New record after archive", category: "bug" }, { id: "new" }),
|
|
1798
|
-
);
|
|
1799
|
-
assert.throws(
|
|
1800
|
-
() =>
|
|
1801
|
-
restoreRetentionBackup({
|
|
1802
|
-
storePath,
|
|
1803
|
-
retentionPath,
|
|
1804
|
-
backupDir,
|
|
1805
|
-
backupPath: archived.backupPath,
|
|
1806
|
-
confirmationToken: archived.restoreConfirmationToken,
|
|
1807
|
-
}),
|
|
1808
|
-
/stale backup/,
|
|
1809
|
-
);
|
|
1810
|
-
});
|
|
1811
|
-
|
|
1812
|
-
test("retention archive rejects stale confirmation when active store changes", () => {
|
|
1813
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-retention-stale-token-"));
|
|
1814
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1815
|
-
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
1816
|
-
const retentionPath = path.join(dir, "retention-events.jsonl");
|
|
1817
|
-
const backupDir = path.join(dir, "backups");
|
|
1818
|
-
const record = createVentRecord(
|
|
1819
|
-
{ summary: "Archive target", category: "bug", recurrenceKey: "target" },
|
|
1820
|
-
{ id: "target" },
|
|
1821
|
-
);
|
|
1822
|
-
appendVentRecord(storePath, record);
|
|
1823
|
-
appendReviewEvent(
|
|
1824
|
-
reviewPath,
|
|
1825
|
-
createReviewEvent({ recurrenceKey: record.recurrenceKey, state: "acknowledged" }),
|
|
1826
|
-
);
|
|
1827
|
-
const state = loadDiagnosticState({ storePath, reviewPath, retentionPath, backupDir });
|
|
1828
|
-
const preview = buildRetentionPreview({
|
|
1829
|
-
recurrenceKey: record.recurrenceKey,
|
|
1830
|
-
records: state.records,
|
|
1831
|
-
reviewEvents: state.reviewEvents,
|
|
1832
|
-
storeHash: state.ventsHash,
|
|
1833
|
-
reviewHash: state.reviewEventsHash,
|
|
1834
|
-
curationHash: state.curationEventsHash,
|
|
1835
|
-
});
|
|
1836
|
-
appendVentRecord(
|
|
1837
|
-
storePath,
|
|
1838
|
-
createVentRecord({ summary: "Concurrent new record", category: "bug" }, { id: "new" }),
|
|
1839
|
-
);
|
|
1840
|
-
assert.throws(
|
|
1841
|
-
() =>
|
|
1842
|
-
archiveRecurrenceGroup({
|
|
1843
|
-
storePath,
|
|
1844
|
-
reviewPath,
|
|
1845
|
-
retentionPath,
|
|
1846
|
-
backupDir,
|
|
1847
|
-
recurrenceKey: record.recurrenceKey,
|
|
1848
|
-
confirmationToken: preview.confirmationToken,
|
|
1849
|
-
}),
|
|
1850
|
-
/requires exact confirmation token/,
|
|
1851
|
-
);
|
|
1852
|
-
assert.deepEqual(
|
|
1853
|
-
readVentRecords(storePath)
|
|
1854
|
-
.records.map((entry) => entry.id)
|
|
1855
|
-
.sort(),
|
|
1856
|
-
["new", "target"],
|
|
1857
|
-
);
|
|
1858
|
-
});
|
|
1859
|
-
|
|
1860
|
-
test("retention archive rolls active store back when receipt append fails", (context) => {
|
|
1861
|
-
if (process.getuid?.() === 0) {
|
|
1862
|
-
context.skip("root can bypass file write permissions");
|
|
1863
|
-
return;
|
|
1864
|
-
}
|
|
1865
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-retention-receipt-fail-"));
|
|
1866
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1867
|
-
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
1868
|
-
const retentionPath = path.join(dir, "retention-events.jsonl");
|
|
1869
|
-
const backupDir = path.join(dir, "backups");
|
|
1870
|
-
const record = createVentRecord(
|
|
1871
|
-
{ summary: "Receipt failure target", category: "bug", recurrenceKey: "receipt-fail" },
|
|
1872
|
-
{ id: "target" },
|
|
1873
|
-
);
|
|
1874
|
-
appendVentRecord(storePath, record);
|
|
1875
|
-
appendReviewEvent(
|
|
1876
|
-
reviewPath,
|
|
1877
|
-
createReviewEvent({ recurrenceKey: record.recurrenceKey, state: "dismissed" }),
|
|
1878
|
-
);
|
|
1879
|
-
fs.writeFileSync(retentionPath, "", "utf8");
|
|
1880
|
-
fs.chmodSync(retentionPath, 0o400);
|
|
1881
|
-
try {
|
|
1882
|
-
const state = loadDiagnosticState({ storePath, reviewPath, retentionPath, backupDir });
|
|
1883
|
-
const token = buildRetentionPreview({
|
|
1884
|
-
recurrenceKey: record.recurrenceKey,
|
|
1885
|
-
records: state.records,
|
|
1886
|
-
reviewEvents: state.reviewEvents,
|
|
1887
|
-
storeHash: state.ventsHash,
|
|
1888
|
-
reviewHash: state.reviewEventsHash,
|
|
1889
|
-
curationHash: state.curationEventsHash,
|
|
1890
|
-
}).confirmationToken;
|
|
1891
|
-
assert.throws(
|
|
1892
|
-
() =>
|
|
1893
|
-
archiveRecurrenceGroup({
|
|
1894
|
-
storePath,
|
|
1895
|
-
reviewPath,
|
|
1896
|
-
retentionPath,
|
|
1897
|
-
backupDir,
|
|
1898
|
-
recurrenceKey: record.recurrenceKey,
|
|
1899
|
-
confirmationToken: token,
|
|
1900
|
-
}),
|
|
1901
|
-
/EACCES|EPERM|permission/i,
|
|
1902
|
-
);
|
|
1903
|
-
assert.equal(readVentRecords(storePath).records.length, 1);
|
|
1904
|
-
assert.equal(fs.readdirSync(backupDir).length, 0);
|
|
1905
|
-
} finally {
|
|
1906
|
-
fs.chmodSync(retentionPath, 0o600);
|
|
1907
|
-
}
|
|
1908
|
-
});
|
|
1909
|
-
|
|
1910
|
-
test("retention restore rejects symlinked backup directories and tampered restore tokens", () => {
|
|
1911
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-retention-symlink-backup-"));
|
|
1912
|
-
const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-retention-external-"));
|
|
1913
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1914
|
-
const retentionPath = path.join(dir, "retention-events.jsonl");
|
|
1915
|
-
const backupDir = path.join(dir, "backups");
|
|
1916
|
-
const current = createVentRecord({ summary: "Current", category: "bug" }, { id: "current" });
|
|
1917
|
-
const injectedJsonl = `${JSON.stringify(createVentRecord({ summary: "Injected", category: "bug" }, { id: "injected" }))}\n`;
|
|
1918
|
-
appendVentRecord(storePath, current);
|
|
1919
|
-
const currentText = fs.readFileSync(storePath, "utf8");
|
|
1920
|
-
const hash = (value) => createHash("sha256").update(value, "utf8").digest("hex");
|
|
1921
|
-
const backup = {
|
|
1922
|
-
schemaVersion: 1,
|
|
1923
|
-
artifactType: "agent_vent_retention_backup",
|
|
1924
|
-
recurrenceKey: "bug:crafted",
|
|
1925
|
-
beforeHash: hash(injectedJsonl),
|
|
1926
|
-
afterHash: hash(currentText),
|
|
1927
|
-
restoreConfirmationToken: "restore:not-derived",
|
|
1928
|
-
ventsJsonl: injectedJsonl,
|
|
1929
|
-
};
|
|
1930
|
-
fs.writeFileSync(
|
|
1931
|
-
path.join(externalDir, "crafted.agent-vent-backup.json"),
|
|
1932
|
-
JSON.stringify(backup),
|
|
1933
|
-
);
|
|
1934
|
-
fs.symlinkSync(externalDir, backupDir);
|
|
1935
|
-
assert.throws(
|
|
1936
|
-
() =>
|
|
1937
|
-
restoreRetentionBackup({
|
|
1938
|
-
storePath,
|
|
1939
|
-
retentionPath,
|
|
1940
|
-
backupDir,
|
|
1941
|
-
backupPath: path.join(backupDir, "crafted.agent-vent-backup.json"),
|
|
1942
|
-
confirmationToken: "restore:not-derived",
|
|
1943
|
-
}),
|
|
1944
|
-
/backup directory must be a real directory/,
|
|
1945
|
-
);
|
|
1946
|
-
fs.rmSync(backupDir);
|
|
1947
|
-
fs.mkdirSync(backupDir);
|
|
1948
|
-
const localBackupPath = path.join(backupDir, "crafted.agent-vent-backup.json");
|
|
1949
|
-
fs.writeFileSync(localBackupPath, JSON.stringify(backup));
|
|
1950
|
-
assert.throws(
|
|
1951
|
-
() =>
|
|
1952
|
-
restoreRetentionBackup({
|
|
1953
|
-
storePath,
|
|
1954
|
-
retentionPath,
|
|
1955
|
-
backupDir,
|
|
1956
|
-
backupPath: localBackupPath,
|
|
1957
|
-
confirmationToken: "restore:not-derived",
|
|
1958
|
-
}),
|
|
1959
|
-
/restore token failed integrity check/,
|
|
1960
|
-
);
|
|
1961
|
-
});
|
|
1962
|
-
|
|
1963
|
-
test("retention restore fails closed when current store is missing", () => {
|
|
1964
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-retention-missing-"));
|
|
1965
|
-
const storePath = path.join(dir, "vents.jsonl");
|
|
1966
|
-
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
1967
|
-
const retentionPath = path.join(dir, "retention-events.jsonl");
|
|
1968
|
-
const backupDir = path.join(dir, "backups");
|
|
1969
|
-
const record = createVentRecord(
|
|
1970
|
-
{
|
|
1971
|
-
summary: "Archive then remove active store",
|
|
1972
|
-
category: "bug",
|
|
1973
|
-
recurrenceKey: "missing-store",
|
|
1974
|
-
},
|
|
1975
|
-
{ id: "missing" },
|
|
1976
|
-
);
|
|
1977
|
-
appendVentRecord(storePath, record);
|
|
1978
|
-
appendReviewEvent(
|
|
1979
|
-
reviewPath,
|
|
1980
|
-
createReviewEvent({ recurrenceKey: record.recurrenceKey, state: "dismissed" }),
|
|
1981
|
-
);
|
|
1982
|
-
const state = loadDiagnosticState({ storePath, reviewPath, retentionPath, backupDir });
|
|
1983
|
-
const token = buildRetentionPreview({
|
|
1984
|
-
recurrenceKey: record.recurrenceKey,
|
|
1985
|
-
records: state.records,
|
|
1986
|
-
reviewEvents: state.reviewEvents,
|
|
1987
|
-
storeHash: state.ventsHash,
|
|
1988
|
-
reviewHash: state.reviewEventsHash,
|
|
1989
|
-
curationHash: state.curationEventsHash,
|
|
1990
|
-
}).confirmationToken;
|
|
1991
|
-
const archived = archiveRecurrenceGroup({
|
|
1992
|
-
storePath,
|
|
1993
|
-
reviewPath,
|
|
1994
|
-
retentionPath,
|
|
1995
|
-
backupDir,
|
|
1996
|
-
recurrenceKey: record.recurrenceKey,
|
|
1997
|
-
confirmationToken: token,
|
|
1998
|
-
});
|
|
1999
|
-
fs.rmSync(storePath);
|
|
2000
|
-
assert.throws(
|
|
2001
|
-
() =>
|
|
2002
|
-
restoreRetentionBackup({
|
|
2003
|
-
storePath,
|
|
2004
|
-
retentionPath,
|
|
2005
|
-
backupDir,
|
|
2006
|
-
backupPath: archived.backupPath,
|
|
2007
|
-
confirmationToken: archived.restoreConfirmationToken,
|
|
2008
|
-
}),
|
|
2009
|
-
/requires current vents store/,
|
|
2010
|
-
);
|
|
2011
|
-
});
|
|
2012
|
-
|
|
2013
|
-
test("record requires non-empty summary", () => {
|
|
2014
|
-
assert.throws(() => createVentRecord({ summary: " " }), /requires a non-empty summary/);
|
|
2015
|
-
});
|