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,734 +0,0 @@
1
- /**
2
- * T10: `examples/bash-card.ts` as a PLUGIN — the handshake, the sidebar section, and the surfaces
3
- * it contributes.
4
- *
5
- * The example is what a plugin author copies, so what is proven here is its whole contract:
6
- *
7
- * - it reaches the bridge through the symbol and the `gentle-shell-bridge:ready` announcement, with
8
- * no path, no environment variable and no import (both load orders work);
9
- * - it registers its design for `bash`, its `plain` card style and its sidebar section, and its own
10
- * `session_start` handler is the only entry point (the demo row provider is covered here too);
11
- * - the design's `Cards` line appears with no extra wiring, and the section reads the live selection;
12
- * - disposing the load leaves neither surface behind;
13
- * - nothing is ever written to stdout/stderr.
14
- *
15
- * The `plain` card style's own coverage lives in `test/example-plain-style.test.ts`, restored from
16
- * the two suites that were deleted with the style (`5c0c123:test/example-transform.test.ts`,
17
- * `5c0c123:test/example-late-load.test.ts`) and extended with the integration matrix.
18
- */
19
-
20
- import assert from "node:assert/strict";
21
- import { spawnSync } from "node:child_process";
22
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
23
- import { tmpdir } from "node:os";
24
- import { join } from "node:path";
25
- import test from "node:test";
26
-
27
- import {
28
- BRIDGE_API_KEY,
29
- BRIDGE_READY_EVENT,
30
- announceBridgeReady,
31
- bridgeApi,
32
- publishBridgeApi,
33
- resetBridgeApiForTests,
34
- } from "../src/api.ts";
35
- import { resetBusForTests, resetSectionsForTests, snapshotSections } from "../src/bus.ts";
36
- import gentleShellBridge, { type BridgeExtensionApi } from "../src/extension.ts";
37
- import { resetNoticesForTests } from "../src/notices.ts";
38
- import { resetPluginLinesForTests } from "../src/plugin-lines.ts";
39
- import { buildRows, resetRowBuildStateForTests } from "../src/rows.ts";
40
- import { resetStoreStateForTests } from "../src/store.ts";
41
- import {
42
- TOOL_DESIGN_STORE_ID,
43
- getToolDesignSelection,
44
- resetToolDesignsForTests,
45
- setToolDesignSelection,
46
- snapshotToolDesigns,
47
- } from "../src/tool-designs.ts";
48
- import { resetToolDesignRowStateForTests, installToolDesignRows } from "../src/tool-design-rows.ts";
49
- import { resetToolDesignWiringForTests } from "../src/tool-design-wiring.ts";
50
- import type { CustomizeRow, InlineHelpers, SectionRow, SidebarSection } from "../src/types.ts";
51
- import { createFakeEvents, type FakeEvents } from "./fake-events.ts";
52
-
53
- type SessionHandler = (event: unknown, ctx: { hasUI?: boolean; ui?: unknown }) => void;
54
-
55
- interface FakePi {
56
- on?: (event: string, handler: SessionHandler) => void;
57
- events?: FakeEvents;
58
- }
59
-
60
- type ExampleInit = (pi: FakePi) => () => void;
61
-
62
- /** One fresh module instance per scenario: the example keeps nothing on `globalThis`. */
63
- const loadExample = async (tag: string): Promise<ExampleInit> => {
64
- const specifier = new URL(`../examples/bash-card.ts?${tag}`, import.meta.url).href;
65
- return ((await import(specifier)) as { default: ExampleInit }).default;
66
- };
67
-
68
- const hostSlot = (): Record<symbol, unknown> => globalThis as unknown as Record<symbol, unknown>;
69
-
70
- const recordingPi = (events?: FakeEvents): { pi: FakePi; sessionHandlers: SessionHandler[] } => {
71
- const sessionHandlers: SessionHandler[] = [];
72
- const pi: FakePi = {
73
- on(_event, handler) {
74
- sessionHandlers.push(handler);
75
- },
76
- ...(events === undefined ? {} : { events }),
77
- };
78
- return { pi, sessionHandlers };
79
- };
80
-
81
- /** The package loading in the same process: publishing, then announcing itself. */
82
- const loadBridge = (events: FakeEvents): void => {
83
- const pi: BridgeExtensionApi = { on() {}, registerCommand() {}, events };
84
- gentleShellBridge(pi);
85
- };
86
-
87
- const notifySurface = (): {
88
- ui: { notify: (message: string, level?: string) => void };
89
- calls: Array<[string, string | undefined]>;
90
- } => {
91
- const calls: Array<[string, string | undefined]> = [];
92
- return { ui: { notify: (message, level) => calls.push([message, level]) }, calls };
93
- };
94
-
95
- /** How many of `plain` and of the section the example owns right now (the design is checked too). */
96
- const surfaceCounts = (): number[] => [
97
- snapshotToolDesigns("bash").filter((design) => design.id === "bash-card.bash-heading").length,
98
- snapshotSections().filter((section) => section.id === "bash-card.status").length,
99
- ];
100
-
101
- const DESIGN_AND_SECTION = [1, 1];
102
-
103
- /** Every terminal write the process would show: the example must produce none. */
104
- const captureConsole = (): { writes: string[]; restore: () => void } => {
105
- const writes: string[] = [];
106
- const sink = (globalThis as unknown as { console: { log: unknown; warn: unknown; error: unknown } })
107
- .console;
108
- const original = { log: sink.log, warn: sink.warn, error: sink.error };
109
- const record =
110
- (method: string) =>
111
- (...args: unknown[]): void => {
112
- writes.push(`${method}: ${args.map((arg) => String(arg)).join(" ")}`);
113
- };
114
- sink.log = record("log");
115
- sink.warn = record("warn");
116
- sink.error = record("error");
117
- return {
118
- writes,
119
- restore: () => {
120
- sink.log = original.log;
121
- sink.warn = original.warn;
122
- sink.error = original.error;
123
- },
124
- };
125
- };
126
-
127
- const sectionOf = (): SidebarSection => {
128
- const section = snapshotSections().find((candidate) => candidate.id === "bash-card.status");
129
- assert.ok(section !== undefined, "the example registers CARD BASH");
130
- return section;
131
- };
132
-
133
- const context = (): { theme: unknown; configHome: string } => ({ theme: undefined, configHome });
134
-
135
- /** The inline helpers a demo row's action is handed; none of them are used by the trivial demo. */
136
- const inlineHelpers: InlineHelpers = {
137
- input: async () => undefined,
138
- confirm: async () => true,
139
- capture: async () => undefined,
140
- };
141
-
142
- let configHome: string;
143
- let previousConfigHome: string | undefined;
144
- let previousModuleEnv: string | undefined;
145
-
146
- test.before(() => {
147
- configHome = mkdtempSync(join(tmpdir(), "gsb-example-designs-"));
148
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
149
- previousModuleEnv = process.env.GENTLE_SHELL_BRIDGE_MODULE;
150
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
151
- delete process.env.GENTLE_SHELL_BRIDGE_MODULE;
152
- });
153
-
154
- test.after(() => {
155
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
156
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
157
- if (previousModuleEnv === undefined) delete process.env.GENTLE_SHELL_BRIDGE_MODULE;
158
- else process.env.GENTLE_SHELL_BRIDGE_MODULE = previousModuleEnv;
159
- rmSync(configHome, { recursive: true, force: true });
160
- });
161
-
162
- test.beforeEach(() => {
163
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
164
- resetBridgeApiForTests();
165
- resetBusForTests();
166
- resetSectionsForTests();
167
- resetNoticesForTests();
168
- resetPluginLinesForTests();
169
- resetRowBuildStateForTests();
170
- resetStoreStateForTests();
171
- resetToolDesignsForTests();
172
- resetToolDesignRowStateForTests();
173
- resetToolDesignWiringForTests();
174
- });
175
-
176
- // ---------------------------------------------------------------------------------------------
177
- // The handshake: absent, present, and loaded-first
178
- // ---------------------------------------------------------------------------------------------
179
-
180
- test("with no bridge installed the example adds nothing and says so once", async () => {
181
- const captured = captureConsole();
182
- const { pi, sessionHandlers } = recordingPi();
183
- try {
184
- const init = await loadExample("no-bridge");
185
- assert.doesNotThrow(() => init(pi));
186
- } finally {
187
- captured.restore();
188
- }
189
-
190
- assert.deepEqual(captured.writes, [], "nothing lands in the terminal");
191
- assert.equal(sessionHandlers.length, 1, "a session_start handler carries the answer");
192
-
193
- const { ui, calls } = notifySurface();
194
- sessionHandlers[0]({}, { hasUI: true, ui });
195
- sessionHandlers[0]({}, { hasUI: true, ui });
196
- assert.equal(calls.length, 1, "one notice, delivered exactly once");
197
- assert.equal(calls[0][1], "warning");
198
- assert.match(calls[0][0], /no compatible gentle-shell-bridge is installed/u);
199
- assert.match(calls[0][0], /no cross-extension event bus/u);
200
- assert.match(calls[0][0], /adds nothing/u);
201
- assert.deepEqual(surfaceCounts(), [0, 0]);
202
- });
203
-
204
- test("the absent notice waits for a UI and is delivered exactly once", async () => {
205
- const { pi, sessionHandlers } = recordingPi();
206
- (await loadExample("absent-waits-for-ui"))(pi);
207
-
208
- const { ui, calls } = notifySurface();
209
- sessionHandlers[0]({}, { hasUI: false, ui });
210
- sessionHandlers[0]({}, {});
211
- assert.deepEqual(calls, [], "a session without a UI shows nothing");
212
-
213
- sessionHandlers[0]({}, { hasUI: true, ui });
214
- sessionHandlers[0]({}, { hasUI: true, ui });
215
- assert.equal(calls.length, 1, "the buffered message waits for a UI that can show it");
216
- assert.match(calls[0][0], /no compatible gentle-shell-bridge is installed/u);
217
- assert.deepEqual(surfaceCounts(), [0, 0]);
218
- });
219
-
220
- test("an old or foreign bridge is treated as absent, and only through notify", async () => {
221
- hostSlot()[BRIDGE_API_KEY] = { schema: "gentle-shell-bridge/api", version: 0 };
222
- const captured = captureConsole();
223
- const { pi, sessionHandlers } = recordingPi();
224
- try {
225
- (await loadExample("old-bridge"))(pi);
226
- } finally {
227
- captured.restore();
228
- }
229
-
230
- assert.deepEqual(captured.writes, [], "nothing lands in the terminal");
231
- const { ui, calls } = notifySurface();
232
- sessionHandlers[0]({}, { hasUI: true, ui });
233
- assert.equal(calls.length, 1);
234
- assert.match(calls[0][0], /no compatible gentle-shell-bridge is installed/u);
235
- assert.deepEqual(surfaceCounts(), [0, 0]);
236
- assert.equal(hostSlot()[BRIDGE_API_KEY] !== undefined, true, "the slot it cannot use is left alone");
237
- });
238
-
239
- test("the session handler does nothing without a UI, then registers on the next one", async () => {
240
- publishBridgeApi();
241
- const { pi, sessionHandlers } = recordingPi();
242
- (await loadExample("no-ui"))(pi);
243
-
244
- const { ui, calls } = notifySurface();
245
- sessionHandlers[0]({}, { hasUI: false, ui });
246
- sessionHandlers[0]({}, {});
247
- assert.deepEqual(calls, []);
248
- assert.deepEqual(surfaceCounts(), [0, 0]);
249
-
250
- sessionHandlers[0]({}, { hasUI: true, ui });
251
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
252
- });
253
-
254
- test("two session_start events leave one design and one section, not two", async () => {
255
- publishBridgeApi();
256
- const { pi, sessionHandlers } = recordingPi();
257
- (await loadExample("twice"))(pi);
258
- const { ui } = notifySurface();
259
-
260
- sessionHandlers[0]({}, { hasUI: true, ui });
261
- sessionHandlers[0]({}, { hasUI: true, ui });
262
-
263
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
264
- });
265
-
266
- test("the disposer removes both the design and the section", async () => {
267
- publishBridgeApi();
268
- const { pi, sessionHandlers } = recordingPi();
269
- const dispose = (await loadExample("dispose"))(pi);
270
- const { ui } = notifySurface();
271
- sessionHandlers[0]({}, { hasUI: true, ui });
272
-
273
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
274
- dispose();
275
- assert.deepEqual(surfaceCounts(), [0, 0]);
276
- });
277
-
278
- test("an older bridge object without registerToolDesign keeps the section and says absent", async () => {
279
- const published = bridgeApi() ?? publishBridgeApi();
280
- // First-wins: a mixed process can hand back an object that predates the design bus.
281
- hostSlot()[BRIDGE_API_KEY] = {
282
- schema: published.schema,
283
- version: published.version,
284
- VERSION: published.VERSION,
285
- registerRows: published.registerRows,
286
- registerSection: published.registerSection,
287
- createStore: published.createStore,
288
- };
289
-
290
- const { pi, sessionHandlers } = recordingPi();
291
- const dispose = (await loadExample("old-object"))(pi);
292
- const { ui, calls } = notifySurface();
293
- sessionHandlers[0]({}, { hasUI: true, ui });
294
-
295
- assert.deepEqual(calls, [], "a surface the bridge never promised is not warned about");
296
- assert.deepEqual(snapshotToolDesigns("bash"), [], "and no design is registered");
297
- assert.deepEqual(surfaceCounts(), [0, 1], "the section still registers");
298
- assert.equal(sectionOf().lines(context())[0], "resolver absent", "the resolver report is now the first body row");
299
-
300
- dispose();
301
- });
302
-
303
- // ---------------------------------------------------------------------------------------------
304
- // The rows and the section: what the user reads
305
- // ---------------------------------------------------------------------------------------------
306
-
307
- test("the Cards line appears with no extra wiring, and the section reads the live selection", async () => {
308
- publishBridgeApi();
309
- const { pi, sessionHandlers } = recordingPi();
310
- (await loadExample("live-state"))(pi);
311
- const { ui } = notifySurface();
312
- sessionHandlers[0]({}, { hasUI: true, ui });
313
-
314
- installToolDesignRows();
315
- const rows: CustomizeRow[] = buildRows({ theme: undefined, configHome }).rows;
316
- const designLine = rows.filter((row) => isBashDesignSetting(row.choice?.setting));
317
- assert.deepEqual(
318
- designLine.map((row) => row.choice?.value),
319
- ["native", "bash heading"],
320
- "the tool's own card is the default, the design is the other value",
321
- );
322
-
323
- const section = sectionOf();
324
- assert.equal(section.title, "CARD BASH");
325
- assert.equal(section.rowLabel, "card bash");
326
- assert.equal(section.meta?.(context()), "native", "the header count is the live selection");
327
-
328
- const lines = section.lines(context());
329
- assert.equal(
330
- lines.some((line) => typeof line !== "string" && line.label === "selected"),
331
- false,
332
- "the live selection rides the header meta, never a duplicate body row",
333
- );
334
- assert.equal(lines[0], "resolver registered", "a plain string row: the design is on the bus");
335
- const presses = lines.find(
336
- (line): line is SectionRow => typeof line !== "string" && line.label === "demo presses",
337
- );
338
- assert.equal(presses?.onClick === undefined, false, "the section's one action row is the demo");
339
-
340
- setToolDesignSelection("bash", "bash-card.bash-heading");
341
-
342
- assert.equal(section.meta?.(context()), "bash heading", "the same section object reads the value");
343
- assert.equal(
344
- section
345
- .lines(context())
346
- .some((line) => typeof line !== "string" && line.label === "selected"),
347
- false,
348
- "and selecting a design still adds no second copy of the value",
349
- );
350
-
351
- // The selection lives in the BRIDGE's own file, not in one of the example's.
352
- const stored = JSON.parse(
353
- readFileSync(join(configHome, "gentle-shell-bridge", `${TOOL_DESIGN_STORE_ID}.json`), "utf8"),
354
- );
355
- assert.deepEqual(stored.values.selections, { bash: "bash-card.bash-heading" });
356
- assert.equal(getToolDesignSelection("bash"), "bash-card.bash-heading");
357
-
358
- const settings = rows.map((row) => row.choice?.setting).filter((setting) => setting !== undefined);
359
- assert.equal(
360
- settings.some((setting) => /bash-card\./u.test(setting)),
361
- false,
362
- "no dotted id reaches a row name",
363
- );
364
- });
365
-
366
- // ---------------------------------------------------------------------------------------------
367
- // The demo category: a toggle and a button in a category the plugin invents
368
- // ---------------------------------------------------------------------------------------------
369
-
370
- const DEMO_CATEGORY = "Card bash";
371
-
372
- /** The tool-design line's setting, old bridge (`bash`) and current bridge (`Card bash`). */
373
- const isBashDesignSetting = (setting: string | undefined): boolean =>
374
- setting === "bash" || setting === "Card bash";
375
-
376
- /** The category's rows, through the real provider and the real build. */
377
- const demoRows = (): CustomizeRow[] =>
378
- buildRows({ theme: undefined, configHome }).rows.filter((row) => row.category === DEMO_CATEGORY);
379
-
380
- const sectionStrings = (): string[] =>
381
- sectionOf()
382
- .lines(context())
383
- .filter((line): line is string => typeof line === "string");
384
-
385
- /** The `demo presses` ACTION row's live meta, read through the real section. */
386
- const pressesMeta = (): string | undefined =>
387
- sectionOf()
388
- .lines(context())
389
- .find(
390
- (line): line is SectionRow => typeof line !== "string" && line.label === "demo presses",
391
- )?.meta;
392
-
393
- const demoStoreValues = (): Record<string, unknown> => {
394
- const document = JSON.parse(
395
- readFileSync(join(configHome, "gentle-shell-bridge", "bash-card.json"), "utf8"),
396
- ) as { values: Record<string, unknown> };
397
- return document.values;
398
- };
399
-
400
- test("a plugin adds its OWN category with a toggle and a button, through one provider", async () => {
401
- publishBridgeApi();
402
- const { pi, sessionHandlers } = recordingPi();
403
- (await loadExample("demo-category"))(pi);
404
- const { ui } = notifySurface();
405
- sessionHandlers[0]({}, { hasUI: true, ui });
406
-
407
- const rows = demoRows();
408
- assert.deepEqual(
409
- rows.map((row) => row.label),
410
- ["Demo toggle", "Demo toggle", "Demo button"],
411
- "a two-value toggle (one line) and a plain button, through one provider",
412
- );
413
-
414
- const [on, off, button] = rows;
415
- assert.equal(on?.choice?.setting, "Demo toggle");
416
- assert.equal(off?.choice?.setting, "Demo toggle");
417
- assert.deepEqual([on?.choice?.value, off?.choice?.value], ["on", "off"]);
418
- assert.equal(button?.choice, undefined, "the button carries no choice");
419
- assert.equal(typeof button?.action, "function", "Enter can run it");
420
-
421
- // The category is created by the row, exactly once, and the default is `off`.
422
- const categories = [...new Set(buildRows({ theme: undefined, configHome }).rows.map((row) => row.category))];
423
- assert.equal(categories.filter((category) => category === DEMO_CATEGORY).length, 1);
424
- assert.equal(on?.choice?.current(), false, "the stored default is off");
425
- assert.equal(off?.choice?.current(), true);
426
-
427
- for (const row of rows) {
428
- const preview = row.preview?.();
429
- assert.ok(preview !== undefined, "every demo row depicts itself");
430
- assert.equal([...preview.title].length < 40, true, "a short literal title");
431
- for (const line of preview.sample.split("\n")) {
432
- assert.equal([...line].length < 40, true, "a short literal sample row");
433
- }
434
- }
435
-
436
- const section = sectionStrings();
437
- assert.equal(section.includes("demo toggle off"), true, "the sidebar shows the live value");
438
- assert.equal(
439
- pressesMeta(),
440
- "0",
441
- "the clickable `demo presses` row carries the live count as its right-aligned meta",
442
- );
443
- });
444
-
445
- test("choosing the demo toggle persists it and the sidebar shows the new value", async () => {
446
- publishBridgeApi();
447
- const { pi, sessionHandlers } = recordingPi();
448
- (await loadExample("demo-toggle"))(pi);
449
- const { ui } = notifySurface();
450
- sessionHandlers[0]({}, { hasUI: true, ui });
451
-
452
- const onRow = demoRows().find(
453
- (row) => row.choice?.setting === "Demo toggle" && row.choice.value === "on",
454
- );
455
- assert.ok(onRow?.action !== undefined);
456
- await onRow.action(inlineHelpers);
457
-
458
- assert.equal(onRow.choice?.current(), true, "the same row reads the new value");
459
- assert.equal(sectionStrings().includes("demo toggle on"), true, "the sidebar shows it live");
460
- assert.equal(demoStoreValues().demoToggle, "on", "and it is persisted in the example's own store");
461
- });
462
-
463
- test("the demo button counts its presses in the section, through the example's store", async () => {
464
- publishBridgeApi();
465
- const { pi, sessionHandlers } = recordingPi();
466
- (await loadExample("demo-button"))(pi);
467
- const { ui } = notifySurface();
468
- sessionHandlers[0]({}, { hasUI: true, ui });
469
-
470
- const button = demoRows().find((row) => row.label === "Demo button");
471
- assert.ok(button?.action !== undefined);
472
- await button.action(inlineHelpers);
473
- await button.action(inlineHelpers);
474
-
475
- assert.equal(pressesMeta(), "2", "Enter increments the counter");
476
- assert.equal(demoStoreValues().demoPresses, 2, "and the count is persisted");
477
- });
478
-
479
- test("a second registration replaces the demo provider instead of stacking a category", async () => {
480
- publishBridgeApi();
481
- const { pi, sessionHandlers } = recordingPi();
482
- (await loadExample("demo-reload-1"))(pi);
483
- const { ui } = notifySurface();
484
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
485
-
486
- const onRow = demoRows().find(
487
- (row) => row.choice?.setting === "Demo toggle" && row.choice.value === "on",
488
- );
489
- const button = demoRows().find((row) => row.label === "Demo button");
490
- assert.ok(onRow?.action !== undefined && button?.action !== undefined);
491
- await onRow.action(inlineHelpers);
492
- await button.action(inlineHelpers);
493
-
494
- // A `/reload` loads the file again: the same provider id must replace, not duplicate.
495
- (await loadExample("demo-reload-2"))(pi);
496
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
497
-
498
- assert.equal(demoRows().length, 3, "one category, one toggle line, one button");
499
- const section = sectionStrings();
500
- assert.equal(section.includes("demo toggle on"), true, "the stored toggle survives the reload");
501
- assert.equal(pressesMeta(), "1", "and so does the press count");
502
- });
503
-
504
- test("an older bridge without registerRows keeps the design and section, and adds no category", async () => {
505
- const published = bridgeApi() ?? publishBridgeApi();
506
- hostSlot()[BRIDGE_API_KEY] = {
507
- schema: published.schema,
508
- version: published.version,
509
- VERSION: published.VERSION,
510
- registerToolDesign: published.registerToolDesign,
511
- registerSection: published.registerSection,
512
- createStore: published.createStore,
513
- };
514
-
515
- const { pi, sessionHandlers } = recordingPi();
516
- // `registerRows` is absent from this object: the example must degrade, not throw.
517
- const dispose = (await loadExample("demo-old-bridge"))(pi);
518
- const { ui } = notifySurface();
519
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
520
-
521
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION, "the promised surfaces still register");
522
- assert.deepEqual(demoRows(), [], "a surface the bridge never promised adds nothing");
523
- dispose();
524
- });
525
-
526
- test("the design's own preview depicts the card in that design", async () => {
527
- publishBridgeApi();
528
- const { pi, sessionHandlers } = recordingPi();
529
- (await loadExample("preview"))(pi);
530
- const { ui } = notifySurface();
531
- sessionHandlers[0]({}, { hasUI: true, ui });
532
-
533
- const [design] = snapshotToolDesigns("bash");
534
- assert.ok(design !== undefined);
535
- const preview = design.preview?.();
536
- assert.ok(preview !== undefined, "the design depicts itself");
537
- assert.match(preview.title, /bash heading/u);
538
- assert.match(preview.sample, /╭─ bash npm test/u, "the sample heads the card with the word bash");
539
-
540
- installToolDesignRows();
541
- const row = buildRows({ theme: undefined, configHome }).rows.find(
542
- (candidate) => candidate.choice?.value === "bash heading",
543
- );
544
- assert.deepEqual(row?.preview?.(), preview, "the value shows the design's own preview");
545
- });
546
-
547
- // ---------------------------------------------------------------------------------------------
548
- // The load order Pi really produces: the plugin runs first
549
- // ---------------------------------------------------------------------------------------------
550
-
551
- test("a plugin loaded before the bridge registers when the announcement arrives", async () => {
552
- const bus = createFakeEvents();
553
- const { pi, sessionHandlers } = recordingPi(bus);
554
- (await loadExample("late-announcement"))(pi);
555
-
556
- assert.deepEqual(surfaceCounts(), [0, 0], "nothing registers at module load");
557
- assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 1, "the early loader is listening");
558
-
559
- loadBridge(bus);
560
-
561
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION, "the announcement served the early loader");
562
- assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 0, "and it stopped listening afterwards");
563
-
564
- const { ui } = notifySurface();
565
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
566
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION, "a reload adds no second copy");
567
- });
568
-
569
- test("the announcement alone is enough: a late bridge registers with no session_start", async () => {
570
- const bus = createFakeEvents();
571
- const { pi, sessionHandlers } = recordingPi(bus);
572
- (await loadExample("late-no-session"))(pi);
573
-
574
- publishBridgeApi();
575
- announceBridgeReady(bus);
576
-
577
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
578
- assert.equal(sessionHandlers.length, 1, "the handler is still there for the next session");
579
-
580
- const { ui, calls } = notifySurface();
581
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
582
- assert.deepEqual(calls, [], "no notice: nothing is pending");
583
- });
584
-
585
- test("session_start before and after the announcement leaves exactly one of each", async () => {
586
- const bus = createFakeEvents();
587
- const { pi, sessionHandlers } = recordingPi(bus);
588
- (await loadExample("late-both"))(pi);
589
- const { ui } = notifySurface();
590
-
591
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
592
- assert.deepEqual(surfaceCounts(), [0, 0], "no bridge yet: nothing to register with");
593
-
594
- loadBridge(bus);
595
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
596
-
597
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
598
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION, "the later session_start adds no duplicate");
599
- });
600
-
601
- test("a plugin loaded after the bridge registers from session_start and leaves no listener", async () => {
602
- const bus = createFakeEvents();
603
- loadBridge(bus);
604
-
605
- const { pi, sessionHandlers } = recordingPi(bus);
606
- (await loadExample("early-bridge"))(pi);
607
-
608
- assert.equal(
609
- bus.listenerCount(BRIDGE_READY_EVENT),
610
- 0,
611
- "the API was already published: nothing to wait for, so nothing is subscribed",
612
- );
613
- assert.deepEqual(surfaceCounts(), [0, 0], "and still nothing at module load");
614
-
615
- const { ui, calls } = notifySurface();
616
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
617
- assert.deepEqual(calls, []);
618
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
619
- });
620
-
621
- test("disposing before the announcement drops the listener and registers nothing", async () => {
622
- const bus = createFakeEvents();
623
- const { pi } = recordingPi(bus);
624
- const dispose = (await loadExample("late-dispose"))(pi);
625
-
626
- assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 1);
627
- dispose();
628
- assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 0, "a disposed load hears nothing");
629
-
630
- loadBridge(bus);
631
- assert.deepEqual(surfaceCounts(), [0, 0]);
632
- });
633
-
634
- test("an announcement backed by no published object registers nothing", async () => {
635
- const bus = createFakeEvents();
636
- const { pi } = recordingPi(bus);
637
- (await loadExample("late-foreign"))(pi);
638
-
639
- bus.emit(BRIDGE_READY_EVENT, { schema: "someone-else/api", version: 1 });
640
-
641
- assert.deepEqual(surfaceCounts(), [0, 0], "the symbol decides, never the payload");
642
- assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 1, "and the example is still waiting");
643
- });
644
-
645
- // ---------------------------------------------------------------------------------------------
646
- // The two honest notices
647
- // ---------------------------------------------------------------------------------------------
648
-
649
- test("with a bus but no bridge yet the notice says it is waiting for the announcement", async () => {
650
- const bus = createFakeEvents();
651
- const { pi, sessionHandlers } = recordingPi(bus);
652
- (await loadExample("notice-waiting"))(pi);
653
-
654
- const { ui, calls } = notifySurface();
655
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
656
-
657
- assert.equal(calls.length, 1);
658
- const message = calls[0][0];
659
- assert.match(message, /waiting for the "gentle-shell-bridge:ready" announcement/u);
660
- assert.match(message, /registers the bash design/u);
661
- assert.match(message, /the CARD BASH sidebar section/u);
662
- assert.match(message, /if it is not installed, or the published object is incompatible, nothing arrives/u);
663
- assert.doesNotMatch(message, /no compatible gentle-shell-bridge is installed/u);
664
- });
665
-
666
- test("a waiting notice is withdrawn once the announcement registers the surfaces", async () => {
667
- const bus = createFakeEvents();
668
- const { pi, sessionHandlers } = recordingPi(bus);
669
- (await loadExample("notice-withdrawn"))(pi);
670
-
671
- for (const handler of sessionHandlers) handler({}, { hasUI: false });
672
- const { ui, calls } = notifySurface();
673
- for (const handler of sessionHandlers) handler({}, { hasUI: false, ui });
674
- assert.deepEqual(calls, [], "a session without a UI shows nothing");
675
-
676
- loadBridge(bus);
677
- assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
678
-
679
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
680
- assert.deepEqual(calls, [], "the surfaces registered, so the waiting notice is no longer true");
681
- });
682
-
683
- // ---------------------------------------------------------------------------------------------
684
- // Nothing outside the TUI
685
- // ---------------------------------------------------------------------------------------------
686
-
687
- test("the example's runtime path resolves no module of the bridge", () => {
688
- const source = readFileSync(new URL("../examples/bash-card.ts", import.meta.url), "utf8");
689
-
690
- assert.equal(/GENTLE_SHELL_BRIDGE_MODULE/u.test(source), false, "no environment-variable override");
691
- assert.equal(/bridgeCandidates/u.test(source), false, "no path probing");
692
- assert.equal(/await\s+import\s*\(/u.test(source), false, "nothing is imported at runtime");
693
- assert.equal(
694
- /from\s+["']gentle-shell-bridge["']/u.test(source),
695
- false,
696
- "the package is never imported, not even for types",
697
- );
698
- assert.match(source, /Symbol\.for\("gentle-shell-bridge\/api\/v1"\)/u);
699
- assert.equal(/console\./u.test(source), false, "no console write survives in the example");
700
- assert.match(source, /pi\.on\("session_start"/u, "registration happens in session_start");
701
- assert.match(source, /gentle-shell-bridge:ready/u, "a later bridge is heard on the announcement");
702
- assert.match(source, /pi\.events/u, "the channel comes from Pi's own bus, not a module");
703
- assert.match(source, /registerToolDesign/u, "the design is the surface it registers");
704
- assert.equal(/registerTransform/u.test(source), false, "there is no second transcript grammar");
705
- assert.equal(/registerToolRenderer\s*\(/u.test(source), false, "the example claims no resolver slot");
706
- });
707
-
708
- test("the absent case writes zero bytes to stdout and stderr", () => {
709
- const specifier = new URL("../examples/bash-card.ts?zero-bytes", import.meta.url).href;
710
- const script = `
711
- import example from ${JSON.stringify(specifier)};
712
- const handlers = [];
713
- const pi = {
714
- on(event, handler) { handlers.push(handler); },
715
- };
716
- example(pi);
717
- for (const handler of handlers) handler({}, { hasUI: true, ui: { notify() {} } });
718
- `;
719
- const captured = captureConsole();
720
- let result: ReturnType<typeof spawnSync>;
721
- try {
722
- result = spawnSync(process.execPath, ["--input-type=module", "-e", script], {
723
- cwd: new URL("..", import.meta.url),
724
- encoding: "utf8",
725
- env: { ...process.env, GENTLE_PI_CONFIG_HOME: configHome },
726
- });
727
- } finally {
728
- captured.restore();
729
- }
730
-
731
- assert.equal(result.status, 0, `the example ran (stderr: ${String(result.stderr)})`);
732
- assert.equal(result.stdout, "", "no stdout bytes");
733
- assert.equal(result.stderr, "", "no stderr bytes");
734
- });