gentle-shell-bridge 0.3.0 → 0.3.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 (76) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +91 -722
  3. package/docs/contract.md +92 -35
  4. package/package.json +13 -1
  5. package/src/api.ts +1 -1
  6. package/.codegraph/.gitignore +0 -5
  7. package/PRD.md +0 -144
  8. package/examples/bash-card.ts +0 -1694
  9. package/gentle-shell-bridge-0.3.0.tgz +0 -0
  10. package/odd/tasks/palette-sections.md +0 -30
  11. package/odd/tasks/plugin-keymap.md +0 -32
  12. package/odd/tasks/plugin-surface-contract.md +0 -55
  13. package/test/api.test.ts +0 -395
  14. package/test/apply-on-move.test.ts +0 -477
  15. package/test/arm.test.ts +0 -220
  16. package/test/bridge-ready.test.ts +0 -309
  17. package/test/bus.test.ts +0 -165
  18. package/test/button-rows.test.ts +0 -409
  19. package/test/capture.test.ts +0 -356
  20. package/test/card-style-rows.test.ts +0 -510
  21. package/test/card-style-transform.test.ts +0 -379
  22. package/test/card-style-wiring.test.ts +0 -258
  23. package/test/card-styles-e2e.test.ts +0 -491
  24. package/test/card-styles.test.ts +0 -324
  25. package/test/card-transcript-path.test.ts +0 -178
  26. package/test/config.test.ts +0 -93
  27. package/test/e2e.test.ts +0 -443
  28. package/test/example-bash-identity.test.ts +0 -372
  29. package/test/example-bash-matrix.test.ts +0 -856
  30. package/test/example-designs.test.ts +0 -734
  31. package/test/example-neon-transform.test.ts +0 -283
  32. package/test/example-plain-style.test.ts +0 -1268
  33. package/test/extension.test.ts +0 -160
  34. package/test/fake-events.ts +0 -58
  35. package/test/fake-host.ts +0 -211
  36. package/test/fake-sidebar.ts +0 -516
  37. package/test/fake-transcript.ts +0 -486
  38. package/test/frame-telemetry.test.ts +0 -417
  39. package/test/hook.test.ts +0 -196
  40. package/test/manifest.test.ts +0 -105
  41. package/test/new-category.test.ts +0 -979
  42. package/test/notices.test.ts +0 -328
  43. package/test/plugin-integration-lines.test.ts +0 -979
  44. package/test/plugin-surface.test.ts +0 -152
  45. package/test/presence.test.ts +0 -459
  46. package/test/rail-surfaces.test.ts +0 -457
  47. package/test/rail-width.test.ts +0 -423
  48. package/test/regression.test.ts +0 -483
  49. package/test/repaint-api.test.ts +0 -200
  50. package/test/reserved-categories.test.ts +0 -361
  51. package/test/row-labels.test.ts +0 -284
  52. package/test/row-names.test.ts +0 -473
  53. package/test/row-origin.test.ts +0 -395
  54. package/test/section-interaction.test.ts +0 -364
  55. package/test/section-panel.test.ts +0 -403
  56. package/test/sections-rows.test.ts +0 -291
  57. package/test/sidebar-backend.test.ts +0 -471
  58. package/test/sidebar-e2e.test.ts +0 -331
  59. package/test/sidebar-hover.test.ts +0 -276
  60. package/test/sidebar-independent.test.ts +0 -349
  61. package/test/sidebar-render.test.ts +0 -281
  62. package/test/sidebar-sections.test.ts +0 -245
  63. package/test/status-card-styles.test.ts +0 -201
  64. package/test/status-section-frame.test.ts +0 -181
  65. package/test/status-sections.test.ts +0 -243
  66. package/test/status.test.ts +0 -209
  67. package/test/store.test.ts +0 -192
  68. package/test/surface-repaint-cost.test.ts +0 -520
  69. package/test/tool-design-chain.test.ts +0 -483
  70. package/test/tool-design-cost.test.ts +0 -473
  71. package/test/tool-design-rows.test.ts +0 -359
  72. package/test/tool-designs.test.ts +0 -745
  73. package/test/transform-pipeline.test.ts +0 -465
  74. package/test/transforms-bus.test.ts +0 -233
  75. package/test/verifier-width-regressions.test.ts +0 -164
  76. package/tsconfig.json +0 -18
