@pi-unipi/unipi 2.4.0 → 2.4.2

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +26 -22
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +4 -2
  5. package/packages/btw/package.json +2 -2
  6. package/packages/cocoindex/package.json +2 -2
  7. package/packages/cocoindex/tools.ts +0 -8
  8. package/packages/compactor/README.md +18 -22
  9. package/packages/compactor/package.json +3 -3
  10. package/packages/compactor/src/compaction/auto-trigger.ts +0 -0
  11. package/packages/compactor/src/compaction/build-sections.ts +2 -2
  12. package/packages/compactor/src/compaction/format.ts +1 -1
  13. package/packages/compactor/src/config/presets.ts +19 -30
  14. package/packages/compactor/src/config/schema.ts +0 -0
  15. package/packages/compactor/src/index.ts +39 -56
  16. package/packages/compactor/src/session/resume-inject.ts +27 -0
  17. package/packages/compactor/src/tools/ctx-batch-execute.ts +4 -2
  18. package/packages/compactor/src/tools/ctx-execute-file.ts +4 -2
  19. package/packages/compactor/src/tools/ctx-execute.ts +4 -2
  20. package/packages/compactor/src/tools/register.ts +22 -4
  21. package/packages/compactor/src/tui/settings-overlay.ts +9 -22
  22. package/packages/compactor/src/types.ts +24 -2
  23. package/packages/core/events.ts +13 -13
  24. package/packages/core/package.json +3 -2
  25. package/packages/footer/README.md +2 -2
  26. package/packages/footer/package.json +3 -3
  27. package/packages/footer/src/events.ts +0 -10
  28. package/packages/footer/src/segments/core.ts +0 -9
  29. package/packages/image/package.json +2 -2
  30. package/packages/info-screen/core-groups.ts +0 -14
  31. package/packages/info-screen/package.json +2 -2
  32. package/packages/input-shortcuts/package.json +2 -2
  33. package/packages/kanboard/commands.ts +0 -1
  34. package/packages/kanboard/package.json +2 -2
  35. package/packages/mcp/package.json +2 -2
  36. package/packages/memory/package.json +3 -3
  37. package/packages/memory/storage.ts +0 -18
  38. package/packages/memory/tools.ts +0 -1
  39. package/packages/milestone/hooks.ts +9 -15
  40. package/packages/milestone/package.json +2 -2
  41. package/packages/notify/README.md +2 -0
  42. package/packages/notify/events.ts +23 -13
  43. package/packages/notify/package.json +2 -2
  44. package/packages/notify/tools.ts +17 -8
  45. package/packages/notify/types.ts +5 -1
  46. package/packages/ralph/index.ts +10 -5
  47. package/packages/ralph/package.json +3 -3
  48. package/packages/ralph/ralph-loop.ts +10 -0
  49. package/packages/subagents/README.md +3 -1
  50. package/packages/subagents/package.json +10 -6
  51. package/packages/subagents/src/agent-manager.ts +46 -11
  52. package/packages/subagents/src/core-compat.ts +38 -0
  53. package/packages/subagents/src/global.d.ts +7 -0
  54. package/packages/subagents/src/index.ts +6 -3
  55. package/packages/subagents/src/widget.ts +0 -7
  56. package/packages/unipi/bundled.js +371 -438
  57. package/packages/updater/package.json +4 -3
  58. package/packages/utility/README.md +1 -1
  59. package/packages/utility/package.json +7 -4
  60. package/packages/utility/src/commands.ts +2 -10
  61. package/packages/utility/src/diff/theme.ts +0 -3
  62. package/packages/utility/src/tui/name-badge.ts +0 -9
  63. package/packages/web-api/package.json +2 -2
  64. package/packages/web-api/src/engine/extract.ts +0 -26
  65. package/packages/web-api/src/tools.ts +0 -13
  66. package/packages/workflow/commands.ts +13 -1
  67. package/packages/workflow/index.ts +12 -1
  68. package/packages/workflow/lifecycle.ts +43 -0
  69. package/packages/workflow/package.json +3 -2
  70. package/packages/autocomplete/index.ts +0 -7
  71. package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +0 -120
  72. package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +0 -423
  73. package/packages/compactor/src/compaction/recall-scope.ts +0 -32
  74. package/packages/compactor/src/compaction/sections.ts +0 -15
  75. package/packages/compactor/src/display/bash-display.ts +0 -28
  76. package/packages/compactor/src/display/diff-presentation.ts +0 -25
  77. package/packages/compactor/src/display/pending-diff-preview.ts +0 -51
  78. package/packages/compactor/src/display/user-message-box.ts +0 -16
  79. package/packages/compactor/src/session/analytics.ts +0 -205
  80. package/packages/image/index.ts +0 -1
  81. package/packages/input-shortcuts/index.ts +0 -5
  82. package/packages/input-shortcuts/src/status.ts +0 -35
  83. package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
  84. package/packages/mcp/index.ts +0 -1
  85. package/packages/memory/search.ts +0 -134
  86. package/packages/milestone/coexist.ts +0 -114
  87. package/packages/notify/src/__tests__/ask-user-prompt-message.test.ts +0 -159
  88. package/packages/notify/src/__tests__/event-bus.test.ts +0 -119
  89. package/packages/notify/src/__tests__/permission-prompt-message.test.ts +0 -172
  90. package/packages/subagents/src/__tests__/badge-generation.test.ts +0 -315
  91. package/packages/subagents/src/__tests__/config.test.ts +0 -240
  92. package/packages/subagents/src/__tests__/esc-propagation.test.ts +0 -162
  93. package/packages/subagents/src/__tests__/file-lock.test.ts +0 -244
  94. package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
  95. package/packages/subagents/src/__tests__/workflow-integration.test.ts +0 -334
  96. package/packages/subagents/src/prompts.ts +0 -39
  97. package/packages/utility/index.ts +0 -1
  98. package/packages/web-api/src/tui/progress.ts +0 -168
  99. package/packages/web-api/src/tui/result.ts +0 -173
