gentle-shell-bridge 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +202 -0
  2. package/docs/contract.md +1 -1
  3. package/package.json +13 -1
  4. package/src/api.ts +1 -1
  5. package/.codegraph/.gitignore +0 -5
  6. package/PRD.md +0 -144
  7. package/examples/bash-card.ts +0 -1694
  8. package/gentle-shell-bridge-0.3.0.tgz +0 -0
  9. package/odd/tasks/palette-sections.md +0 -30
  10. package/odd/tasks/plugin-keymap.md +0 -32
  11. package/odd/tasks/plugin-surface-contract.md +0 -55
  12. package/test/api.test.ts +0 -395
  13. package/test/apply-on-move.test.ts +0 -477
  14. package/test/arm.test.ts +0 -220
  15. package/test/bridge-ready.test.ts +0 -309
  16. package/test/bus.test.ts +0 -165
  17. package/test/button-rows.test.ts +0 -409
  18. package/test/capture.test.ts +0 -356
  19. package/test/card-style-rows.test.ts +0 -510
  20. package/test/card-style-transform.test.ts +0 -379
  21. package/test/card-style-wiring.test.ts +0 -258
  22. package/test/card-styles-e2e.test.ts +0 -491
  23. package/test/card-styles.test.ts +0 -324
  24. package/test/card-transcript-path.test.ts +0 -178
  25. package/test/config.test.ts +0 -93
  26. package/test/e2e.test.ts +0 -443
  27. package/test/example-bash-identity.test.ts +0 -372
  28. package/test/example-bash-matrix.test.ts +0 -856
  29. package/test/example-designs.test.ts +0 -734
  30. package/test/example-neon-transform.test.ts +0 -283
  31. package/test/example-plain-style.test.ts +0 -1268
  32. package/test/extension.test.ts +0 -160
  33. package/test/fake-events.ts +0 -58
  34. package/test/fake-host.ts +0 -211
  35. package/test/fake-sidebar.ts +0 -516
  36. package/test/fake-transcript.ts +0 -486
  37. package/test/frame-telemetry.test.ts +0 -417
  38. package/test/hook.test.ts +0 -196
  39. package/test/manifest.test.ts +0 -105
  40. package/test/new-category.test.ts +0 -979
  41. package/test/notices.test.ts +0 -328
  42. package/test/plugin-integration-lines.test.ts +0 -979
  43. package/test/plugin-surface.test.ts +0 -152
  44. package/test/presence.test.ts +0 -459
  45. package/test/rail-surfaces.test.ts +0 -457
  46. package/test/rail-width.test.ts +0 -423
  47. package/test/regression.test.ts +0 -483
  48. package/test/repaint-api.test.ts +0 -200
  49. package/test/reserved-categories.test.ts +0 -361
  50. package/test/row-labels.test.ts +0 -284
  51. package/test/row-names.test.ts +0 -473
  52. package/test/row-origin.test.ts +0 -395
  53. package/test/section-interaction.test.ts +0 -364
  54. package/test/section-panel.test.ts +0 -403
  55. package/test/sections-rows.test.ts +0 -291
  56. package/test/sidebar-backend.test.ts +0 -471
  57. package/test/sidebar-e2e.test.ts +0 -331
  58. package/test/sidebar-hover.test.ts +0 -276
  59. package/test/sidebar-independent.test.ts +0 -349
  60. package/test/sidebar-render.test.ts +0 -281
  61. package/test/sidebar-sections.test.ts +0 -245
  62. package/test/status-card-styles.test.ts +0 -201
  63. package/test/status-section-frame.test.ts +0 -181
  64. package/test/status-sections.test.ts +0 -243
  65. package/test/status.test.ts +0 -209
  66. package/test/store.test.ts +0 -192
  67. package/test/surface-repaint-cost.test.ts +0 -520
  68. package/test/tool-design-chain.test.ts +0 -483
  69. package/test/tool-design-cost.test.ts +0 -473
  70. package/test/tool-design-rows.test.ts +0 -359
  71. package/test/tool-designs.test.ts +0 -745
  72. package/test/transform-pipeline.test.ts +0 -465
  73. package/test/transforms-bus.test.ts +0 -233
  74. package/test/verifier-width-regressions.test.ts +0 -164
  75. package/tsconfig.json +0 -18