@@ -1,379 +0,0 @@
1
- /**
2
- * The line transformer (S3, S6, S12): detection, extraction, repaint, and every pass-through.
3
- *
4
- * Everything here runs against `test/fake-transcript.ts`, whose rows are real `float` output, so the
5
- * detector is proven on the grammar it was written from rather than on a simplified copy of it.
6
- *
7
- * The bounded bets in this suite, stated so a reviewer can see them:
8
- * - a pass-through returns the SAME array instance (S3: "the same array reference, untouched");
9
- * - a card is a run of float rows: padding, header, optional separator, body, padding — the
10
- * empty-body card being exactly three rows;
11
- * - `neon` rows start with `╭`, `│` or `╰` and are therefore never float cards;
12
- * - a style that throws leaves the WHOLE frame alone and is recorded (S6).
13
- */
14
-
15
- import assert from "node:assert/strict";
16
- import { mkdtempSync, rmSync } from "node:fs";
17
- import { tmpdir } from "node:os";
18
- import { join } from "node:path";
19
- import test from "node:test";
20
-
21
- import { registerCardStyle, resetBusForTests, resetCardStylesForTests } from "../src/bus.ts";
22
- import {
23
- getCardStyleTransformFailures,
24
- resetCardStyleTransformForTests,
25
- transformCardLines,
26
- } from "../src/card-style-transform.ts";
27
- import { setCardStyleSelection } from "../src/card-styles.ts";
28
- import { resetStoreStateForTests } from "../src/store.ts";
29
- import type {
30
- CardFrameBottomContext,
31
- CardFrameLineContext,
32
- CardFrameTopContext,
33
- CardStyleRegistration,
34
- } from "../src/types.ts";
35
- import {
36
- BASH_CARD,
37
- BLANK_LINE,
38
- CONVERSATION,
39
- NEON_CARD,
40
- PROSE_LINE,
41
- READ_CARD,
42
- SUBTITLE_CARD,
43
- TRUNCATED_CARD,
44
- stripAnsi,
45
- } from "./fake-transcript.ts";
46
-
47
- let configHome: string;
48
- let previousConfigHome: string | undefined;
49
-
50
- test.before(() => {
51
- configHome = mkdtempSync(join(tmpdir(), "gsb-transform-"));
52
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
53
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
54
- });
55
-
56
- test.after(() => {
57
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
58
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
59
- rmSync(configHome, { recursive: true, force: true });
60
- });
61
-
62
- test.beforeEach(() => {
63
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
64
- resetBusForTests();
65
- resetCardStylesForTests();
66
- resetCardStyleTransformForTests();
67
- resetStoreStateForTests();
68
- setCardStyleSelection("native");
69
- });
70
-
71
- /** What one style was asked to draw, in call order. */
72
- interface Recorded {
73
- top: CardFrameTopContext[];
74
- line: CardFrameLineContext[];
75
- bottom: CardFrameBottomContext[];
76
- }
77
-
78
- /** A style with a frame a test can read at a glance, and a log of every call it received. */
79
- function recordingStyle(id = "loon.cute", label = "cute"): {
80
- style: CardStyleRegistration;
81
- calls: Recorded;
82
- } {
83
- const calls: Recorded = { top: [], line: [], bottom: [] };
84
- return {
85
- calls,
86
- style: {
87
- id,
88
- label,
89
- frame: {
90
- top: (ctx) => {
91
- calls.top.push(ctx);
92
- return `+${ctx.title ?? ""}${ctx.hint === undefined ? "" : `|${ctx.hint}`}`;
93
- },
94
- line: (ctx) => {
95
- calls.line.push(ctx);
96
- return `> ${ctx.text}`;
97
- },
98
- bottom: (ctx) => {
99
- calls.bottom.push(ctx);
100
- return "-";
101
- },
102
- },
103
- },
104
- };
105
- }
106
-
107
- /** Visible width of a row, ANSI ignored: the rows this suite produces are narrow single-width. */
108
- function widthOf(line: string): number {
109
- return [...stripAnsi(line)].length;
110
- }
111
-
112
- // ---------------------------------------------------------------------------------------------
113
- // Pass-through: the same array instance, untouched
114
- // ---------------------------------------------------------------------------------------------
115
-
116
- test("native selection is a pass-through: the same array reference (S3)", () => {
117
- const lines = [...CONVERSATION];
118
-
119
- const output = transformCardLines(lines, 40);
120
-
121
- assert.equal(output, lines, "identity, not a copy");
122
- assert.deepEqual(output, [...CONVERSATION]);
123
- });
124
-
125
- test("a selection that names no registered style is a pass-through too (S12)", () => {
126
- registerCardStyle(recordingStyle().style);
127
- setCardStyleSelection("loon.gone");
128
- const lines = [...CONVERSATION];
129
-
130
- assert.equal(transformCardLines(lines, 40), lines, "missing degrades to native, not to a guess");
131
- });
132
-
133
- test("a frame with no card is returned untouched even with a style selected", () => {
134
- registerCardStyle(recordingStyle().style);
135
- setCardStyleSelection("loon.cute");
136
- const lines = [PROSE_LINE, BLANK_LINE, PROSE_LINE];
137
-
138
- assert.equal(transformCardLines(lines, 40), lines);
139
- });
140
-
141
- // ---------------------------------------------------------------------------------------------
142
- // Repaint
143
- // ---------------------------------------------------------------------------------------------
144
-
145
- test("a float card is reframed by the selected style (S3)", () => {
146
- const { style, calls } = recordingStyle();
147
- registerCardStyle(style);
148
- setCardStyleSelection(style.id);
149
-
150
- const output = transformCardLines([...READ_CARD], 40);
151
-
152
- assert.deepEqual(output, ["+read|ctrl+o", "> config.ts", "-"]);
153
- assert.deepEqual(calls.top, [{ width: 40, title: "read", hint: "ctrl+o" }], "one extraction");
154
- assert.deepEqual(calls.line, [{ text: "config.ts", width: 40, first: true }]);
155
- assert.deepEqual(calls.bottom, [{ width: 40 }]);
156
- });
157
-
158
- test("the content width the host rendered at is the width the style is told", () => {
159
- const { style, calls } = recordingStyle();
160
- registerCardStyle(style);
161
- setCardStyleSelection(style.id);
162
-
163
- transformCardLines([...READ_CARD], 39);
164
-
165
- assert.equal(calls.top[0]?.width, 39);
166
- assert.equal(calls.line[0]?.width, 39);
167
- assert.equal(calls.bottom[0]?.width, 39);
168
- });
169
-
170
- test("an empty-body card is padding, header, closer: no line calls (S3)", () => {
171
- const { style, calls } = recordingStyle();
172
- registerCardStyle(style);
173
- setCardStyleSelection(style.id);
174
-
175
- const output = transformCardLines([...BASH_CARD], 40);
176
-
177
- assert.deepEqual(output, ["+bash", "-"]);
178
- assert.deepEqual(calls.line, [], "there was no body to draw");
179
- assert.deepEqual(calls.top, [{ width: 40, title: "bash", hint: undefined }]);
180
- });
181
-
182
- test("a neon card is not a float card: it is left byte-identical", () => {
183
- const { style, calls } = recordingStyle();
184
- registerCardStyle(style);
185
- setCardStyleSelection(style.id);
186
- const lines = [...NEON_CARD];
187
-
188
- const output = transformCardLines(lines, 40);
189
-
190
- assert.equal(output, lines, "nothing changed, so nothing was copied");
191
- assert.deepEqual(calls.top, [], "and no style was consulted about a card it did not draw");
192
- });
193
-
194
- test("non-card rows between and around cards are returned byte-identical (S3.4)", () => {
195
- const { style } = recordingStyle();
196
- registerCardStyle(style);
197
- setCardStyleSelection(style.id);
198
-
199
- const output = transformCardLines([...CONVERSATION], 40);
200
-
201
- assert.deepEqual(output.slice(0, 2), [PROSE_LINE, BLANK_LINE], "the prefix is byte-identical");
202
- assert.equal(
203
- output.filter((line) => line === BLANK_LINE).length,
204
- CONVERSATION.filter((line) => line === BLANK_LINE).length,
205
- "the blank rows between cards survive, and no new one appears",
206
- );
207
- // Three cards in the conversation fixture, each drawn as top + body + bottom.
208
- assert.deepEqual(
209
- output.filter((line) => line === "-").length,
210
- 3,
211
- "every float card in the frame was reframed",
212
- );
213
- assert.equal(output.at(-1), "-", "the frame ends with the last card's closer, as the host drew it");
214
- assert.equal(output.includes(PROSE_LINE), true);
215
- });
216
-
217
- test("a subtitle is not mistaken for a hint: the whole heading becomes the title", () => {
218
- const { style, calls } = recordingStyle();
219
- registerCardStyle(style);
220
- setCardStyleSelection(style.id);
221
-
222
- transformCardLines([...SUBTITLE_CARD], 40);
223
-
224
- assert.deepEqual(
225
- calls.top,
226
- [{ width: 40, title: "read · config.ts", hint: undefined }],
227
- "the muted run is a subtitle, not a right-aligned hint; no split is guessed",
228
- );
229
- });
230
-
231
- test("a truncated body row keeps its visible text and loses nothing", () => {
232
- const { style, calls } = recordingStyle();
233
- registerCardStyle(style);
234
- setCardStyleSelection(style.id);
235
-
236
- transformCardLines([...TRUNCATED_CARD], 40);
237
-
238
- assert.equal(calls.line.length, 1);
239
- assert.equal(calls.line[0]?.text, "npm run typecheck exits zero and …", "the host's ellipsis stays");
240
- });
241
-
242
- test("only the selected style draws; another registered style is never called (S12)", () => {
243
- const first = recordingStyle("loon.cute", "cute");
244
- const second = recordingStyle("pix.mono", "mono");
245
- registerCardStyle(first.style);
246
- registerCardStyle(second.style);
247
- setCardStyleSelection("pix.mono");
248
-
249
- const output = transformCardLines([...READ_CARD], 40);
250
-
251
- assert.deepEqual(output, ["+read|ctrl+o", "> config.ts", "-"]);
252
- assert.deepEqual(first.calls, { top: [], line: [], bottom: [] }, "the unselected style stayed idle");
253
- assert.equal(second.calls.top.length, 1);
254
- });
255
-
256
- // ---------------------------------------------------------------------------------------------
257
- // Width discipline
258
- // ---------------------------------------------------------------------------------------------
259
-
260
- test("a line wider than the host's width is clipped, never emitted as is (S3)", () => {
261
- const { style } = recordingStyle();
262
- registerCardStyle({
263
- ...style,
264
- frame: {
265
- top: () => "+".repeat(120),
266
- line: (ctx) => `> ${ctx.text} ${"x".repeat(200)}`,
267
- bottom: () => "-".repeat(300),
268
- },
269
- });
270
- setCardStyleSelection(style.id);
271
-
272
- const output = transformCardLines([...READ_CARD], 40);
273
-
274
- const clippedBody = `> config.ts ${"x".repeat(200)}`.slice(0, 40);
275
- assert.deepEqual(output, ["+".repeat(40), clippedBody, "-".repeat(40)]);
276
- for (const line of output) assert.ok(widthOf(line) <= 40, `${widthOf(line)} <= 40`);
277
- });
278
-
279
- test("a style that fits is never padded or rewritten", () => {
280
- const { style } = recordingStyle();
281
- registerCardStyle(style);
282
- setCardStyleSelection(style.id);
283
-
284
- const output = transformCardLines([...READ_CARD], 40);
285
-
286
- assert.deepEqual(output, ["+read|ctrl+o", "> config.ts", "-"], "clipping only ever removes");
287
- });
288
-
289
- // ---------------------------------------------------------------------------------------------
290
- // Failure is a pass-through (S6)
291
- // ---------------------------------------------------------------------------------------------
292
-
293
- test("a throwing top leaves the whole frame identical and is recorded", () => {
294
- registerCardStyle({
295
- id: "loon.broken",
296
- label: "broken",
297
- frame: {
298
- top: () => {
299
- throw new Error("top exploded");
300
- },
301
- line: (ctx) => ctx.text,
302
- bottom: () => "-",
303
- },
304
- });
305
- setCardStyleSelection("loon.broken");
306
- const lines = [...CONVERSATION];
307
-
308
- const output = transformCardLines(lines, 40);
309
-
310
- assert.equal(output, lines, "the original array, not a partially repainted one");
311
- assert.deepEqual(output, [...CONVERSATION]);
312
- assert.deepEqual(getCardStyleTransformFailures(), [
313
- { styleId: "loon.broken", message: "top exploded" },
314
- ]);
315
- });
316
-
317
- test("a throwing line is recorded and passes the frame through", () => {
318
- registerCardStyle({
319
- id: "loon.broken",
320
- label: "broken",
321
- frame: {
322
- top: () => "+",
323
- line: () => {
324
- throw new Error("line exploded");
325
- },
326
- bottom: () => "-",
327
- },
328
- });
329
- setCardStyleSelection("loon.broken");
330
- const lines = [...READ_CARD];
331
-
332
- assert.equal(transformCardLines(lines, 40), lines);
333
- assert.deepEqual(getCardStyleTransformFailures(), [
334
- { styleId: "loon.broken", message: "line exploded" },
335
- ]);
336
- });
337
-
338
- test("a style that threw does not poison the next style (S6, S12)", () => {
339
- registerCardStyle({
340
- id: "loon.broken",
341
- label: "broken",
342
- frame: {
343
- top: () => {
344
- throw new Error("boom");
345
- },
346
- line: (ctx) => ctx.text,
347
- bottom: () => "-",
348
- },
349
- });
350
- const good = recordingStyle("loon.cute", "cute");
351
- registerCardStyle(good.style);
352
- setCardStyleSelection("loon.broken");
353
- transformCardLines([...READ_CARD], 40);
354
-
355
- setCardStyleSelection("loon.cute");
356
-
357
- assert.deepEqual(transformCardLines([...READ_CARD], 40), ["+read|ctrl+o", "> config.ts", "-"]);
358
- assert.equal(good.calls.top.length, 1, "the healthy style drew, in the same process");
359
- });
360
-
361
- test("a failure is remembered across frames until the seam clears it", () => {
362
- registerCardStyle({
363
- id: "loon.broken",
364
- label: "broken",
365
- frame: {
366
- top: () => {
367
- throw new Error("boom");
368
- },
369
- line: (ctx) => ctx.text,
370
- bottom: () => "-",
371
- },
372
- });
373
- setCardStyleSelection("loon.broken");
374
- transformCardLines([...READ_CARD], 40);
375
-
376
- resetCardStyleTransformForTests();
377
-
378
- assert.deepEqual(getCardStyleTransformFailures(), []);
379
- });
@@ -1,258 +0,0 @@
1
- /**
2
- * Wiring the transform to a live `tui` (S5, S4): the path `src/card-styles.ts` proved, armed from the
3
- * one surface that hands this package a real TUI, with the sidebar backend's marker/generation
4
- * discipline.
5
- *
6
- * The fixture models Pi's tree (`test/fake-transcript.ts` documents every citation), so these tests
7
- * prove the real walk: `tui.children[0]` → the document → `children[2]` first, and
8
- * `tui.layoutRoot` → its layout entries → the primary `ScrollView` → its child → `children[2]` as the
9
- * secondary. Every absence is a reported outcome, never a throw and never a guess.
10
- */
11
-
12
- import assert from "node:assert/strict";
13
- import { mkdtempSync, rmSync } from "node:fs";
14
- import { tmpdir } from "node:os";
15
- import { join } from "node:path";
16
- import test from "node:test";
17
-
18
- import { registerCardStyle, resetBusForTests, resetCardStylesForTests } from "../src/bus.ts";
19
- import { getCardStyleLastOutcome, resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
20
- import {
21
- disableCardStyleTransform,
22
- getCardStyleWiringDiagnostics,
23
- installCardStyleTransform,
24
- resetCardStyleWiringForTests,
25
- } from "../src/card-style-wiring.ts";
26
- import { setCardStyleSelection } from "../src/card-styles.ts";
27
- import { armSidebar } from "../src/extension.ts";
28
- import { resetNoticesForTests } from "../src/notices.ts";
29
- import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
30
- import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
31
- import { resetStoreStateForTests } from "../src/store.ts";
32
- import type { CardStyleRegistration } from "../src/types.ts";
33
- import { READ_CARD, createFakeTranscriptTui } from "./fake-transcript.ts";
34
-
35
- let configHome: string;
36
- let previousConfigHome: string | undefined;
37
-
38
- test.before(() => {
39
- configHome = mkdtempSync(join(tmpdir(), "gsb-wiring-"));
40
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
41
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
42
- });
43
-
44
- test.after(() => {
45
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
46
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
47
- rmSync(configHome, { recursive: true, force: true });
48
- });
49
-
50
- test.beforeEach(() => {
51
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
52
- resetBusForTests();
53
- resetCardStylesForTests();
54
- resetCardStyleTransformForTests();
55
- resetCardStyleWiringForTests();
56
- resetSidebarStateForTests();
57
- resetSectionFailuresForTests();
58
- resetNoticesForTests();
59
- resetStoreStateForTests();
60
- disableSidebarBackend();
61
- disableCardStyleTransform();
62
- setCardStyleSelection("native");
63
- });
64
-
65
- const style = (id: string, label: string): CardStyleRegistration => ({
66
- id,
67
- label,
68
- frame: {
69
- top: (ctx) => `+${ctx.title ?? ""}${ctx.hint === undefined ? "" : `|${ctx.hint}`}`,
70
- line: (ctx) => `> ${ctx.text}`,
71
- bottom: () => "-",
72
- },
73
- });
74
-
75
- // ---------------------------------------------------------------------------------------------
76
- // The proven path
77
- // ---------------------------------------------------------------------------------------------
78
-
79
- test("arming wraps the conversation component found through children[0] → document → children[2]", () => {
80
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
81
- const originalRender = tui.chat.render;
82
-
83
- const result = installCardStyleTransform(tui);
84
-
85
- assert.deepEqual(result, { installed: true, outcome: "wrapped", component: "chat" });
86
- assert.notEqual(tui.chat.render, originalRender, "the conversation component is the one wrapped");
87
- assert.deepEqual(getCardStyleWiringDiagnostics(), { wrapped: 1, component: "chat", lastOutcome: "wrapped" });
88
- });
89
-
90
- test("a selected style repaints the live conversation; native is a pass-through", () => {
91
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
92
- installCardStyleTransform(tui);
93
- registerCardStyle(style("loon.cute", "cute"));
94
-
95
- assert.deepEqual(tui.chat.render(40), [...READ_CARD], "native: the host's rows, untouched");
96
- assert.deepEqual(getCardStyleLastOutcome(), { kind: "pass-through", reason: "native" });
97
-
98
- setCardStyleSelection("loon.cute");
99
-
100
- assert.deepEqual(tui.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
101
- assert.deepEqual(getCardStyleLastOutcome(), { kind: "transformed", styleId: "loon.cute", cards: 1 });
102
- });
103
-
104
- test("the layoutRoot hop is the secondary path when children[0] is not the document", () => {
105
- const mounted = createFakeTranscriptTui({ lines: [...READ_CARD] });
106
- // A host whose mounted children are not the document (an older/plainer renderer), but whose
107
- // layout root still carries the transcript (interactive-mode.js:623-629).
108
- const tui = { terminal: {}, children: [], layoutRoot: mounted.layoutRoot };
109
-
110
- const result = installCardStyleTransform(tui);
111
-
112
- assert.deepEqual(result, { installed: true, outcome: "wrapped", component: "chat" });
113
- assert.deepEqual(mounted.chat.render(40), [...READ_CARD], "native through the secondary path");
114
- setCardStyleSelection("loon.cute");
115
- registerCardStyle(style("loon.cute", "cute"));
116
- assert.deepEqual(mounted.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
117
- });
118
-
119
- test("the walk stops at the first shape that is not the document and reports it", () => {
120
- const mounted = createFakeTranscriptTui({ lines: [...READ_CARD] });
121
- // children[0] exists but is not a container, so the primary hop fails and nothing else is there.
122
- const tui = { terminal: {}, children: [{ render: () => [] }] };
123
-
124
- assert.deepEqual(installCardStyleTransform(tui), { installed: false, outcome: "no-document" });
125
- assert.equal(mounted.chat.render, mounted.chat.render, "an unrelated tree is never touched");
126
- });
127
-
128
- // ---------------------------------------------------------------------------------------------
129
- // Every absence is a reported state
130
- // ---------------------------------------------------------------------------------------------
131
-
132
- test("no tui is reported, not thrown", () => {
133
- for (const tui of [undefined, null, 42, "tui", {}]) {
134
- const result = installCardStyleTransform(tui);
135
- assert.equal(result.installed, false, `installed must stay false for ${JSON.stringify(tui)}`);
136
- assert.ok(
137
- ["no-tui", "no-children"].includes(result.outcome),
138
- `unexpected outcome ${result.outcome} for ${JSON.stringify(tui)}`,
139
- );
140
- }
141
- assert.deepEqual(getCardStyleWiringDiagnostics(), { wrapped: 0, lastOutcome: "no-children" });
142
- });
143
-
144
- test("a tui without a document reports no-document", () => {
145
- const result = installCardStyleTransform({ terminal: {}, children: [{}] });
146
-
147
- assert.deepEqual(result, { installed: false, outcome: "no-document" });
148
- assert.equal(getCardStyleWiringDiagnostics().lastOutcome, "no-document");
149
- });
150
-
151
- test("a document without a third child reports no-conversation", () => {
152
- const result = installCardStyleTransform({ terminal: {}, children: [{ children: [{}, {}] }] });
153
-
154
- assert.deepEqual(result, { installed: false, outcome: "no-conversation" });
155
- });
156
-
157
- test("a third child that cannot render reports render-missing", () => {
158
- const result = installCardStyleTransform({
159
- terminal: {},
160
- children: [{ children: [{}, {}, { render: "not a function" }] }],
161
- });
162
-
163
- assert.deepEqual(result, { installed: false, outcome: "render-missing" });
164
- });
165
-
166
- test("a tui whose children accessor throws is reported with its reason, never thrown", () => {
167
- const tui = {
168
- terminal: {},
169
- get children(): never {
170
- throw new Error("accessor exploded");
171
- },
172
- };
173
-
174
- const result = installCardStyleTransform(tui);
175
-
176
- assert.equal(result.installed, false);
177
- assert.equal(result.outcome, "wrong-shape");
178
- assert.match(result.reason ?? "", /accessor exploded/u);
179
- assert.match(getCardStyleWiringDiagnostics().lastReason ?? "", /accessor exploded/u);
180
- });
181
-
182
- // ---------------------------------------------------------------------------------------------
183
- // Idempotency and the generation guard
184
- // ---------------------------------------------------------------------------------------------
185
-
186
- test("a second arm reports already-wrapped and installs nothing new", () => {
187
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
188
- installCardStyleTransform(tui);
189
- const wrapped = tui.chat.render;
190
-
191
- const second = installCardStyleTransform(tui);
192
-
193
- assert.deepEqual(second, { installed: true, outcome: "already-wrapped", component: "chat" });
194
- assert.equal(tui.chat.render, wrapped, "one wrapper, not a stack of them");
195
- assert.equal(getCardStyleWiringDiagnostics().wrapped, 1);
196
- });
197
-
198
- test("after disabling, a wrapper foreign code retained is a pass-through (S5)", () => {
199
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
200
- installCardStyleTransform(tui);
201
- registerCardStyle(style("loon.cute", "cute"));
202
- setCardStyleSelection("loon.cute");
203
- const retained = tui.chat.render;
204
- assert.deepEqual(tui.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
205
-
206
- const restored = disableCardStyleTransform();
207
-
208
- assert.equal(restored, 1);
209
- assert.equal(getCardStyleWiringDiagnostics().wrapped, 0);
210
- assert.deepEqual(retained(40), [...READ_CARD], "the retained wrapper is inert");
211
- assert.deepEqual(tui.chat.render(40), [...READ_CARD], "and the host's own render is back");
212
- });
213
-
214
- // ---------------------------------------------------------------------------------------------
215
- // Stale selection, end to end (S12)
216
- // ---------------------------------------------------------------------------------------------
217
-
218
- test("a disposed style degrades to an untouched frame on the next render (S12)", () => {
219
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
220
- installCardStyleTransform(tui);
221
- const dispose = registerCardStyle(style("loon.cute", "cute"));
222
- setCardStyleSelection("loon.cute");
223
- assert.deepEqual(tui.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
224
-
225
- dispose();
226
-
227
- assert.deepEqual(tui.chat.render(40), [...READ_CARD], "no chrome for a style that no longer exists");
228
- assert.deepEqual(getCardStyleLastOutcome(), { kind: "pass-through", reason: "missing" });
229
- });
230
-
231
- // ---------------------------------------------------------------------------------------------
232
- // Where it arms
233
- // ---------------------------------------------------------------------------------------------
234
-
235
- test("the widget factory that captures the tui is what arms the transform", () => {
236
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
237
- const factoryCalls: unknown[] = [];
238
- const ui = {
239
- setWidget: (key: string, factory: (tuiArg: unknown, theme: unknown) => unknown) => {
240
- factoryCalls.push(key);
241
- factory(tui, {});
242
- },
243
- };
244
-
245
- armSidebar(ui);
246
-
247
- assert.equal(factoryCalls.length, 1, "one factory, as Pi installs it");
248
- assert.equal(getCardStyleWiringDiagnostics().wrapped, 1, "and the factory armed the transform");
249
- registerCardStyle(style("loon.cute", "cute"));
250
- setCardStyleSelection("loon.cute");
251
- assert.deepEqual(tui.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
252
- });
253
-
254
- test("a ui without setWidget arms nothing and does not throw", () => {
255
- assert.doesNotThrow(() => armSidebar({}));
256
- assert.equal(getCardStyleWiringDiagnostics().wrapped, 0);
257
- assert.equal(getCardStyleWiringDiagnostics().lastOutcome, undefined, "never attempted, not failed");
258
- });