@@ -1,114 +0,0 @@
1
- /**
2
- * @pi-unipi/milestone — Coexist triggers
3
- *
4
- * Hooks into workflow skill completions to offer milestone integration.
5
- * Non-blocking — if MILESTONES.md doesn't exist, triggers silently skip.
6
- */
7
-
8
- import * as path from "node:path";
9
- import { MILESTONE_DIRS, tryRead } from "@pi-unipi/core";
10
- import { parseMilestones, updateItemStatus, writeMilestones } from "./milestone.js";
11
- import type { MilestoneDoc } from "./types.js";
12
-
13
- /**
14
- * After brainstorm completes: check if new spec items map to milestones.
15
- * Offers to mark matching items as planned.
16
- */
17
- export function onBrainstormComplete(specPath: string): void {
18
- const cwd = process.cwd();
19
- const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
20
-
21
- // Silently skip if no MILESTONES.md
22
- if (!tryRead(milestonesPath)) return;
23
-
24
- const specContent = tryRead(specPath);
25
- if (!specContent) return;
26
-
27
- // Extract checklist items from the new spec
28
- const specItems: string[] = [];
29
- for (const line of specContent.split("\n")) {
30
- const match = line.match(/^-\s+\[([ xX])\]\s+(.+)$/);
31
- if (match) {
32
- specItems.push(match[2].trim().toLowerCase());
33
- }
34
- }
35
-
36
- if (specItems.length === 0) return;
37
-
38
- // Check against milestones
39
- const doc = parseMilestones(milestonesPath);
40
- const matched: string[] = [];
41
-
42
- for (const phase of doc.phases) {
43
- for (const item of phase.items) {
44
- const normalized = item.text.toLowerCase().trim();
45
- if (specItems.includes(normalized) && !item.checked) {
46
- matched.push(`"${item.text}" in ${phase.name}`);
47
- }
48
- }
49
- }
50
-
51
- if (matched.length > 0) {
52
- // Removed console.log — milestone matches are informational.
53
- // Use /unipi:milestone-update to sync manually.
54
- }
55
- }
56
-
57
- /**
58
- * After plan completes: check if plan tasks map to milestone items.
59
- * Logs matching items for awareness.
60
- */
61
- export function onPlanComplete(planPath: string): void {
62
- const cwd = process.cwd();
63
- const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
64
-
65
- // Silently skip if no MILESTONES.md
66
- if (!tryRead(milestonesPath)) return;
67
-
68
- const planContent = tryRead(planPath);
69
- if (!planContent) return;
70
-
71
- // Extract task names from plan (### Task N — Name pattern)
72
- const planTasks: string[] = [];
73
- for (const line of planContent.split("\n")) {
74
- const match = line.match(/^###\s+Task\s+\d+\s*[—–-]\s*(.+)$/);
75
- if (match) {
76
- planTasks.push(match[1].trim().toLowerCase());
77
- }
78
- }
79
-
80
- if (planTasks.length === 0) return;
81
-
82
- // Check against milestones
83
- const doc = parseMilestones(milestonesPath);
84
- const matched: string[] = [];
85
-
86
- for (const phase of doc.phases) {
87
- for (const item of phase.items) {
88
- const normalized = item.text.toLowerCase().trim();
89
- // Check if any plan task contains the milestone item text or vice versa
90
- for (const task of planTasks) {
91
- if (task.includes(normalized) || normalized.includes(task)) {
92
- matched.push(`"${item.text}" → plan task`);
93
- break;
94
- }
95
- }
96
- }
97
- }
98
-
99
- if (matched.length > 0) {
100
- // Removed console.log — plan-milestone matches are informational.
101
- }
102
- }
103
-
104
- /**
105
- * After consolidate: reference milestone sync that already happened.
106
- */
107
- export function onConsolidate(): void {
108
- const cwd = process.cwd();
109
- const milestonesPath = path.join(cwd, MILESTONE_DIRS.MILESTONES);
110
-
111
- if (!tryRead(milestonesPath)) return;
112
-
113
- // Removed console.log — milestone auto-sync is silent.
114
- }
@@ -1,159 +0,0 @@
1
- /**
2
- * Test: Notify — buildAskUserPromptMessage
3
- *
4
- * Tests the internal helper directly by importing from its module.
5
- * The test fixtures use `satisfies AskUserPromptEventPayload` to
6
- * enforce compile-time alignment with the upstream event contract.
7
- *
8
- * Run: node --experimental-strip-types --test
9
- */
10
-
11
- import { describe, it } from "node:test";
12
- import assert from "node:assert/strict";
13
- import {
14
- buildAskUserPromptMessage,
15
- type AskUserPromptEventPayload,
16
- } from "../../ask-user-prompt-message.ts";
17
-
18
- describe("buildAskUserPromptMessage", () => {
19
- it("standard lossless payload", () => {
20
- const payload = {
21
- questions: [
22
- {
23
- question: "Which library?",
24
- header: "Lib",
25
- multiSelect: false,
26
- options: [
27
- { label: "React", description: "UI library", hasPreview: false },
28
- { label: "Vue", description: "Another UI library", hasPreview: false },
29
- ],
30
- },
31
- ],
32
- } satisfies AskUserPromptEventPayload;
33
-
34
- assert.equal(buildAskUserPromptMessage(payload), "Agent asks: Which library? — React, Vue");
35
- });
36
-
37
- it("multiple questions appends (+N more) suffix", () => {
38
- const payload = {
39
- questions: [
40
- {
41
- question: "First question",
42
- header: "Q1",
43
- multiSelect: false,
44
- options: [{ label: "A", description: "Option A", hasPreview: false }],
45
- },
46
- {
47
- question: "Second question",
48
- header: "Q2",
49
- multiSelect: false,
50
- options: [{ label: "B", description: "Option B", hasPreview: false }],
51
- },
52
- ],
53
- } satisfies AskUserPromptEventPayload;
54
-
55
- assert.equal(buildAskUserPromptMessage(payload), "Agent asks: First question (+1 more) — A");
56
- });
57
-
58
- it("malformed payload (empty question object) falls back to A question", () => {
59
- // 故意构造非法 payload,验证运行时降级路径
60
- const payload = { questions: [{}] } as unknown as AskUserPromptEventPayload;
61
-
62
- assert.equal(buildAskUserPromptMessage(payload), "Agent asks: A question");
63
- });
64
-
65
- it("empty payload falls back to A question", () => {
66
- assert.equal(buildAskUserPromptMessage({}), "Agent asks: A question");
67
- });
68
-
69
- it("null payload falls back to A question", () => {
70
- assert.equal(buildAskUserPromptMessage(null), "Agent asks: A question");
71
- });
72
-
73
- it("missing question string in first question falls back", () => {
74
- const payload = {
75
- questions: [
76
- {
77
- question: "",
78
- header: "H",
79
- multiSelect: false,
80
- options: [{ label: "Opt", description: "D", hasPreview: false }],
81
- },
82
- ],
83
- } satisfies AskUserPromptEventPayload;
84
-
85
- assert.equal(buildAskUserPromptMessage(payload), "Agent asks: A question — Opt");
86
- });
87
-
88
- it("whitespace-only question is treated as missing", () => {
89
- const payload = {
90
- questions: [
91
- {
92
- question: " ",
93
- header: "H",
94
- multiSelect: false,
95
- options: [{ label: "Opt", description: "D", hasPreview: false }],
96
- },
97
- ],
98
- } satisfies AskUserPromptEventPayload;
99
-
100
- // nonEmptyString uses trim().length check, so whitespace-only falls back
101
- assert.equal(buildAskUserPromptMessage(payload), "Agent asks: A question — Opt");
102
- });
103
-
104
- it("empty option labels do not produce stray commas", () => {
105
- const payload = {
106
- questions: [
107
- {
108
- question: "Pick one?",
109
- header: "Pick",
110
- multiSelect: false,
111
- options: [
112
- { label: "React", description: "UI lib", hasPreview: false },
113
- { label: "", description: "Empty label", hasPreview: false },
114
- { label: "Vue", description: "Another UI lib", hasPreview: false },
115
- ],
116
- },
117
- ],
118
- } satisfies AskUserPromptEventPayload;
119
-
120
- assert.equal(buildAskUserPromptMessage(payload), "Agent asks: Pick one? — React, Vue");
121
- });
122
-
123
- it("no options produces bare question", () => {
124
- const payload = {
125
- questions: [
126
- {
127
- question: "Just type?",
128
- header: "Free",
129
- multiSelect: false,
130
- options: [],
131
- },
132
- ],
133
- } satisfies AskUserPromptEventPayload;
134
-
135
- assert.equal(buildAskUserPromptMessage(payload), "Agent asks: Just type?");
136
- });
137
-
138
- it("legacy flat UniPi payload preserves question and context", () => {
139
- const payload = {
140
- question: "Proceed with deploy?",
141
- context: "Production, with smoke tests",
142
- optionCount: 2,
143
- allowMultiple: false,
144
- allowFreeform: true,
145
- };
146
-
147
- assert.equal(
148
- buildAskUserPromptMessage(payload),
149
- "Agent asks: Proceed with deploy? — Production, with smoke tests",
150
- );
151
- });
152
-
153
- it("legacy flat UniPi payload works without context", () => {
154
- assert.equal(
155
- buildAskUserPromptMessage({ question: "Proceed?", context: "" }),
156
- "Agent asks: Proceed?",
157
- );
158
- });
159
- });
@@ -1,119 +0,0 @@
1
- /**
2
- * Test: Notify — event bus registration
3
- *
4
- * Verifies that the notify plugin correctly uses pi.events.on() for custom
5
- * unipi events and pi.on() for pi lifecycle events — same pattern enforced
6
- * by subagents/badge-generation.test.ts.
7
- *
8
- * BUG 2 (Wrong event bus):
9
- * Cross-module events emitted via pi.events.emit() must be listened via
10
- * pi.events.on(), NOT pi.on() (which only dispatches lifecycle events).
11
- */
12
-
13
- import { describe, it } from "node:test";
14
- import assert from "node:assert/strict";
15
- import { readFileSync, existsSync } from "node:fs";
16
- import { join } from "node:path";
17
-
18
- const ROOT = join(import.meta.dirname, "../../../../");
19
-
20
- function readSource(relativePath: string): string {
21
- const fullPath = join(ROOT, relativePath);
22
- if (!existsSync(fullPath)) throw new Error(`File not found: ${fullPath}`);
23
- return readFileSync(fullPath, "utf-8");
24
- }
25
-
26
- // ─── Known lifecycle events (mirrors LIFECYCLE_EVENTS in events.ts) ──
27
-
28
- const LIFECYCLE_EVENTS = new Set([
29
- "agent_end",
30
- "agent_settled",
31
- "session_shutdown",
32
- ]);
33
-
34
- // ─── Test: events.ts correctly uses pi.events.on() for custom events ──
35
-
36
- describe("notify — event bus registration", () => {
37
- it("events.ts defines LIFECYCLE_EVENTS with correct lifecycle events", () => {
38
- const src = readSource("packages/notify/events.ts");
39
-
40
- const lifecycleMatch = src.match(
41
- /const LIFECYCLE_EVENTS\s*=\s*new Set\((\[.*?\])\)/s,
42
- );
43
- assert.ok(lifecycleMatch, "LIFECYCLE_EVENTS should be defined");
44
-
45
- const parsed = [...lifecycleMatch[1].matchAll(/"([^"]+)"/g)].map(
46
- (match) => match[1],
47
- );
48
- assert.deepStrictEqual(
49
- parsed.sort(),
50
- [...LIFECYCLE_EVENTS].sort(),
51
- "LIFECYCLE_EVENTS should contain exactly agent_end, agent_settled, and session_shutdown",
52
- );
53
- });
54
-
55
- it("unipi:* events use pi.events.on(), NOT pi.on()", () => {
56
- const src = readSource("packages/notify/events.ts");
57
-
58
- assert.match(
59
- src,
60
- /pi\.events\.on\(def\.hook,\s*handler\)/,
61
- "Custom unipi events should use pi.events.on(def.hook, handler)",
62
- );
63
-
64
- assert.doesNotMatch(
65
- src,
66
- /(?:\(pi\s+as\s+any\)|pi)\.on\s*\(\s*UNIPI_EVENTS\./,
67
- "Should NOT use pi.on() for custom unipi events",
68
- );
69
- });
70
-
71
- it("lifecycle events (agent_end, agent_settled, session_shutdown) still use pi.on()", () => {
72
- const src = readSource("packages/notify/events.ts");
73
-
74
- assert.ok(
75
- src.includes("LIFECYCLE_EVENTS.has(eventKey)") &&
76
- src.includes("(pi as any).on(def.hook, handler)"),
77
- "Lifecycle events should be routed through pi.on() in the registration loop",
78
- );
79
- });
80
-
81
- it("MODULE_READY listener uses pi.events.on()", () => {
82
- const src = readSource("packages/notify/events.ts");
83
-
84
- assert.match(
85
- src,
86
- /pi\.events\s*\.\s*on\s*\(\s*UNIPI_EVENTS\.MODULE_READY/,
87
- "MODULE_READY should use pi.events.on()",
88
- );
89
-
90
- assert.doesNotMatch(
91
- src,
92
- /(?:\(pi\s+as\s+any\)|pi)\.on\s*\(\s*UNIPI_EVENTS\.MODULE_READY/,
93
- "MODULE_READY should NOT use pi.on()",
94
- );
95
- });
96
- });
97
-
98
- // ─── Test: index.ts only uses pi.on() for lifecycle events ──────────
99
-
100
- describe("notify — index.ts event registration", () => {
101
- it("all pi.on() calls in index.ts use lifecycle events only", () => {
102
- const src = readSource("packages/notify/index.ts");
103
-
104
- const validLifecycleEvents = [
105
- "resources_discover",
106
- "session_start",
107
- "session_shutdown",
108
- ];
109
-
110
- const piOnPattern = /pi\.on\("([^"]+)"/g;
111
- let match: RegExpExecArray | null;
112
- while ((match = piOnPattern.exec(src)) !== null) {
113
- assert.ok(
114
- validLifecycleEvents.includes(match[1]),
115
- `index.ts: pi.on("${match[1]}") should be a lifecycle event`,
116
- );
117
- }
118
- });
119
- });
@@ -1,172 +0,0 @@
1
- /**
2
- * Tests for buildPermissionPromptMessage — the `permissions:ui_prompt`
3
- * payload projection used by the `permission_request` notify event.
4
- *
5
- * The test fixtures use `satisfies PermissionPromptEventPayload` to
6
- * enforce the payload contract at compile time.
7
- */
8
-
9
- import { describe, it } from "node:test";
10
- import assert from "node:assert/strict";
11
-
12
- import {
13
- buildPermissionPromptMessage,
14
- type PermissionPromptEventPayload,
15
- } from "../../permission-prompt-message.ts";
16
-
17
- describe("buildPermissionPromptMessage", () => {
18
- it("formats a standard bash permission payload", () => {
19
- const payload = {
20
- requestId: "req-1",
21
- source: "tool",
22
- surface: "bash",
23
- value: "npm test",
24
- agentName: "Current agent",
25
- message: "Allow this command?",
26
- } satisfies PermissionPromptEventPayload;
27
-
28
- assert.equal(
29
- buildPermissionPromptMessage(payload),
30
- "Current agent requested bash 'npm test'. Allow this command?",
31
- );
32
- });
33
-
34
- it("falls back to \"Agent\" when agentName is absent", () => {
35
- const payload = {
36
- surface: "edit",
37
- value: "src/index.ts",
38
- } satisfies PermissionPromptEventPayload;
39
-
40
- assert.equal(
41
- buildPermissionPromptMessage(payload),
42
- "Agent requested edit 'src/index.ts'.",
43
- );
44
- });
45
-
46
- it("marks forwarded subagent prompts", () => {
47
- const payload = {
48
- surface: "mcp",
49
- value: "github.create_issue",
50
- agentName: "explore",
51
- forwarding: { from: "subagent-1" },
52
- } satisfies PermissionPromptEventPayload;
53
-
54
- assert.equal(
55
- buildPermissionPromptMessage(payload),
56
- "explore requested mcp 'github.create_issue'. (forwarded)",
57
- );
58
- });
59
-
60
- it("does not mark prompts when forwarding is explicitly falsy", () => {
61
- for (const forwarding of [false, null, "", 0, {}, []]) {
62
- const payload = {
63
- surface: "read",
64
- value: "/etc/hosts",
65
- forwarding,
66
- } satisfies PermissionPromptEventPayload;
67
-
68
- const result = buildPermissionPromptMessage(payload);
69
- assert.equal(
70
- result.includes("(forwarded)"),
71
- false,
72
- `expected no forwarded marker for ${JSON.stringify(forwarding)}`,
73
- );
74
- }
75
- });
76
-
77
- it("uses surface alone when no value is provided", () => {
78
- const payload = { surface: "bash" } satisfies PermissionPromptEventPayload;
79
-
80
- assert.equal(
81
- buildPermissionPromptMessage(payload),
82
- "Agent requested bash access.",
83
- );
84
- });
85
-
86
- it("uses value alone when no surface is provided", () => {
87
- const payload = { value: "rm -rf build" } satisfies PermissionPromptEventPayload;
88
-
89
- assert.equal(
90
- buildPermissionPromptMessage(payload),
91
- "Agent requested 'rm -rf build'.",
92
- );
93
- });
94
-
95
- it("falls back to the message when surface and value are missing", () => {
96
- const payload = {
97
- requestId: "req-2",
98
- message: "Allow access to the workspace?",
99
- } satisfies PermissionPromptEventPayload;
100
-
101
- assert.equal(
102
- buildPermissionPromptMessage(payload),
103
- "Allow access to the workspace?",
104
- );
105
- });
106
-
107
- it("does not duplicate the message when it equals the request clause", () => {
108
- const payload = {
109
- surface: "bash",
110
- value: "npm test",
111
- agentName: "Current agent",
112
- message: "Current agent requested bash 'npm test'.",
113
- } satisfies PermissionPromptEventPayload;
114
-
115
- assert.equal(
116
- buildPermissionPromptMessage(payload),
117
- "Current agent requested bash 'npm test'.",
118
- );
119
- });
120
-
121
- it("returns a generic fallback for an empty payload", () => {
122
- assert.equal(
123
- buildPermissionPromptMessage({}),
124
- "Pi is waiting for a permission decision.",
125
- );
126
- });
127
-
128
- it("handles null, undefined and non-object payloads without throwing", () => {
129
- for (const payload of [null, undefined, 42, "nope", true]) {
130
- assert.equal(
131
- buildPermissionPromptMessage(payload),
132
- "Pi is waiting for a permission decision.",
133
- );
134
- }
135
- });
136
-
137
- it("treats empty and whitespace-only strings as absent", () => {
138
- const payload = {
139
- surface: " ",
140
- value: "",
141
- agentName: " ",
142
- message: "\t\n",
143
- } satisfies PermissionPromptEventPayload;
144
-
145
- assert.equal(
146
- buildPermissionPromptMessage(payload),
147
- "Pi is waiting for a permission decision.",
148
- );
149
- });
150
-
151
- it("ignores non-string surface/value/message fields", () => {
152
- const payload = {
153
- surface: 123,
154
- value: { cmd: "ls" },
155
- message: ["a"],
156
- } as unknown as PermissionPromptEventPayload;
157
-
158
- assert.equal(
159
- buildPermissionPromptMessage(payload),
160
- "Pi is waiting for a permission decision.",
161
- );
162
- });
163
-
164
- it("still reports a forwarded prompt with no other detail", () => {
165
- const payload = { forwarding: true } satisfies PermissionPromptEventPayload;
166
-
167
- assert.equal(
168
- buildPermissionPromptMessage(payload),
169
- "Pi is waiting for a permission decision. (forwarded)",
170
- );
171
- });
172
- });