@@ -1,465 +0,0 @@
1
- /**
2
- * The transform pipeline (S2, S3, S4, S5): one ordered pass over the rows the conversation
3
- * component produced, run AFTER the card-style repaint, inside the wrapper that already exists.
4
- *
5
- * What these tests hold the pipeline to:
6
- *
7
- * - it is a pass-through by IDENTITY when there is nothing to do (no transform registered, or every
8
- * transform returning its input), so a session that never used the feature is byte-identical;
9
- * - each transform receives the previous transform's output;
10
- * - a throwing (or lying) transform loses only its own contribution, is reported with its id and
11
- * message, and the remaining transforms still run;
12
- * - nothing a transform returns reaches the screen wider than the host's own frame;
13
- * - `/bridge-status` lists the pipeline in order with the LAST frame's outcome per transform, and
14
- * says nothing at all in a session that registered none.
15
- */
16
-
17
- import assert from "node:assert/strict";
18
- import { mkdtempSync, rmSync } from "node:fs";
19
- import { tmpdir } from "node:os";
20
- import { join } from "node:path";
21
- import test from "node:test";
22
-
23
- import {
24
- registerCardStyle,
25
- registerTransform,
26
- resetBusForTests,
27
- resetCardStylesForTests,
28
- resetSectionsForTests,
29
- resetTransformsForTests,
30
- } from "../src/bus.ts";
31
- import { resetCardStyleTransformForTests, hostColumns } from "../src/card-style-transform.ts";
32
- import {
33
- disableCardStyleTransform,
34
- installCardStyleTransform,
35
- resetCardStyleWiringForTests,
36
- } from "../src/card-style-wiring.ts";
37
- import { setCardStyleSelection } from "../src/card-styles.ts";
38
- import { disableCaptureBackend, resetCaptureStateForTests } from "../src/capture.ts";
39
- import { resetHookStateForTests } from "../src/hook.ts";
40
- import { resetNoticesForTests } from "../src/notices.ts";
41
- import { resetRowBuildStateForTests } from "../src/rows.ts";
42
- import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
43
- import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
44
- import { collectStatus, formatStatus } from "../src/status.ts";
45
- import { resetStoreStateForTests } from "../src/store.ts";
46
- import {
47
- applyTransforms,
48
- getTransformFrameOutcomes,
49
- resetTransformPipelineForTests,
50
- } from "../src/transform-pipeline.ts";
51
- import type { CardStyleRegistration, TranscriptTransform } from "../src/types.ts";
52
- import { READ_CARD, createFakeTranscriptTui } from "./fake-transcript.ts";
53
-
54
- let configHome: string;
55
- let previousConfigHome: string | undefined;
56
-
57
- test.before(() => {
58
- configHome = mkdtempSync(join(tmpdir(), "gsb-pipeline-"));
59
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
60
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
61
- });
62
-
63
- test.after(() => {
64
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
65
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
66
- rmSync(configHome, { recursive: true, force: true });
67
- });
68
-
69
- test.beforeEach(() => {
70
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
71
- resetBusForTests();
72
- resetSectionsForTests();
73
- resetCardStylesForTests();
74
- resetTransformsForTests();
75
- resetTransformPipelineForTests();
76
- resetCardStyleTransformForTests();
77
- resetCardStyleWiringForTests();
78
- resetCaptureStateForTests();
79
- resetHookStateForTests();
80
- resetRowBuildStateForTests();
81
- resetSidebarStateForTests();
82
- resetSectionFailuresForTests();
83
- resetNoticesForTests();
84
- resetStoreStateForTests();
85
- disableCaptureBackend();
86
- disableSidebarBackend();
87
- disableCardStyleTransform();
88
- setCardStyleSelection("native");
89
- });
90
-
91
- const statusLines = (): string[] => formatStatus(collectStatus());
92
-
93
- /** The transforms block of the report: from its first line up to the next block that can follow. */
94
- const transformBlock = (lines: readonly string[]): string[] => {
95
- const start = lines.findIndex((line) => line.startsWith("transforms: "));
96
- if (start === -1) return [];
97
- const rest = lines.slice(start);
98
- const end = rest.findIndex((line) => line.startsWith("notices"));
99
- return end === -1 ? rest : rest.slice(0, end);
100
- };
101
-
102
- const style = (id: string, label: string): CardStyleRegistration => ({
103
- id,
104
- label,
105
- frame: {
106
- top: (ctx) => `+${ctx.title ?? ""}`,
107
- line: (ctx) => `> ${ctx.text}`,
108
- bottom: () => "-",
109
- },
110
- });
111
-
112
- // ---------------------------------------------------------------------------------------------
113
- // S2 — one pipeline, in order, pass-through by identity
114
- // ---------------------------------------------------------------------------------------------
115
-
116
- test("with no transform registered the very same array comes back, and nothing is recorded", () => {
117
- const lines = ["one", "two"];
118
-
119
- const output = applyTransforms(lines, 40);
120
-
121
- assert.equal(output, lines, "the identical array instance (S2)");
122
- assert.deepEqual(getTransformFrameOutcomes(), [], "no frame fact to report either");
123
- });
124
-
125
- test("transforms run in order and each one receives the previous output", () => {
126
- const seen: string[][] = [];
127
- registerTransform({
128
- id: "second",
129
- order: 20,
130
- transform: (lines) => {
131
- seen.push([...lines]);
132
- return lines.map((line) => `${line}|2`);
133
- },
134
- });
135
- registerTransform({
136
- id: "first",
137
- order: 10,
138
- transform: (lines) => {
139
- seen.push([...lines]);
140
- return lines.map((line) => `${line}|1`);
141
- },
142
- });
143
-
144
- assert.deepEqual(applyTransforms(["a"], 40), ["a|1|2"]);
145
- assert.deepEqual(seen, [["a"], ["a|1"]], "the second transform saw the first one's rows");
146
- assert.deepEqual(getTransformFrameOutcomes(), [
147
- { id: "first", status: "applied", counts: { lines: 1, candidates: 0, recognised: 0, rewritten: 0, shapes: [] } },
148
- { id: "second", status: "applied", counts: { lines: 1, candidates: 0, recognised: 0, rewritten: 0, shapes: [] } },
149
- ]);
150
- });
151
-
152
- test("a transform that leaves the frame alone is skipped and the array survives by identity", () => {
153
- const lines = ["one"];
154
- registerTransform({ id: "noop", transform: (lines) => lines });
155
- registerTransform({ id: "copy", transform: (lines) => [...lines] });
156
-
157
- const output = applyTransforms(lines, 40);
158
-
159
- assert.equal(output, lines, "an unchanged frame is the same frame");
160
- assert.deepEqual(getTransformFrameOutcomes(), [
161
- { id: "noop", status: "skipped", counts: { lines: 1, candidates: 0, recognised: 0, rewritten: 0, shapes: [] } },
162
- { id: "copy", status: "skipped", counts: { lines: 1, candidates: 0, recognised: 0, rewritten: 0, shapes: [] } },
163
- ]);
164
- });
165
-
166
- test("the frame outcomes are the LAST frame's, not a history", () => {
167
- let paint = true;
168
- registerTransform({
169
- id: "flip",
170
- transform: (lines) => (paint ? lines.map((line) => `${line}!`) : lines),
171
- });
172
-
173
- assert.deepEqual(applyTransforms(["a"], 40), ["a!"]);
174
- assert.deepEqual(getTransformFrameOutcomes(), [
175
- { id: "flip", status: "applied", counts: { lines: 1, candidates: 0, recognised: 0, rewritten: 0, shapes: [] } },
176
- ]);
177
-
178
- paint = false;
179
- assert.deepEqual(applyTransforms(["a"], 40), ["a"]);
180
- assert.deepEqual(getTransformFrameOutcomes(), [
181
- { id: "flip", status: "skipped", counts: { lines: 1, candidates: 0, recognised: 0, rewritten: 0, shapes: [] } },
182
- ]);
183
- });
184
-
185
- // ---------------------------------------------------------------------------------------------
186
- // S3 — isolation
187
- // ---------------------------------------------------------------------------------------------
188
-
189
- test("a throwing transform is skipped for the frame, reported, and the rest still run", () => {
190
- registerTransform({
191
- id: "boom",
192
- order: 10,
193
- transform: () => {
194
- throw new Error("boom exploded");
195
- },
196
- });
197
- registerTransform({
198
- id: "after",
199
- order: 20,
200
- transform: (lines) => lines.map((line) => `${line}!`),
201
- });
202
-
203
- const output = applyTransforms(["a"], 40);
204
-
205
- assert.deepEqual(output, ["a!"], "the frame kept its lines and the later transform ran");
206
- assert.deepEqual(getTransformFrameOutcomes(), [
207
- {
208
- id: "boom",
209
- status: "failed",
210
- message: "boom exploded",
211
- counts: { lines: 1, candidates: 0, recognised: 0, rewritten: 0, shapes: [] },
212
- },
213
- { id: "after", status: "applied", counts: { lines: 1, candidates: 0, recognised: 0, rewritten: 0, shapes: [] } },
214
- ]);
215
- });
216
-
217
- test("a transform that does not return lines loses its contribution instead of the frame", () => {
218
- registerTransform({
219
- id: "junk",
220
- transform: () => "not lines" as unknown as string[],
221
- });
222
- registerTransform({
223
- id: "mixed",
224
- transform: () => ["ok", 42] as unknown as string[],
225
- });
226
-
227
- const output = applyTransforms(["a"], 40);
228
-
229
- assert.deepEqual(output, ["a"], "the previous lines are what the frame keeps");
230
- const outcomes = getTransformFrameOutcomes();
231
- assert.equal(outcomes.length, 2);
232
- for (const outcome of outcomes) {
233
- assert.equal(outcome.status, "failed");
234
- assert.match(outcome.message ?? "", /must return an array of lines/u);
235
- }
236
- });
237
-
238
- test("a registration mutated after it was accepted is reported, never thrown into the frame", () => {
239
- const registration: TranscriptTransform = { id: "mutated", transform: (lines) => lines };
240
- registerTransform(registration);
241
- (registration as unknown as { transform: unknown }).transform = undefined;
242
-
243
- assert.doesNotThrow(() => applyTransforms(["a"], 40));
244
- assert.deepEqual(applyTransforms(["a"], 40), ["a"]);
245
- assert.equal(getTransformFrameOutcomes()[0]?.status, "failed");
246
- });
247
-
248
- // ---------------------------------------------------------------------------------------------
249
- // S4 — width discipline
250
- // ---------------------------------------------------------------------------------------------
251
-
252
- test("what a transform returns is clipped to the host's width, SGR closed", () => {
253
- registerTransform({ id: "wide", transform: () => ["0123456789ABC"] });
254
- assert.deepEqual(applyTransforms(["x"], 10), ["0123456789"]);
255
-
256
- resetTransformsForTests();
257
- registerTransform({
258
- id: "wide-styled",
259
- transform: () => ["\u001b[31m0123456789ABCDEF\u001b[39m"],
260
- });
261
- assert.deepEqual(applyTransforms(["x"], 4), ["\u001b[31m0123\u001b[0m"]);
262
- });
263
-
264
- test("a row that already fits is never touched by the clip", () => {
265
- registerTransform({ id: "exact", transform: () => ["0123456789"] });
266
-
267
- assert.deepEqual(applyTransforms(["x"], 10), ["0123456789"]);
268
- });
269
-
270
- // ---------------------------------------------------------------------------------------------
271
- // S4 — the host's OWN rows survive a transform (W2-1..W2-4, L3)
272
- // ---------------------------------------------------------------------------------------------
273
-
274
- /** An OSC-8 hyperlink, the shape Pi's TUI emits for file paths. */
275
- const hyperlink = (url: string, text: string): string => `\u001b]8;;${url}\u0007${text}\u001b]8;;\u0007`;
276
- const HOST_BG = "\u001b[48;5;236m";
277
- const HOST_RESET = "\u001b[0m";
278
-
279
- /** Rows the host really produces: a link inside a background, ZWJ and modifier emoji, plain text. */
280
- const hostFrame = (): string[] => [
281
- `${HOST_BG}${hyperlink("file:///tmp/config.ts", "config.ts")}${" ".repeat(30)}${HOST_RESET}`,
282
- `${HOST_BG}read this file: 👨‍👩‍👧‍👦 now${" ".repeat(18)}${HOST_RESET}`,
283
- "thumbs 👍🏽 ok",
284
- "plain ascii message",
285
- ];
286
-
287
- test("the host's measure ignores an OSC-8 hyperlink's URL (W2-4)", () => {
288
- assert.equal(hostColumns(hyperlink("file:///tmp/a.ts", "a.ts")), 4);
289
- assert.equal(hostColumns(`${HOST_BG}blue${HOST_RESET}`), 4);
290
- assert.equal(hostColumns("a\tb"), 5, "a TAB is three columns, exactly as the host charges it");
291
- });
292
-
293
- test("a no-op transform leaves the host's own array and its bytes untouched (W2-1, W2-2)", () => {
294
- const lines = hostFrame();
295
- for (const line of lines) assert.ok(hostColumns(line) <= 40, "the fixture fits the width");
296
- registerTransform({ id: "noop", transform: (lines) => lines });
297
-
298
- const output = applyTransforms(lines, 40);
299
-
300
- assert.equal(output, lines, "the host's own array, just as with no transform registered");
301
- assert.deepEqual(output, lines, "and every row byte for byte");
302
- });
303
-
304
- test("a transform that returns an identical copy changes no byte of the host frame (W2-2)", () => {
305
- const lines = hostFrame();
306
- registerTransform({ id: "copy", transform: (lines) => [...lines] });
307
-
308
- const output = applyTransforms(lines, 40);
309
-
310
- assert.equal(output, lines, "an unchanged frame is the same frame");
311
- assert.deepEqual(output, lines);
312
- });
313
-
314
- test("a widened row is cut at the host's width, never inside a cluster or an escape (W2-3)", () => {
315
- const family = "👨‍👩‍👧‍👦";
316
- registerTransform({
317
- id: "wide",
318
- transform: () => [`\u001b[44m${hyperlink("https://example.com", `${family} family 👍🏽 end`)}\u001b[49m`],
319
- });
320
-
321
- const [line] = applyTransforms(["x"], 10);
322
-
323
- assert.equal(
324
- line,
325
- `\u001b[44m\u001b]8;;https://example.com\u0007${family} family \u001b]8;;\u0007\u001b[0m`,
326
- "an open link is closed and the paint reset, the cluster kept whole",
327
- );
328
- assert.equal(hostColumns(line ?? ""), 10, "exactly the host's width, no more");
329
- assert.equal((line ?? "").split("\u200d").length - 1, 3, "every joiner of the family cluster kept");
330
- });
331
-
332
- test("a cut row with a TAB is measured with the host's three columns (W2-3)", () => {
333
- registerTransform({ id: "tab", transform: () => ["aaaaaaaaa\tZZ"] });
334
-
335
- const [line] = applyTransforms(["x"], 10);
336
-
337
- assert.equal(line, "aaaaaaaaa", "the tab does not fit in the last column");
338
- assert.ok(hostColumns(line ?? "") <= 10);
339
- });
340
-
341
- // ---------------------------------------------------------------------------------------------
342
- // S2 — the conversation wrapper runs the repaint first, then the pipeline
343
- // ---------------------------------------------------------------------------------------------
344
-
345
- test("the card-style repaint runs before the transforms, and the transform sees its rows", () => {
346
- const seen: string[][] = [];
347
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
348
- installCardStyleTransform(tui);
349
- registerCardStyle(style("loon.cute", "cute"));
350
- setCardStyleSelection("loon.cute");
351
- registerTransform({
352
- id: "spy",
353
- transform: (lines) => {
354
- seen.push([...lines]);
355
- return lines;
356
- },
357
- });
358
-
359
- assert.deepEqual(tui.chat.render(40), ["+read", "> config.ts", "-"]);
360
- assert.deepEqual(seen, [["+read", "> config.ts", "-"]], "the repaint happened first");
361
- });
362
-
363
- test("a transform restyles rows the host already drew, through the wrapper", () => {
364
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
365
- installCardStyleTransform(tui);
366
- registerTransform({
367
- id: "gutter",
368
- transform: (lines) => lines.map((line) => `|${line}`),
369
- });
370
-
371
- const output = tui.chat.render(40);
372
-
373
- assert.equal(output.length, READ_CARD.length);
374
- assert.ok(output.every((line) => line.startsWith("|")), "every host row went through it");
375
- });
376
-
377
- test("with no style and no transform the wrapper returns the host's own array by identity", () => {
378
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
379
- // The host returns ONE cached array, as Pi's conversation component does between frames.
380
- let cached: string[] | undefined;
381
- const hostRender = tui.chat.render.bind(tui.chat);
382
- tui.chat.render = (width: number): string[] => (cached ??= hostRender(width));
383
- installCardStyleTransform(tui);
384
-
385
- const first = tui.chat.render(40);
386
-
387
- assert.equal(first, cached, "the identical array the host produced");
388
- assert.equal(tui.chat.render(40), first, "and it stays that same instance");
389
- });
390
-
391
- // ---------------------------------------------------------------------------------------------
392
- // S5 — `/bridge-status`
393
- // ---------------------------------------------------------------------------------------------
394
-
395
- test("a session with no transform keeps the historical report byte for byte", () => {
396
- assert.deepEqual(statusLines(), [
397
- "gentle-shell-bridge status",
398
- "providers: none",
399
- "backend: none",
400
- "last capture: none",
401
- "last failures: none",
402
- "store problems: none",
403
- "duplicate settings: no build yet",
404
- ]);
405
- assert.equal(collectStatus().transforms, undefined);
406
- });
407
-
408
- test("the report lists the transforms in pipeline order with the last frame's outcome", () => {
409
- registerTransform({
410
- id: "later",
411
- order: 20,
412
- transform: (lines) => lines.map((line) => `${line}+`),
413
- });
414
- registerTransform({
415
- id: "explodes",
416
- order: 10,
417
- transform: () => {
418
- throw new Error("explodes now");
419
- },
420
- });
421
- registerTransform({ id: "unordered", transform: (lines) => lines });
422
-
423
- applyTransforms(["x"], 40);
424
-
425
- assert.deepEqual(transformBlock(statusLines()), [
426
- "transforms: 3 registered",
427
- " - explodes (order 10): failed (explodes now), 1 line, 0 candidates, 0 recognised, 0 rewritten",
428
- " - later (order 20): applied, 1 line, 0 candidates, 0 recognised, 0 rewritten",
429
- " - unordered (registration order): skipped, 1 line, 0 candidates, 0 recognised, 0 rewritten",
430
- ]);
431
- });
432
-
433
- test("a transform registered after the last frame is reported as not run", () => {
434
- registerTransform({ id: "ran", transform: (lines) => lines });
435
- applyTransforms(["x"], 40);
436
- registerTransform({ id: "later", transform: (lines) => lines });
437
-
438
- assert.deepEqual(transformBlock(statusLines()), [
439
- "transforms: 2 registered",
440
- " - ran (registration order): skipped, 1 line, 0 candidates, 0 recognised, 0 rewritten",
441
- " - later (registration order): not run",
442
- ]);
443
- });
444
-
445
- test("the transforms block sits after every existing block, none of which move", () => {
446
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
447
- installCardStyleTransform(tui);
448
- registerCardStyle(style("loon.cute", "cute"));
449
- setCardStyleSelection("loon.cute");
450
- tui.chat.render(40);
451
- registerTransform({ id: "one", transform: (lines) => lines });
452
- tui.chat.render(40);
453
-
454
- const lines = statusLines();
455
- const cardNote = lines.findIndex((line) => line.startsWith("card style note: "));
456
- const transforms = lines.findIndex((line) => line.startsWith("transforms: "));
457
-
458
- assert.ok(cardNote > 0, "the card-style block is still there");
459
- assert.ok(transforms > cardNote, "and the transforms block follows it");
460
- assert.deepEqual(
461
- lines.slice(0, transforms).filter((line) => line.startsWith("card style frame: ")),
462
- ["card style frame: transformed 1 card"],
463
- "the existing card-style lines are unchanged",
464
- );
465
- });
@@ -1,233 +0,0 @@
1
- /**
2
- * Transcript transforms on the bus (S1): the same entry discipline providers, sections and card
3
- * styles already have — validated id, optional `order`, replace-in-place on the same id, an exact
4
- * disposer, an ordered snapshot, and one process-global list shared by every copy of the package.
5
- *
6
- * The published object is checked here too: `registerTransform` is on it, it is the working
7
- * function (not a stale copy), and the test-only seams stay off `src/index.ts`.
8
- */
9
-
10
- import assert from "node:assert/strict";
11
- import test from "node:test";
12
-
13
- import { BRIDGE_API_KEY, publishBridgeApi, resetBridgeApiForTests } from "../src/api.ts";
14
- import {
15
- registerTransform,
16
- resetBusForTests,
17
- resetTransformsForTests,
18
- snapshotTransforms,
19
- } from "../src/bus.ts";
20
- import type { TransformContext, TranscriptTransform } from "../src/types.ts";
21
-
22
- const withId = (id: string, order?: number): TranscriptTransform => ({
23
- id,
24
- ...(order === undefined ? {} : { order }),
25
- transform: (lines) => lines,
26
- });
27
-
28
- const ids = (): string[] => snapshotTransforms().map((transform) => transform.id);
29
-
30
- test.beforeEach(() => {
31
- resetBridgeApiForTests();
32
- resetBusForTests();
33
- resetTransformsForTests();
34
- });
35
-
36
- // ---------------------------------------------------------------------------------------------
37
- // Ordering, replacement and the disposer
38
- // ---------------------------------------------------------------------------------------------
39
-
40
- test("transforms are returned in registration order", () => {
41
- registerTransform(withId("a"));
42
- registerTransform(withId("b"));
43
- registerTransform(withId("c"));
44
-
45
- assert.deepEqual(ids(), ["a", "b", "c"], "no order: registration order decides");
46
- });
47
-
48
- test("an empty list is an empty snapshot", () => {
49
- assert.deepEqual(snapshotTransforms(), []);
50
- });
51
-
52
- test("transforms with an order come first, ascending, then the unordered ones in registration order", () => {
53
- registerTransform(withId("plain-b"));
54
- registerTransform(withId("late", 30));
55
- registerTransform(withId("plain-a"));
56
- registerTransform(withId("early", 10));
57
- registerTransform(withId("not-a-key", Number.NaN));
58
-
59
- assert.deepEqual(ids(), ["early", "late", "plain-b", "plain-a", "not-a-key"]);
60
- });
61
-
62
- test("registering an existing id replaces it in place and keeps its position", () => {
63
- registerTransform(withId("a"));
64
- registerTransform(withId("b"));
65
- registerTransform(withId("c"));
66
-
67
- const replacement = withId("b", 5);
68
- registerTransform(replacement);
69
-
70
- assert.deepEqual(ids(), ["b", "a", "c"], "one entry, and the order it earned now applies");
71
- assert.equal(snapshotTransforms()[0], replacement, "the new registration is the live one");
72
- });
73
-
74
- test("the replaced transform's disposer does not remove its replacement", () => {
75
- const disposeFirst = registerTransform(withId("a"));
76
- const replacement = withId("a");
77
- registerTransform(replacement);
78
-
79
- disposeFirst();
80
-
81
- assert.deepEqual(ids(), ["a"]);
82
- assert.equal(snapshotTransforms()[0], replacement, "the stale disposer is a no-op");
83
- });
84
-
85
- test("a disposer called twice is a no-op the second time", () => {
86
- registerTransform(withId("a"));
87
- const disposeB = registerTransform(withId("b"));
88
- registerTransform(withId("c"));
89
-
90
- disposeB();
91
- disposeB();
92
-
93
- assert.deepEqual(ids(), ["a", "c"]);
94
- });
95
-
96
- test("a disposer after a reset cannot remove a later registration", () => {
97
- const disposeA = registerTransform(withId("a"));
98
- resetTransformsForTests();
99
- registerTransform(withId("a"));
100
-
101
- disposeA();
102
-
103
- assert.deepEqual(ids(), ["a"], "the pre-reset disposer must not touch the new registration");
104
- });
105
-
106
- test("a snapshot is a stable copy: registering during iteration does not change it", () => {
107
- registerTransform(withId("a"));
108
-
109
- const during = snapshotTransforms();
110
- for (const transform of during) {
111
- assert.equal(transform.id, "a");
112
- registerTransform(withId("late"));
113
- }
114
-
115
- assert.deepEqual(during.map((transform) => transform.id), ["a"]);
116
- assert.deepEqual(ids(), ["a", "late"], "the registry itself did grow");
117
- });
118
-
119
- // ---------------------------------------------------------------------------------------------
120
- // What a registration must satisfy, checked BEFORE anything is stored
121
- // ---------------------------------------------------------------------------------------------
122
-
123
- test("a bad id is rejected with the section/row rule, and nothing is stored", () => {
124
- for (const id of ["", " ", "__proto__", "constructor", "prototype", "a\u0000b", 42, undefined]) {
125
- assert.throws(
126
- () => registerTransform(withId(id as unknown as string)),
127
- /invalid transform id/u,
128
- `id ${JSON.stringify(id)} must be rejected`,
129
- );
130
- }
131
-
132
- assert.deepEqual(ids(), []);
133
- });
134
-
135
- test("a transform that is not a function is rejected and nothing is stored", () => {
136
- assert.throws(
137
- () =>
138
- registerTransform({
139
- id: "loon.bad",
140
- transform: "lines",
141
- } as unknown as TranscriptTransform),
142
- /invalid transform "loon\.bad": transform must be a function/u,
143
- );
144
-
145
- assert.deepEqual(ids(), []);
146
- });
147
-
148
- test("the transform receives the lines and the width the host rendered at", () => {
149
- const seen: Array<{ lines: readonly string[]; width: number }> = [];
150
- registerTransform({
151
- id: "loon.spy",
152
- transform: (lines: readonly string[], ctx: TransformContext): readonly string[] => {
153
- seen.push({ lines, width: ctx.width });
154
- return lines;
155
- },
156
- });
157
-
158
- const registered = snapshotTransforms()[0];
159
- assert.ok(registered !== undefined);
160
- const input = ["one", "two"];
161
- assert.equal(registered.transform(input, { width: 40 }), input);
162
- assert.deepEqual(seen, [{ lines: input, width: 40 }]);
163
- });
164
-
165
- // ---------------------------------------------------------------------------------------------
166
- // Process-global, like every other registration list
167
- // ---------------------------------------------------------------------------------------------
168
-
169
- test("two module copies in one process share one transform list", async () => {
170
- const secondCopySpecifier = "../src/bus.ts?copy=2";
171
- const copyTwo = (await import(secondCopySpecifier)) as typeof import("../src/bus.ts");
172
- assert.notEqual(copyTwo, await import("../src/bus.ts"));
173
-
174
- registerTransform(withId("from-copy-one"));
175
- copyTwo.registerTransform(withId("from-copy-two"));
176
-
177
- assert.deepEqual(ids(), ["from-copy-one", "from-copy-two"]);
178
- assert.deepEqual(
179
- copyTwo.snapshotTransforms().map((transform) => transform.id),
180
- ["from-copy-one", "from-copy-two"],
181
- );
182
- });
183
-
184
- test("the transform list lives on the process-global bus slot, not on module scope", () => {
185
- registerTransform(withId("a"));
186
- const slot = (globalThis as unknown as Record<symbol, unknown>)[
187
- Symbol.for("gentle-shell-bridge.providers")
188
- ];
189
-
190
- assert.ok(typeof slot === "object" && slot !== null);
191
- assert.deepEqual(
192
- (slot as { transforms: Array<{ transform: TranscriptTransform }> }).transforms.map(
193
- (entry) => entry.transform.id,
194
- ),
195
- ["a"],
196
- );
197
- });
198
-
199
- // ---------------------------------------------------------------------------------------------
200
- // The published object and the public entry
201
- // ---------------------------------------------------------------------------------------------
202
-
203
- test("the published API object carries registerTransform, and it is the working function", () => {
204
- const api = publishBridgeApi();
205
- const register = api.registerTransform;
206
-
207
- assert.equal(typeof register, "function", "S1: the transform surface is on the object");
208
- assert.ok(
209
- (globalThis as unknown as Record<symbol, unknown>)[BRIDGE_API_KEY] === api,
210
- "and the slot holds that object",
211
- );
212
-
213
- const dispose = register?.call(api, withId("loon.one")) ?? (() => {});
214
- assert.deepEqual(ids(), ["loon.one"]);
215
-
216
- dispose();
217
- assert.deepEqual(ids(), []);
218
- });
219
-
220
- test("the public entry exports registerTransform and keeps the test seams internal", async () => {
221
- const entry = (await import("../src/index.ts")) as Record<string, unknown>;
222
-
223
- assert.equal(typeof entry.registerTransform, "function");
224
- for (const seam of [
225
- "snapshotTransforms",
226
- "resetTransformsForTests",
227
- "applyTransforms",
228
- "getTransformFrameOutcomes",
229
- "resetTransformPipelineForTests",
230
- ]) {
231
- assert.equal(seam in entry, false, `${seam} must not be public`);
232
- }
233
- });