@tryinget/pi-agent-vent 0.1.2 → 0.2.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.
@@ -1,17 +1,25 @@
1
+ // ---
2
+ // summary: tests agent_vent tool registration, commands, actions, and authority-safe output
3
+ // read_when:
4
+ // - modifying the extension schema, command surface, lifecycle actions, or smoke contracts
5
+ // ---
6
+
1
7
  import assert from "node:assert/strict";
2
8
  import fs from "node:fs";
3
9
  import os from "node:os";
4
10
  import path from "node:path";
5
11
  import test from "node:test";
6
- import agentVentExtension from "../extensions/agent-vent.ts";
12
+ import agentVentExtension, { _test } from "../extensions/agent-vent.ts";
7
13
  import { createCurationEvent, createVentRecord, readReviewEvents } from "../src/vent-store.js";
8
14
 
9
15
  function createMockPi() {
10
16
  const tools = new Map();
11
17
  const commands = new Map();
18
+ const messages = [];
12
19
  return {
13
20
  tools,
14
21
  commands,
22
+ messages,
15
23
  api: {
16
24
  registerTool(tool) {
17
25
  tools.set(tool.name, tool);
@@ -19,6 +27,9 @@ function createMockPi() {
19
27
  registerCommand(name, command) {
20
28
  commands.set(name, command);
21
29
  },
30
+ sendMessage(message) {
31
+ messages.push(message);
32
+ },
22
33
  },
23
34
  };
24
35
  }
@@ -39,6 +50,9 @@ test("agent_vent tool schema stays aligned with retention candidate and compare
39
50
  const schemaText = JSON.stringify(pi.tools.get("agent_vent").parameters);
40
51
 
41
52
  assert.match(schemaText, /"compare"/);
53
+ assert.match(schemaText, /"preview"/);
54
+ assert.match(schemaText, /"workflow_friction"/);
55
+ assert.match(schemaText, /"context_alignment"/);
42
56
  assert.match(schemaText, /"candidates"/);
43
57
  assert.match(schemaText, /"history"/);
44
58
  assert.match(schemaText, /retentionCandidateState/);
@@ -48,6 +62,41 @@ test("agent_vent tool schema stays aligned with retention candidate and compare
48
62
  assert.match(schemaText, /outcomes, compare, export, or retention planning/);
49
63
  });
50
64
 
65
+ test("agent_vent keeps long command output persistent while short statuses stay notifications", async () => {
66
+ const pi = createMockPi();
67
+ agentVentExtension(pi.api);
68
+ const notifications = [];
69
+ const ctx = { hasUI: true, ui: { notify: (message) => notifications.push(message) } };
70
+
71
+ await pi.commands.get("agent_vent").handler("help", ctx);
72
+ assert.equal(pi.messages.length, 1);
73
+ assert.equal(pi.messages[0].customType, "agent-vent-command");
74
+ assert.match(pi.messages[0].content, /agent_vent commands/);
75
+ assert.match(notifications[0], /added to the session transcript/);
76
+
77
+ await pi.commands.get("agent_vent").handler("unknown", ctx);
78
+ assert.equal(pi.messages.length, 1);
79
+ assert.match(notifications[1], /Unknown \/agent_vent action/);
80
+ assert.equal(_test.isLongCommandOutput("short"), false);
81
+ });
82
+
83
+ test("agent_vent cancellation fails before expensive store work", async () => {
84
+ const pi = createMockPi();
85
+ agentVentExtension(pi.api);
86
+ const controller = new AbortController();
87
+ controller.abort();
88
+ await assert.rejects(
89
+ () =>
90
+ pi.tools
91
+ .get("agent_vent")
92
+ .execute("cancelled", { action: "summary" }, controller.signal, undefined, {
93
+ cwd: "/repo",
94
+ sessionManager: { getSessionFile: () => undefined },
95
+ }),
96
+ /cancelled/,
97
+ );
98
+ });
99
+
51
100
  test("agent_vent records minimized local diagnostics without external authority claims", async () => {
52
101
  const pi = createMockPi();
53
102
  agentVentExtension(pi.api);
@@ -474,6 +523,146 @@ test("agent_vent records minimized local diagnostics without external authority
474
523
  }
475
524
  });
476
525
 
526
+ test("agent_vent accepts category aliases for tool records and command filters", async () => {
527
+ const pi = createMockPi();
528
+ agentVentExtension(pi.api);
529
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-alias-"));
530
+ const oldDir = process.env.PI_AGENT_VENT_DIR;
531
+ const oldLog = console.log;
532
+ const messages = [];
533
+ process.env.PI_AGENT_VENT_DIR = dir;
534
+ console.log = (message) => messages.push(String(message));
535
+ try {
536
+ const tool = pi.tools.get("agent_vent");
537
+ const result = await tool.execute(
538
+ "tool-call-category-alias",
539
+ {
540
+ action: "record",
541
+ summary: "Workflow friction alias should normalize",
542
+ category: "workflow_friction",
543
+ severity: "medium",
544
+ },
545
+ undefined,
546
+ undefined,
547
+ {
548
+ cwd: "/repo",
549
+ sessionManager: { getSessionFile: () => undefined },
550
+ },
551
+ );
552
+
553
+ assert.equal(result.details.record.category, "workflow");
554
+ assert.equal(
555
+ result.details.record.recurrenceKey,
556
+ "workflow:workflow-friction-alias-should-normalize",
557
+ );
558
+
559
+ const alignmentPreview = await tool.execute(
560
+ "tool-call-context-alignment-alias",
561
+ {
562
+ action: "preview",
563
+ summary: "Context alignment candidate should remain callable",
564
+ category: "context_alignment",
565
+ },
566
+ undefined,
567
+ undefined,
568
+ {
569
+ cwd: "/repo",
570
+ sessionManager: { getSessionFile: () => undefined },
571
+ },
572
+ );
573
+ assert.equal(alignmentPreview.details.wouldRecord, true);
574
+ assert.equal(alignmentPreview.details.recordPreview.category, "other");
575
+
576
+ const reviewResult = await tool.execute(
577
+ "tool-call-category-alias-review",
578
+ { action: "review", category: "workflow_friction" },
579
+ undefined,
580
+ undefined,
581
+ {
582
+ cwd: "/repo",
583
+ sessionManager: { getSessionFile: () => undefined },
584
+ },
585
+ );
586
+ assert.equal(reviewResult.details.reviewQueue.filters.category, "workflow");
587
+ assert.equal(reviewResult.details.reviewQueue.matchingGroupCount, 1);
588
+
589
+ await pi.commands.get("agent_vent").handler("review category=workflow_friction", {
590
+ hasUI: false,
591
+ });
592
+ assert.match(messages[0], /Filters: category=workflow/);
593
+ assert.doesNotMatch(messages[0], /category=workflow_friction/);
594
+ } finally {
595
+ console.log = oldLog;
596
+ if (oldDir === undefined) delete process.env.PI_AGENT_VENT_DIR;
597
+ else process.env.PI_AGENT_VENT_DIR = oldDir;
598
+ fs.rmSync(dir, { recursive: true, force: true });
599
+ }
600
+ });
601
+
602
+ test("agent_vent preview runs anti-junk checks without writing records", async () => {
603
+ const pi = createMockPi();
604
+ agentVentExtension(pi.api);
605
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-preview-"));
606
+ const oldDir = process.env.PI_AGENT_VENT_DIR;
607
+ process.env.PI_AGENT_VENT_DIR = dir;
608
+ try {
609
+ const tool = pi.tools.get("agent_vent");
610
+ const preview = await tool.execute(
611
+ "tool-call-preview",
612
+ {
613
+ action: "preview",
614
+ summary: "Workflow friction alias should normalize before recording",
615
+ category: "workflow_friction",
616
+ tool: "self",
617
+ },
618
+ undefined,
619
+ undefined,
620
+ {
621
+ cwd: "/repo",
622
+ sessionManager: { getSessionFile: () => undefined },
623
+ },
624
+ );
625
+
626
+ assert.equal(preview.details.wouldRecord, true);
627
+ assert.equal(preview.details.recordPreview.category, "workflow");
628
+ assert.match(preview.content[0].text, /No local diagnostic record was written/);
629
+ assert.equal(fs.existsSync(path.join(dir, "vents.jsonl")), false);
630
+
631
+ const junkPreview = await tool.execute(
632
+ "tool-call-preview-junk",
633
+ { action: "preview", summary: "done" },
634
+ undefined,
635
+ undefined,
636
+ {
637
+ cwd: "/repo",
638
+ sessionManager: { getSessionFile: () => undefined },
639
+ },
640
+ );
641
+ assert.equal(junkPreview.details.wouldRecord, false);
642
+ assert.match(junkPreview.content[0].text, /not recordable/);
643
+
644
+ await assert.rejects(
645
+ () =>
646
+ tool.execute(
647
+ "tool-call-record-junk",
648
+ { action: "record", summary: "done" },
649
+ undefined,
650
+ undefined,
651
+ {
652
+ cwd: "/repo",
653
+ sessionManager: { getSessionFile: () => undefined },
654
+ },
655
+ ),
656
+ /anti-junk quality check/,
657
+ );
658
+ assert.equal(fs.existsSync(path.join(dir, "vents.jsonl")), false);
659
+ } finally {
660
+ if (oldDir === undefined) delete process.env.PI_AGENT_VENT_DIR;
661
+ else process.env.PI_AGENT_VENT_DIR = oldDir;
662
+ fs.rmSync(dir, { recursive: true, force: true });
663
+ }
664
+ });
665
+
477
666
  test("agent_vent command rejects unknown review filter keys without creating stores", async () => {
478
667
  const pi = createMockPi();
479
668
  agentVentExtension(pi.api);
@@ -1,3 +1,9 @@
1
+ // ---
2
+ // summary: tests package whitelist parsing and published markdown link validation
3
+ // read_when:
4
+ // - changing release artifact checks, pack output handling, or files-array policy
5
+ // ---
6
+
1
7
  import assert from "node:assert/strict";
2
8
  import fs from "node:fs";
3
9
  import os from "node:os";
@@ -77,6 +83,28 @@ test("pack json parser fails closed for malformed, stale, or unsafe pack output"
77
83
  );
78
84
  });
79
85
 
86
+ test("pack json parser accepts canonical arrays and npm 12 package-keyed objects", () => {
87
+ const entry = { name: "@tryinget/pi-agent-vent", files: [{ path: "package.json" }] };
88
+ assert.deepEqual(parsePackJson(JSON.stringify([entry])), ["package.json"]);
89
+ assert.deepEqual(parsePackJson(JSON.stringify({ "@tryinget/pi-agent-vent": entry })), [
90
+ "package.json",
91
+ ]);
92
+ assert.throws(
93
+ () =>
94
+ parsePackJson(
95
+ JSON.stringify({
96
+ "@tryinget/pi-agent-vent": entry,
97
+ "@tryinget/other": entry,
98
+ }),
99
+ ),
100
+ /Could not parse npm pack/,
101
+ );
102
+ assert.throws(
103
+ () => parsePackJson(JSON.stringify({ "@tryinget/wrong": entry })),
104
+ /Could not parse npm pack/,
105
+ );
106
+ });
107
+
80
108
  test("package files whitelist fails closed for wildcard entries", () => {
81
109
  assert.throws(
82
110
  () =>
@@ -114,13 +142,19 @@ test("package files whitelist detects missing and extra artifact entries", () =>
114
142
  test("release artifact check accepts directory files entries and always-included metadata", () => {
115
143
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-release-check-ok-"));
116
144
  try {
117
- fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ files: ["docs"] }));
145
+ fs.writeFileSync(
146
+ path.join(dir, "package.json"),
147
+ JSON.stringify({ name: "@tryinget/pi-agent-vent", version: "0.2.0", files: ["docs"] }),
148
+ );
118
149
  fs.writeFileSync(path.join(dir, "README.md"), "[guide](docs/guide.md)\n");
119
150
  fs.writeFileSync(path.join(dir, "LICENSE"), "local test license\n");
120
151
  fs.mkdirSync(path.join(dir, "docs"));
121
152
  fs.writeFileSync(path.join(dir, "docs", "guide.md"), "# Guide\n");
122
153
  const packJsonText = JSON.stringify([
123
154
  {
155
+ id: "@tryinget/pi-agent-vent@0.2.0",
156
+ name: "@tryinget/pi-agent-vent",
157
+ version: "0.2.0",
124
158
  files: [
125
159
  { path: "README.md" },
126
160
  { path: "LICENSE" },
@@ -142,18 +176,51 @@ test("release artifact check accepts directory files entries and always-included
142
176
  }
143
177
  });
144
178
 
179
+ test("release artifact check rejects pack identity that differs from the local manifest", () => {
180
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-release-identity-"));
181
+ try {
182
+ fs.writeFileSync(
183
+ path.join(dir, "package.json"),
184
+ JSON.stringify({ name: "@tryinget/pi-agent-vent", version: "0.2.0", files: ["README.md"] }),
185
+ );
186
+ fs.writeFileSync(path.join(dir, "README.md"), "# Agent Vent\n");
187
+ const packJsonText = JSON.stringify([
188
+ {
189
+ id: "@tryinget/not-pi-agent-vent@999.0.0",
190
+ name: "@tryinget/not-pi-agent-vent",
191
+ version: "999.0.0",
192
+ files: [{ path: "README.md" }, { path: "package.json" }],
193
+ },
194
+ ]);
195
+
196
+ assert.throws(
197
+ () => runReleaseArtifactCheck({ cwd: dir, packJsonText, log: () => {} }),
198
+ /npm pack identity mismatch/,
199
+ );
200
+ } finally {
201
+ fs.rmSync(dir, { recursive: true, force: true });
202
+ }
203
+ });
204
+
145
205
  test("release artifact check reports packaged markdown failures with actionable wording", () => {
146
206
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-release-check-"));
147
207
  try {
148
208
  fs.writeFileSync(
149
209
  path.join(dir, "package.json"),
150
- JSON.stringify({ files: ["README.md", "docs/guide.md"] }),
210
+ JSON.stringify({
211
+ name: "@tryinget/pi-agent-vent",
212
+ version: "0.2.0",
213
+ files: ["README.md", "docs/guide.md"],
214
+ }),
151
215
  );
152
216
  fs.mkdirSync(path.join(dir, "docs"));
153
217
  fs.writeFileSync(path.join(dir, "README.md"), "[missing](docs/missing.md)\n");
154
218
  fs.writeFileSync(path.join(dir, "docs", "guide.md"), "# Guide\n");
155
219
  const packJsonText = JSON.stringify([
156
220
  {
221
+ id: "@tryinget/pi-agent-vent@0.2.0",
222
+ name: "@tryinget/pi-agent-vent",
223
+ version: "0.2.0",
157
224
  files: [{ path: "README.md" }, { path: "docs/guide.md" }, { path: "package.json" }],
158
225
  },
159
226
  ]);
@@ -1,3 +1,9 @@
1
+ // ---
2
+ // summary: tests isolated release-smoke contracts for installed agent_vent artifacts
3
+ // read_when:
4
+ // - changing tarball source validation, package discovery, or path-tool smoke probes
5
+ // ---
6
+
1
7
  import assert from "node:assert/strict";
2
8
  import fs from "node:fs";
3
9
  import os from "node:os";
@@ -5,6 +11,7 @@ import path from "node:path";
5
11
  import test from "node:test";
6
12
  import {
7
13
  assertAgentVentPathSmokeOutput,
14
+ assertExactHostContract,
8
15
  assertInstalledArtifactPackage,
9
16
  assertLocalTarballInstallSource,
10
17
  assertPackageSpecInstalled,
@@ -13,6 +20,24 @@ import {
13
20
  packageSourcesFromSettings,
14
21
  } from "../scripts/release-smoke-check.mjs";
15
22
 
23
+ test("release smoke release-age bypass is gated by one exact host contract", () => {
24
+ const packageJson = {
25
+ devDependencies: {
26
+ "@earendil-works/pi-ai": "0.83.0",
27
+ "@earendil-works/pi-coding-agent": "0.83.0",
28
+ },
29
+ };
30
+ assert.equal(assertExactHostContract({ packageJson, hostVersion: "0.83.0" }), "0.83.0");
31
+ assert.throws(
32
+ () => assertExactHostContract({ packageJson, hostVersion: "^0.83.0" }),
33
+ /requires an exact Pi host version/,
34
+ );
35
+ assert.throws(
36
+ () => assertExactHostContract({ packageJson, hostVersion: "0.83.1" }),
37
+ /host contract mismatch/,
38
+ );
39
+ });
40
+
16
41
  test("release smoke settings check accepts only unfiltered string package entries", () => {
17
42
  const settings = {
18
43
  packages: [
@@ -1,3 +1,9 @@
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
+
1
7
  import assert from "node:assert/strict";
2
8
  import { createHash } from "node:crypto";
3
9
  import fs from "node:fs";
@@ -111,6 +117,43 @@ test("createVentRecord minimizes, redacts, and derives recurrence key", () => {
111
117
  assert.deepEqual(tagOnlySecret.privacy.redactionPatterns, ["assigned_secret"]);
112
118
  });
113
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
+
114
157
  test("explicit recurrence keys are redacted before slugging", () => {
115
158
  const record = createVentRecord({
116
159
  summary: "Explicit key should not leak",