@tryinget/pi-agent-vent 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- 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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
summary: "Vent store lifecycle snapshots, stats, and exports; split from vent-store.test.js."
|
|
3
|
+
read_when:
|
|
4
|
+
- "You change lifecycle snapshots, stats, and exports behavior."
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import {
|
|
12
|
+
appendReviewEvent,
|
|
13
|
+
appendVentRecord,
|
|
14
|
+
buildLifecycleSnapshot,
|
|
15
|
+
createReviewEvent,
|
|
16
|
+
createVentRecord,
|
|
17
|
+
formatExportJson,
|
|
18
|
+
formatExportMarkdown,
|
|
19
|
+
formatLifecycleStats,
|
|
20
|
+
readReviewEvents,
|
|
21
|
+
readVentRecords,
|
|
22
|
+
} from "../src/vent-store.js";
|
|
23
|
+
|
|
24
|
+
test("lifecycle stats and exports are bounded local projections", () => {
|
|
25
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-lifecycle-"));
|
|
26
|
+
const storePath = path.join(dir, "vents.jsonl");
|
|
27
|
+
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
28
|
+
const record = createVentRecord(
|
|
29
|
+
{ summary: "Lifecycle export captures recurrence", category: "documentation" },
|
|
30
|
+
{ id: "v1" },
|
|
31
|
+
);
|
|
32
|
+
const review = createReviewEvent({
|
|
33
|
+
recurrenceKey: record.recurrenceKey,
|
|
34
|
+
state: "escalation_drafted",
|
|
35
|
+
note: "Draft locally only",
|
|
36
|
+
});
|
|
37
|
+
appendVentRecord(storePath, record);
|
|
38
|
+
appendReviewEvent(reviewPath, review);
|
|
39
|
+
|
|
40
|
+
const snapshot = buildLifecycleSnapshot({
|
|
41
|
+
records: readVentRecords(storePath).records,
|
|
42
|
+
reviewEvents: readReviewEvents(reviewPath).events,
|
|
43
|
+
storePath,
|
|
44
|
+
reviewPath,
|
|
45
|
+
now: "2026-05-21T00:00:00.000Z",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
assert.equal(snapshot.counts.vents, 1);
|
|
49
|
+
assert.equal(snapshot.counts.reviewStates.escalation_drafted, 1);
|
|
50
|
+
assert.equal(snapshot.files.vents.exists, true);
|
|
51
|
+
assert.match(formatLifecycleStats(snapshot), /no AK task, GitHub issue, incident, evidence/);
|
|
52
|
+
const markdown = formatExportMarkdown(snapshot);
|
|
53
|
+
assert.match(markdown, /# Agent vent local diagnostic export/);
|
|
54
|
+
assert.match(markdown, /Decision posture: draft_recorded_owner_external/);
|
|
55
|
+
assert.match(
|
|
56
|
+
markdown,
|
|
57
|
+
/not resolution, assignment, evidence, publication, issue status, task truth, or incident state/,
|
|
58
|
+
);
|
|
59
|
+
assert.match(markdown, /## Safe local follow-up/);
|
|
60
|
+
assert.match(markdown, /Optional local retention preview: \/agent_vent retention preview/);
|
|
61
|
+
assert.match(markdown, /Draft-only handoff:/);
|
|
62
|
+
assert.match(
|
|
63
|
+
markdown,
|
|
64
|
+
/They do not file, create, declare, assign, record evidence, publish, archive, restore/,
|
|
65
|
+
);
|
|
66
|
+
const json = JSON.parse(formatExportJson(snapshot));
|
|
67
|
+
assert.equal(json.counts.vents, 1);
|
|
68
|
+
assert.equal(json.nextActions[0].decisionPosture.state, "draft_recorded_owner_external");
|
|
69
|
+
assert.equal(
|
|
70
|
+
json.nextActions[0].retentionPreviewCommand,
|
|
71
|
+
`/agent_vent retention preview ${record.recurrenceKey}`,
|
|
72
|
+
);
|
|
73
|
+
assert.doesNotMatch(
|
|
74
|
+
JSON.stringify(json.nextActions),
|
|
75
|
+
/archive:[a-f0-9]|restore:[a-f0-9]|owner was assigned/,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("lifecycle export can preserve facet scope without owner-routing claims", () => {
|
|
80
|
+
const keep = createVentRecord({
|
|
81
|
+
summary: "Keep export scoped token=abc123",
|
|
82
|
+
category: "tool_failure",
|
|
83
|
+
recurrenceKey: "export-keep",
|
|
84
|
+
tags: ["Export"],
|
|
85
|
+
tool: "pi reload",
|
|
86
|
+
packageName: "@tryinget/pi-agent-vent",
|
|
87
|
+
});
|
|
88
|
+
const omit = createVentRecord({
|
|
89
|
+
summary: "Omit unrelated export group",
|
|
90
|
+
category: "documentation",
|
|
91
|
+
recurrenceKey: "export-omit",
|
|
92
|
+
tags: ["Other"],
|
|
93
|
+
});
|
|
94
|
+
const mixedOut = createVentRecord({
|
|
95
|
+
summary: "Omit mixed-group private payload",
|
|
96
|
+
category: "tool_failure",
|
|
97
|
+
recurrenceKey: "export-keep",
|
|
98
|
+
tags: ["Other"],
|
|
99
|
+
tool: "pi reload",
|
|
100
|
+
packageName: "@tryinget/pi-agent-vent",
|
|
101
|
+
});
|
|
102
|
+
const review = createReviewEvent({
|
|
103
|
+
recurrenceKey: keep.recurrenceKey,
|
|
104
|
+
state: "acknowledged",
|
|
105
|
+
note: "review token=abc123 locally",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const snapshot = buildLifecycleSnapshot({
|
|
109
|
+
records: [keep, omit, mixedOut],
|
|
110
|
+
reviewEvents: [review],
|
|
111
|
+
filters: { tags: ["export"], tool: "pi reload", packageName: "@tryinget/pi-agent-vent" },
|
|
112
|
+
state: "acknowledged",
|
|
113
|
+
limit: 10,
|
|
114
|
+
now: "2026-05-22T00:00:00.000Z",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert.equal(snapshot.scope.hasFilters, true);
|
|
118
|
+
assert.equal(snapshot.scope.matchingGroups, 1);
|
|
119
|
+
assert.equal(snapshot.counts.vents, 1);
|
|
120
|
+
assert.equal(snapshot.counts.reviewStates.acknowledged, 1);
|
|
121
|
+
assert.equal(snapshot.reviewQueue.items.length, 1);
|
|
122
|
+
assert.equal(snapshot.reviewQueue.items[0].recurrenceKey, keep.recurrenceKey);
|
|
123
|
+
assert.equal(snapshot.summary.groups[0].recurrenceKey, keep.recurrenceKey);
|
|
124
|
+
const markdown = formatExportMarkdown(snapshot);
|
|
125
|
+
assert.match(markdown, /Filters: tool=pi-reload; package=tryinget-pi-agent-vent; tags=export/);
|
|
126
|
+
assert.match(markdown, /not owner routing or owner assignment/);
|
|
127
|
+
assert.match(markdown, /token=\[REDACTED\]/);
|
|
128
|
+
assert.match(markdown, /Safe local follow-up/);
|
|
129
|
+
assert.match(
|
|
130
|
+
markdown,
|
|
131
|
+
/Optional local retention preview: \/agent_vent retention preview tool_failure:export-keep/,
|
|
132
|
+
);
|
|
133
|
+
assert.doesNotMatch(
|
|
134
|
+
markdown,
|
|
135
|
+
/Omit unrelated export group|Omit mixed-group private payload|owner was assigned|archive:[a-f0-9]/,
|
|
136
|
+
);
|
|
137
|
+
const json = JSON.parse(formatExportJson(snapshot));
|
|
138
|
+
assert.equal(json.scope.filters.tool, "pi-reload");
|
|
139
|
+
assert.equal(json.counts.vents, 1);
|
|
140
|
+
assert.equal(
|
|
141
|
+
json.nextActions[0].exportBucketCommand,
|
|
142
|
+
"/agent_vent export markdown acknowledged tool=pi-reload package=tryinget-pi-agent-vent tag=export",
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("lifecycle export state scope constrains counts and summaries", () => {
|
|
147
|
+
const acknowledged = createVentRecord({
|
|
148
|
+
summary: "Acknowledged export group",
|
|
149
|
+
category: "bug",
|
|
150
|
+
recurrenceKey: "ack-export",
|
|
151
|
+
tags: ["export-state"],
|
|
152
|
+
});
|
|
153
|
+
const fresh = createVentRecord({
|
|
154
|
+
summary: "Fresh export group should not appear",
|
|
155
|
+
category: "bug",
|
|
156
|
+
recurrenceKey: "fresh-export",
|
|
157
|
+
tags: ["export-state"],
|
|
158
|
+
});
|
|
159
|
+
const review = createReviewEvent({
|
|
160
|
+
recurrenceKey: acknowledged.recurrenceKey,
|
|
161
|
+
state: "acknowledged",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const snapshot = buildLifecycleSnapshot({
|
|
165
|
+
records: [acknowledged, fresh],
|
|
166
|
+
reviewEvents: [review],
|
|
167
|
+
filters: { tags: ["export-state"] },
|
|
168
|
+
state: "acknowledged",
|
|
169
|
+
limit: 10,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
assert.equal(snapshot.scope.stateFilter, "acknowledged");
|
|
173
|
+
assert.equal(snapshot.scope.facetMatchingGroups, 2);
|
|
174
|
+
assert.equal(snapshot.scope.matchingGroups, 1);
|
|
175
|
+
assert.equal(snapshot.counts.vents, 1);
|
|
176
|
+
assert.equal(snapshot.counts.reviewStates.new, 0);
|
|
177
|
+
assert.equal(snapshot.counts.reviewStates.acknowledged, 1);
|
|
178
|
+
assert.equal(snapshot.summary.groups.length, 1);
|
|
179
|
+
assert.equal(snapshot.reviewQueue.items.length, 1);
|
|
180
|
+
const markdown = formatExportMarkdown(snapshot);
|
|
181
|
+
assert.match(markdown, /Acknowledged export group/);
|
|
182
|
+
assert.doesNotMatch(markdown, /Fresh export group should not appear/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("lifecycle export follow-up commands quote legacy keys without authority claims", () => {
|
|
186
|
+
const legacy = {
|
|
187
|
+
...createVentRecord({ summary: "Legacy export key", category: "bug" }),
|
|
188
|
+
recurrenceKey: 'bug:legacy key "quoted"',
|
|
189
|
+
};
|
|
190
|
+
const snapshot = buildLifecycleSnapshot({ records: [legacy], reviewEvents: [], limit: 5 });
|
|
191
|
+
const markdown = formatExportMarkdown(snapshot);
|
|
192
|
+
const json = JSON.parse(formatExportJson(snapshot));
|
|
193
|
+
|
|
194
|
+
assert.match(markdown, /\/agent_vent review show "bug:legacy key \\"quoted\\""/);
|
|
195
|
+
assert.match(markdown, /Choose local review state:/);
|
|
196
|
+
assert.doesNotMatch(markdown, /retention preview "bug:legacy key/);
|
|
197
|
+
assert.match(markdown, /draft commands only generate local text|local diagnostics\/drafts only/);
|
|
198
|
+
assert.equal(json.nextActions[0].reviewCommands.length, 3);
|
|
199
|
+
assert.equal(json.nextActions[0].retentionPreviewCommand, undefined);
|
|
200
|
+
assert.doesNotMatch(
|
|
201
|
+
JSON.stringify(json.nextActions),
|
|
202
|
+
/archive:[a-f0-9]|restore:[a-f0-9]|filed|owner was assigned/,
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("lifecycle export rejects empty facet filters", () => {
|
|
207
|
+
assert.throws(
|
|
208
|
+
() => buildLifecycleSnapshot({ records: [], filters: { tags: [""] } }),
|
|
209
|
+
/invalid agent_vent review filter tag: empty value/,
|
|
210
|
+
);
|
|
211
|
+
assert.throws(
|
|
212
|
+
() => buildLifecycleSnapshot({ records: [], filters: { tool: "" } }),
|
|
213
|
+
/invalid agent_vent review filter tool: empty value/,
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("lifecycle stats report missing stores without creating files", () => {
|
|
218
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-lifecycle-missing-"));
|
|
219
|
+
const storePath = path.join(dir, "vents.jsonl");
|
|
220
|
+
const reviewPath = path.join(dir, "review-events.jsonl");
|
|
221
|
+
const snapshot = buildLifecycleSnapshot({ storePath, reviewPath });
|
|
222
|
+
|
|
223
|
+
assert.equal(snapshot.files.vents.exists, false);
|
|
224
|
+
assert.equal(snapshot.files.reviewEvents.sizeBytes, 0);
|
|
225
|
+
assert.equal(fs.existsSync(storePath), false);
|
|
226
|
+
assert.match(formatLifecycleStats(snapshot), /vents: 0 record/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("lifecycle stats count more groups than the display limit", () => {
|
|
230
|
+
const records = Array.from({ length: 125 }, (_, index) =>
|
|
231
|
+
createVentRecord(
|
|
232
|
+
{
|
|
233
|
+
summary: `Distinct lifecycle group ${index}`,
|
|
234
|
+
category: "workflow",
|
|
235
|
+
recurrenceKey: `distinct-${index}`,
|
|
236
|
+
},
|
|
237
|
+
{ id: `v-${index}` },
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
const snapshot = buildLifecycleSnapshot({ records, limit: 5 });
|
|
241
|
+
|
|
242
|
+
assert.equal(snapshot.counts.recurrenceGroups, 125);
|
|
243
|
+
assert.equal(snapshot.summary.groups.length, 5);
|
|
244
|
+
assert.equal(snapshot.reviewQueue.items.length, 5);
|
|
245
|
+
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
summary: "Vent store record creation, redaction, JSONL append/read, review queue, and store safety; split from vent-store.test.js."
|
|
3
|
+
read_when:
|
|
4
|
+
- "You change record creation, redaction, JSONL append/read, review queue, and store safety behavior."
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import {
|
|
12
|
+
appendReviewEvent,
|
|
13
|
+
appendVentRecord,
|
|
14
|
+
buildRecurrenceKey,
|
|
15
|
+
createReviewEvent,
|
|
16
|
+
createVentRecord,
|
|
17
|
+
defaultBackupDir,
|
|
18
|
+
defaultCurationPath,
|
|
19
|
+
defaultRetentionPath,
|
|
20
|
+
defaultReviewPath,
|
|
21
|
+
defaultStorePath,
|
|
22
|
+
formatReviewQueue,
|
|
23
|
+
formatSummary,
|
|
24
|
+
readReviewEvents,
|
|
25
|
+
readVentRecords,
|
|
26
|
+
redactSensitiveText,
|
|
27
|
+
summarizeRecords,
|
|
28
|
+
summarizeReviewQueue,
|
|
29
|
+
} from "../src/vent-store.js";
|
|
30
|
+
|
|
31
|
+
test("default store and lifecycle paths honor PI_AGENT_VENT_DIR", () => {
|
|
32
|
+
assert.equal(
|
|
33
|
+
defaultStorePath({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
34
|
+
"/tmp/private-vents/vents.jsonl",
|
|
35
|
+
);
|
|
36
|
+
assert.equal(
|
|
37
|
+
defaultReviewPath({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
38
|
+
"/tmp/private-vents/review-events.jsonl",
|
|
39
|
+
);
|
|
40
|
+
assert.equal(
|
|
41
|
+
defaultCurationPath({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
42
|
+
"/tmp/private-vents/curation-events.jsonl",
|
|
43
|
+
);
|
|
44
|
+
assert.equal(
|
|
45
|
+
defaultRetentionPath({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
46
|
+
"/tmp/private-vents/retention-events.jsonl",
|
|
47
|
+
);
|
|
48
|
+
assert.equal(
|
|
49
|
+
defaultBackupDir({ PI_AGENT_VENT_DIR: "/tmp/private-vents" }),
|
|
50
|
+
"/tmp/private-vents/backups",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("createVentRecord minimizes, redacts, and derives recurrence key", () => {
|
|
55
|
+
const record = createVentRecord(
|
|
56
|
+
{
|
|
57
|
+
category: "tool-failure",
|
|
58
|
+
severity: "high",
|
|
59
|
+
summary: "Bash retry keeps failing with token=abc123",
|
|
60
|
+
frustration: "Same brittle flow; Authorization: Bearer abcdefghijklmnop should not leak.",
|
|
61
|
+
tags: ["Tool Failure", "runtime"],
|
|
62
|
+
tool: "bash token=abc123",
|
|
63
|
+
packageName: "@tryinget/pi-agent-vent secret=hidden",
|
|
64
|
+
},
|
|
65
|
+
{ id: "vent-1", now: "2026-05-21T00:00:00.000Z", cwd: "/repo", sessionFile: "session.jsonl" },
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
assert.equal(record.schemaVersion, 1);
|
|
69
|
+
assert.equal(record.category, "tool_failure");
|
|
70
|
+
assert.equal(record.severity, "high");
|
|
71
|
+
assert.equal(record.recurrenceKey, "tool_failure:bash-retry-keeps-failing-with-token-redacted");
|
|
72
|
+
assert.equal(record.tool, "bash-token-redacted");
|
|
73
|
+
assert.equal(record.packageName, "tryinget-pi-agent-vent-secret-redacted");
|
|
74
|
+
assert.equal(record.privacy.redacted, true);
|
|
75
|
+
assert.match(record.summary, /token=\[REDACTED\]/);
|
|
76
|
+
assert.match(record.frustration, /Bearer \[REDACTED\]/);
|
|
77
|
+
assert.deepEqual(record.tags, ["tool-failure", "runtime"]);
|
|
78
|
+
|
|
79
|
+
const tagOnlySecret = createVentRecord({
|
|
80
|
+
summary: "Tag-only secret must update privacy metadata",
|
|
81
|
+
tags: ["token=abc123"],
|
|
82
|
+
});
|
|
83
|
+
assert.deepEqual(tagOnlySecret.tags, ["token-redacted"]);
|
|
84
|
+
assert.equal(tagOnlySecret.privacy.redacted, true);
|
|
85
|
+
assert.deepEqual(tagOnlySecret.privacy.redactionPatterns, ["assigned_secret"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("category aliases normalize at record and review-filter boundaries", () => {
|
|
89
|
+
const workflow = createVentRecord({
|
|
90
|
+
summary: "Workflow alias should not degrade",
|
|
91
|
+
category: "workflow_friction",
|
|
92
|
+
recurrenceKey: "workflow alias",
|
|
93
|
+
});
|
|
94
|
+
const missing = createVentRecord({
|
|
95
|
+
summary: "Missing affordance alias should preserve meaning",
|
|
96
|
+
category: "missing_affordance",
|
|
97
|
+
});
|
|
98
|
+
const alignment = createVentRecord({
|
|
99
|
+
summary: "Context alignment candidate should remain callable",
|
|
100
|
+
category: "context_alignment",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
assert.equal(workflow.category, "workflow");
|
|
104
|
+
assert.equal(workflow.recurrenceKey, "workflow:workflow-alias");
|
|
105
|
+
assert.equal(missing.category, "missing_capability");
|
|
106
|
+
assert.match(missing.recurrenceKey, /^missing_capability:/);
|
|
107
|
+
assert.equal(alignment.category, "other");
|
|
108
|
+
assert.match(alignment.recurrenceKey, /^other:/);
|
|
109
|
+
|
|
110
|
+
const queue = summarizeReviewQueue([workflow, missing, alignment], [], {
|
|
111
|
+
filters: { category: "workflow_friction" },
|
|
112
|
+
});
|
|
113
|
+
assert.equal(queue.filters.category, "workflow");
|
|
114
|
+
assert.equal(queue.matchingGroupCount, 1);
|
|
115
|
+
assert.equal(queue.items[0].recurrenceKey, workflow.recurrenceKey);
|
|
116
|
+
|
|
117
|
+
const alignmentQueue = summarizeReviewQueue([workflow, missing, alignment], [], {
|
|
118
|
+
filters: { category: "context_alignment" },
|
|
119
|
+
});
|
|
120
|
+
assert.equal(alignmentQueue.filters.category, "other");
|
|
121
|
+
assert.equal(alignmentQueue.matchingGroupCount, 1);
|
|
122
|
+
assert.equal(alignmentQueue.items[0].recurrenceKey, alignment.recurrenceKey);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("explicit recurrence keys are redacted before slugging", () => {
|
|
126
|
+
const record = createVentRecord({
|
|
127
|
+
summary: "Explicit key should not leak",
|
|
128
|
+
category: "bug",
|
|
129
|
+
recurrenceKey: "reload token=abc123",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.equal(record.recurrenceKey, "bug:reload-token-redacted");
|
|
133
|
+
assert.doesNotMatch(record.recurrenceKey, /abc123/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("redactSensitiveText catches common secret shapes", () => {
|
|
137
|
+
const result = redactSensitiveText(
|
|
138
|
+
"api_key=secret sk-abcdefghijklmnopqrstuvwxyz ghp_abcdefghijklmnopqrstuvwxyz",
|
|
139
|
+
);
|
|
140
|
+
assert.equal(result.redacted, true);
|
|
141
|
+
assert.match(result.text, /api_key=\[REDACTED\]/);
|
|
142
|
+
assert.match(result.text, /sk-\[REDACTED\]/);
|
|
143
|
+
assert.match(result.text, /gh\*_REDACTED/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("append/read JSONL tolerates malformed old lines", () => {
|
|
147
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-"));
|
|
148
|
+
const filePath = path.join(dir, "vents.jsonl");
|
|
149
|
+
fs.writeFileSync(filePath, "{not-json}\n", "utf8");
|
|
150
|
+
const record = createVentRecord(
|
|
151
|
+
{ summary: "Repeated docs mismatch", category: "documentation" },
|
|
152
|
+
{ id: "v1" },
|
|
153
|
+
);
|
|
154
|
+
appendVentRecord(filePath, record);
|
|
155
|
+
|
|
156
|
+
const result = readVentRecords(filePath);
|
|
157
|
+
assert.equal(result.malformedLines, 1);
|
|
158
|
+
assert.equal(result.records.length, 1);
|
|
159
|
+
assert.equal(result.records[0].id, "v1");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("summary groups recurrence candidates without declaring incidents", () => {
|
|
163
|
+
const records = [
|
|
164
|
+
createVentRecord(
|
|
165
|
+
{
|
|
166
|
+
summary: "Reload drops registered tool",
|
|
167
|
+
category: "bug",
|
|
168
|
+
severity: "medium",
|
|
169
|
+
recurrenceKey: "reload-tools",
|
|
170
|
+
},
|
|
171
|
+
{ id: "a", now: "2026-05-21T00:00:00.000Z" },
|
|
172
|
+
),
|
|
173
|
+
createVentRecord(
|
|
174
|
+
{
|
|
175
|
+
summary: "Reload drops registered tool again",
|
|
176
|
+
category: "bug",
|
|
177
|
+
severity: "high",
|
|
178
|
+
recurrenceKey: "reload-tools",
|
|
179
|
+
},
|
|
180
|
+
{ id: "b", now: "2026-05-21T00:01:00.000Z" },
|
|
181
|
+
),
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
assert.equal(
|
|
185
|
+
buildRecurrenceKey({ category: "bug", recurrenceKey: "reload-tools" }),
|
|
186
|
+
"bug:reload-tools",
|
|
187
|
+
);
|
|
188
|
+
const summary = summarizeRecords(records);
|
|
189
|
+
assert.equal(summary.totalRecords, 2);
|
|
190
|
+
assert.equal(summary.candidateIncidentCount, 1);
|
|
191
|
+
assert.equal(summary.groups[0].candidateIncident, true);
|
|
192
|
+
assert.match(formatSummary(summary), /candidate incident/);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("review events are append-only local state projections", () => {
|
|
196
|
+
const records = [
|
|
197
|
+
createVentRecord(
|
|
198
|
+
{
|
|
199
|
+
summary: "Reload drops registered tool",
|
|
200
|
+
category: "bug",
|
|
201
|
+
severity: "high",
|
|
202
|
+
recurrenceKey: "reload-tools",
|
|
203
|
+
},
|
|
204
|
+
{ id: "a", now: "2026-05-21T00:00:00.000Z" },
|
|
205
|
+
),
|
|
206
|
+
];
|
|
207
|
+
const acknowledged = createReviewEvent(
|
|
208
|
+
{
|
|
209
|
+
recurrenceKey: "bug:reload-tools",
|
|
210
|
+
state: "acknowledged",
|
|
211
|
+
note: "Operator saw token=abc123 and will inspect later",
|
|
212
|
+
},
|
|
213
|
+
{ id: "r1", now: "2026-05-21T00:02:00.000Z" },
|
|
214
|
+
);
|
|
215
|
+
const reset = createReviewEvent(
|
|
216
|
+
{ recurrenceKey: "bug:reload-tools", state: "new" },
|
|
217
|
+
{ id: "r2", now: "2026-05-21T00:03:00.000Z" },
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
assert.equal(acknowledged.privacy.redacted, true);
|
|
221
|
+
assert.match(acknowledged.note, /token=\[REDACTED\]/);
|
|
222
|
+
|
|
223
|
+
const queue = summarizeReviewQueue(records, [acknowledged, reset]);
|
|
224
|
+
assert.equal(queue.queueCount, 1);
|
|
225
|
+
assert.equal(queue.items[0].reviewState, "new");
|
|
226
|
+
assert.match(formatReviewQueue(queue), /local diagnostic state only/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("review events fail closed on invalid state and clamp huge notes", () => {
|
|
230
|
+
assert.throws(
|
|
231
|
+
() => createReviewEvent({ recurrenceKey: "bug:bad-state", state: "resolved" }),
|
|
232
|
+
/invalid agent_vent review state/,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const event = createReviewEvent({
|
|
236
|
+
recurrenceKey: "bug:huge-note",
|
|
237
|
+
state: "acknowledged",
|
|
238
|
+
note: "x".repeat(5000),
|
|
239
|
+
});
|
|
240
|
+
assert.equal(event.note.length, 1200);
|
|
241
|
+
assert.match(event.note, /…$/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("dismissed recurrence groups leave the default active review queue", () => {
|
|
245
|
+
const records = [
|
|
246
|
+
createVentRecord(
|
|
247
|
+
{ summary: "Noisy one-off", category: "workflow", recurrenceKey: "noisy-one-off" },
|
|
248
|
+
{ id: "v1" },
|
|
249
|
+
),
|
|
250
|
+
];
|
|
251
|
+
const event = createReviewEvent({ recurrenceKey: "workflow:noisy-one-off", state: "dismissed" });
|
|
252
|
+
|
|
253
|
+
assert.equal(summarizeReviewQueue(records, [event]).queueCount, 0);
|
|
254
|
+
assert.equal(summarizeReviewQueue(records, [event], { state: "dismissed" }).queueCount, 1);
|
|
255
|
+
assert.equal(summarizeReviewQueue(records, [event], { state: "all" }).queueCount, 1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("review JSONL tolerates malformed lines and records latest state", () => {
|
|
259
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-review-"));
|
|
260
|
+
const filePath = path.join(dir, "review-events.jsonl");
|
|
261
|
+
fs.writeFileSync(filePath, "{not-json}\n", "utf8");
|
|
262
|
+
appendReviewEvent(
|
|
263
|
+
filePath,
|
|
264
|
+
createReviewEvent({ recurrenceKey: "bug:reload-tools", state: "acknowledged" }, { id: "r1" }),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const result = readReviewEvents(filePath);
|
|
268
|
+
assert.equal(result.malformedLines, 1);
|
|
269
|
+
assert.equal(result.events.length, 1);
|
|
270
|
+
assert.equal(result.events[0].state, "acknowledged");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("JSONL store files fail closed when replaced by symlinks", () => {
|
|
274
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-symlink-"));
|
|
275
|
+
const target = path.join(dir, "target.jsonl");
|
|
276
|
+
const link = path.join(dir, "vents.jsonl");
|
|
277
|
+
fs.writeFileSync(target, "", "utf8");
|
|
278
|
+
fs.symlinkSync(target, link);
|
|
279
|
+
|
|
280
|
+
assert.throws(
|
|
281
|
+
() => appendVentRecord(link, createVentRecord({ summary: "Symlink escape", category: "bug" })),
|
|
282
|
+
/must not be a symlink/,
|
|
283
|
+
);
|
|
284
|
+
assert.throws(() => readVentRecords(link), /must not be a symlink|ELOOP/);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("JSONL write permission failures propagate instead of pretending success", (context) => {
|
|
288
|
+
if (process.getuid?.() === 0) {
|
|
289
|
+
context.skip("root can bypass directory write permissions");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-permission-"));
|
|
293
|
+
const filePath = path.join(dir, "vents.jsonl");
|
|
294
|
+
fs.chmodSync(dir, 0o500);
|
|
295
|
+
try {
|
|
296
|
+
assert.throws(
|
|
297
|
+
() => appendVentRecord(filePath, createVentRecord({ summary: "Permission failure" })),
|
|
298
|
+
/EACCES|EPERM/,
|
|
299
|
+
);
|
|
300
|
+
} finally {
|
|
301
|
+
fs.chmodSync(dir, 0o700);
|
|
302
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
303
|
+
}
|
|
304
|
+
});
|