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,491 +0,0 @@
1
- /**
2
- * End to end over the public surface (S8, S11, S12): a plugin's whole world.
3
- *
4
- * Nothing here imports a backend. Styles are registered through `bridgeApi().registerCardStyle`,
5
- * the host's menu is the fake host the earlier suites use, the "conversation" is the fake transcript
6
- * the T1 fixtures pin, and the only bridge internals read are teardown seams plus the diagnostics
7
- * `/bridge-status` itself prints — the same shape `test/e2e.test.ts` uses for rows and sections.
8
- *
9
- * What a plugin cannot see, and therefore what this suite proves by construction: the splice into the
10
- * host's line, the wrapping of `current()`/`action()`, the transform wrapper and the status block are
11
- * all invisible to it. It calls one function and a user clicks.
12
- */
13
-
14
- import assert from "node:assert/strict";
15
- import { mkdtempSync, rmSync } from "node:fs";
16
- import { tmpdir } from "node:os";
17
- import { join } from "node:path";
18
- import test from "node:test";
19
-
20
- // The public surface, exactly as a plugin reaches it.
21
- import {
22
- bridgeApi,
23
- registerRows,
24
- type BridgeApi,
25
- type CardStyleRegistration,
26
- type RowProvider,
27
- } from "../src/index.ts";
28
- import gentleShellBridge, {
29
- type BridgeCommandHandler,
30
- type BridgeExtensionApi,
31
- type BridgeSessionHandler,
32
- } from "../src/extension.ts";
33
- // Teardown-only seams; the API-surface test proves they are not public.
34
- import {
35
- resetBusForTests,
36
- resetCardStylesForTests,
37
- resetSectionsForTests,
38
- snapshotProviders,
39
- } from "../src/bus.ts";
40
- import { resetCardStyleRowStateForTests } from "../src/card-style-rows.ts";
41
- import { resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
42
- import {
43
- disableCardStyleTransform,
44
- getCardStyleWiringDiagnostics,
45
- resetCardStyleWiringForTests,
46
- } from "../src/card-style-wiring.ts";
47
- import { resetCaptureStateForTests } from "../src/capture.ts";
48
- import { resetHookStateForTests } from "../src/hook.ts";
49
- import { resetNoticesForTests } from "../src/notices.ts";
50
- import { resetRowBuildStateForTests } from "../src/rows.ts";
51
- import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
52
- import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
53
- import { resetStoreStateForTests } from "../src/store.ts";
54
- import { createFakeHost, type FakeHost, type FakeRow } from "./fake-host.ts";
55
- import { READ_CARD, createFakeTranscriptTui, type FakeTui } from "./fake-transcript.ts";
56
-
57
- const FALLBACK_SETTING = "Bridge card style";
58
- const HOST_SETTING = "Card style";
59
-
60
- let configHome: string;
61
- let previousConfigHome: string | undefined;
62
-
63
- test.before(() => {
64
- configHome = mkdtempSync(join(tmpdir(), "gsb-cards-e2e-"));
65
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
66
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
67
- });
68
-
69
- test.after(() => {
70
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
71
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
72
- rmSync(configHome, { recursive: true, force: true });
73
- });
74
-
75
- /** `native` before the first test, and between tests: the selection is a real file. */
76
- const resetSelection = (): void => {
77
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
78
- };
79
-
80
- test.beforeEach(() => {
81
- resetSelection();
82
- resetBusForTests();
83
- resetSectionsForTests();
84
- resetCardStylesForTests();
85
- resetCardStyleRowStateForTests();
86
- resetCardStyleTransformForTests();
87
- resetCardStyleWiringForTests();
88
- resetCaptureStateForTests();
89
- resetHookStateForTests();
90
- resetRowBuildStateForTests();
91
- resetStoreStateForTests();
92
- resetNoticesForTests();
93
- resetSidebarStateForTests();
94
- resetSectionFailuresForTests();
95
- disableCardStyleTransform();
96
- disableSidebarBackend();
97
- });
98
-
99
- // ---------------------------------------------------------------------------------------------
100
- // The harness: one `ctx.ui` that is both a customize host and a widget host, as Pi's is.
101
- // ---------------------------------------------------------------------------------------------
102
-
103
- interface Entry {
104
- sessionHandlers: BridgeSessionHandler[];
105
- commands: Array<{ name: string; description?: string; handler: BridgeCommandHandler }>;
106
- }
107
-
108
- /** Load the real entry against a recording API, as Pi would. */
109
- const loadEntry = (): Entry => {
110
- const sessionHandlers: BridgeSessionHandler[] = [];
111
- const commands: Entry["commands"] = [];
112
- const pi: BridgeExtensionApi = {
113
- // `gentle-pi` is present, so `session_start` arms every surface (S9/T12).
114
- getCommands: () => [{ name: "gentle:customize" }],
115
- on(_event, handler) {
116
- sessionHandlers.push(handler);
117
- },
118
- registerCommand(name, command) {
119
- commands.push({ name, description: command.description, handler: command.handler });
120
- },
121
- };
122
- gentleShellBridge(pi);
123
- return { sessionHandlers, commands };
124
- };
125
-
126
- /**
127
- * One `ctx.ui` carrying the customize surface and the widget surface Pi really has.
128
- *
129
- * It augments the fake host's OWN object on purpose: the host builds its menu by calling
130
- * `ui.custom(...)` on that object, so a copy would leave the capture wrapper behind and the splice
131
- * would never see the rows. Pi has one object with both members, and so does this.
132
- */
133
- const combinedUi = (host: FakeHost, tui: unknown, notices: string[]): unknown => {
134
- const ui = host.ui as Record<string, unknown>;
135
- ui.notify = (message: string) => notices.push(message);
136
- ui.setWidget = (_key: string, factory: (tuiArg: unknown, theme: unknown) => unknown) =>
137
- factory(tui, { name: "fake-theme" });
138
- return ui;
139
- };
140
-
141
- const startSession = (entry: Entry, ui: unknown): void => {
142
- for (const handler of entry.sessionHandlers) handler({}, { hasUI: true, ui });
143
- };
144
-
145
- const printStatus = (entry: Entry): string => {
146
- const printed: string[] = [];
147
- entry.commands[0].handler("", { ui: { notify: (message: string) => printed.push(message) } });
148
- return printed.join("\n");
149
- };
150
-
151
- const settingLine = (lines: readonly string[], setting: string): string | undefined =>
152
- lines.find((line) => line.startsWith(`${setting} `));
153
-
154
- /** A style whose chrome is a single readable marker, with a call counter per hook. */
155
- const markerStyle = (
156
- id: string,
157
- label: string,
158
- calls: string[],
159
- extra: Partial<CardStyleRegistration> = {},
160
- ): CardStyleRegistration => ({
161
- id,
162
- label,
163
- ...extra,
164
- frame: {
165
- top: (ctx) => {
166
- calls.push(`${id}:top`);
167
- return `[${label}] ${ctx.title ?? ""}${ctx.hint === undefined ? "" : ` ${ctx.hint}`}`;
168
- },
169
- line: (ctx) => {
170
- calls.push(`${id}:line`);
171
- return `<${label}> ${ctx.text}`;
172
- },
173
- bottom: () => {
174
- calls.push(`${id}:bottom`);
175
- return `[/${label}]`;
176
- },
177
- },
178
- });
179
-
180
- /** The host's own menu, with its `Card style` line inside `Cards`. */
181
- const hostMenu = (): FakeRow[] => [
182
- { category: "Cards", label: "Card bash: builtin", action: () => {} },
183
- {
184
- category: "Cards",
185
- label: () => "Card style: neon",
186
- action: () => {},
187
- choice: { setting: HOST_SETTING, value: "neon", current: () => false, hint: "How cards are drawn" },
188
- },
189
- {
190
- category: "Cards",
191
- label: () => "Card style: float",
192
- action: () => {},
193
- choice: { setting: HOST_SETTING, value: "float", current: () => true, hint: "How cards are drawn" },
194
- },
195
- { category: "Layout", label: "Layout: builtin", action: () => {} },
196
- ];
197
-
198
- interface World {
199
- host: FakeHost;
200
- tui: FakeTui;
201
- entry: Entry;
202
- notices: string[];
203
- /** The array the host's own conversation render returns, so identity is observable. */
204
- hostLines: () => readonly string[];
205
- }
206
-
207
- /**
208
- * A whole session: the host's menu, a Pi-shaped `tui`, the real entry armed through `session_start`,
209
- * and the conversation component cached so "the identical array" can be asserted.
210
- */
211
- function startWorld(options: { rows?: FakeRow[]; tui?: FakeTui } = {}): World {
212
- const host = createFakeHost({ rows: options.rows ?? hostMenu() });
213
- const tui = options.tui ?? createFakeTranscriptTui({ lines: [...READ_CARD] });
214
- // Cache the host's render output and return that instance on every call: the transform wrapper
215
- // returns it unchanged when it does not repaint, which is exactly what S3 promises ("the same
216
- // array reference") and what a live host would see.
217
- let cached: string[] | undefined;
218
- const render = tui.chat.render.bind(tui.chat);
219
- tui.chat.render = (width: number): string[] => (cached ??= render(width));
220
- const notices: string[] = [];
221
- const ui = combinedUi(host, tui, notices);
222
- const entry = loadEntry();
223
- startSession(entry, ui);
224
- host.buildMenu();
225
- return { host, tui, entry, notices, hostLines: () => cached ?? [] };
226
- }
227
-
228
- const api = (): BridgeApi => {
229
- const published = bridgeApi();
230
- assert.ok(published !== undefined, "the bridge is loaded, so its API object is present");
231
- return published;
232
- };
233
-
234
- /**
235
- * Register a style exactly as a plugin does. `registerCardStyle` is optional on the published object
236
- * because the slot is first-wins (an older copy could own it), so the plugin's own gate is here too.
237
- */
238
- const registerStyle = (style: CardStyleRegistration): (() => void) => {
239
- const register = api().registerCardStyle;
240
- assert.ok(typeof register === "function", "this release publishes registerCardStyle");
241
- return register(style);
242
- };
243
-
244
- // ---------------------------------------------------------------------------------------------
245
- // 1. Many plugins, one line
246
- // ---------------------------------------------------------------------------------------------
247
-
248
- test("three plugins, three styles: the host's line shows all of them, once", () => {
249
- const calls: string[] = [];
250
- const dismiss = [
251
- registerStyle(markerStyle("loon.loud", "loud", calls, { order: 20 })),
252
- registerStyle(markerStyle("pix.mono", "mono", calls, { order: 10 })),
253
- registerStyle(markerStyle("zephyr.wave", "wave", calls)),
254
- ];
255
- const world = startWorld();
256
-
257
- assert.equal(
258
- settingLine(world.host.toggleLines(), HOST_SETTING),
259
- "Card style ○ neon ● float ○ mono ○ loud ○ wave",
260
- "the host's values first, then every registered style in the documented order",
261
- );
262
- assert.equal(
263
- settingLine(world.host.toggleLines(), FALLBACK_SETTING),
264
- undefined,
265
- "one line: the bridge never contributes a second one while the host's is reachable",
266
- );
267
- assert.match(printStatus(world.entry), /^duplicate settings: none$/m);
268
- for (const dispose of dismiss) dispose();
269
- });
270
-
271
- // ---------------------------------------------------------------------------------------------
272
- // 2. Each style draws, one at a time
273
- // ---------------------------------------------------------------------------------------------
274
-
275
- test("selecting each style in turn repaints with that style only", async () => {
276
- const calls: string[] = [];
277
- for (const [id, label, order] of [
278
- ["loon.loud", "loud", 20],
279
- ["pix.mono", "mono", 10],
280
- ["zephyr.wave", "wave", undefined],
281
- ] as const) {
282
- registerStyle(markerStyle(id, label, calls, order === undefined ? {} : { order }));
283
- }
284
- const world = startWorld();
285
-
286
- const select = async (label: string): Promise<void> => {
287
- await world.host.pressRow(
288
- (row) => row.choice?.setting === HOST_SETTING && row.choice?.value === label,
289
- );
290
- };
291
-
292
- const expected: Record<string, string[]> = {
293
- mono: ["[mono] read ctrl+o", "<mono> config.ts", "[/mono]"],
294
- loud: ["[loud] read ctrl+o", "<loud> config.ts", "[/loud]"],
295
- wave: ["[wave] read ctrl+o", "<wave> config.ts", "[/wave]"],
296
- };
297
-
298
- for (const [label, lines] of Object.entries(expected)) {
299
- await select(label);
300
- const rendered = world.tui.chat.render(40);
301
- const drawn = new Set(calls.splice(0).map((call) => call.split(":")[0]));
302
-
303
- assert.deepEqual(rendered, lines, `${label} drew the card`);
304
- assert.deepEqual([...drawn], [`${label === "mono" ? "pix" : label === "loud" ? "loon" : "zephyr"}.${label}`]);
305
- }
306
- });
307
-
308
- // ---------------------------------------------------------------------------------------------
309
- // 3. A throwing style loses its frame and nothing else
310
- // ---------------------------------------------------------------------------------------------
311
-
312
- test("a throwing style passes the frame through, is reported, and the others keep working", async () => {
313
- const calls: string[] = [];
314
- registerStyle(markerStyle("pix.mono", "mono", calls));
315
- registerStyle({
316
- id: "loon.broken",
317
- label: "broken",
318
- frame: {
319
- top: () => {
320
- throw new Error("broken top");
321
- },
322
- line: (ctx) => ctx.text,
323
- bottom: () => "-",
324
- },
325
- });
326
- const world = startWorld();
327
- const select = (value: string) =>
328
- world.host.pressRow((row) => row.choice?.setting === HOST_SETTING && row.choice?.value === value);
329
-
330
- await select("broken");
331
- const frame = [...READ_CARD];
332
- assert.deepEqual(world.tui.chat.render(40), frame, "the frame is the host's, unchanged");
333
-
334
- const status = printStatus(world.entry);
335
- assert.match(status, /^card style frame: failed \(loon\.broken: broken top\)$/m);
336
- assert.match(status, /^card style failures: 1$/m);
337
-
338
- await select("mono");
339
- assert.deepEqual(world.tui.chat.render(40), ["[mono] read ctrl+o", "<mono> config.ts", "[/mono]"]);
340
- });
341
-
342
- // ---------------------------------------------------------------------------------------------
343
- // 4. The way back to host rendering (S13)
344
- // ---------------------------------------------------------------------------------------------
345
-
346
- test("pressing a host value returns to host rendering, and the next frame is the identical array", async () => {
347
- const calls: string[] = [];
348
- registerStyle(markerStyle("pix.mono", "mono", calls));
349
- const world = startWorld();
350
- world.tui.chat.render(40); // prime the host's own frame, with nothing selected yet
351
- const hostFrame = world.hostLines();
352
-
353
- await world.host.pressRow(
354
- (row) => row.choice?.setting === HOST_SETTING && row.choice?.value === "mono",
355
- );
356
- assert.deepEqual(world.tui.chat.render(40), ["[mono] read ctrl+o", "<mono> config.ts", "[/mono]"]);
357
-
358
- await world.host.press("Card style: float");
359
-
360
- assert.equal(
361
- settingLine(world.host.toggleLines(), HOST_SETTING),
362
- "Card style ○ neon ● float ○ mono",
363
- "one bullet, on the host's own value",
364
- );
365
- const restored = world.tui.chat.render(40);
366
- assert.equal(restored, hostFrame, "the identical array the host produced (S3)");
367
- assert.match(printStatus(world.entry), /^card style frame: pass-through \(native\)$/m);
368
- });
369
-
370
- // ---------------------------------------------------------------------------------------------
371
- // 5. A disposed style degrades to native
372
- // ---------------------------------------------------------------------------------------------
373
-
374
- test("disposing the selected style degrades to native, is reported, and the row drops the value", async () => {
375
- const calls: string[] = [];
376
- const dispose = registerStyle(markerStyle("pix.mono", "mono", calls));
377
- const world = startWorld();
378
- await world.host.pressRow(
379
- (row) => row.choice?.setting === HOST_SETTING && row.choice?.value === "mono",
380
- );
381
- assert.deepEqual(world.tui.chat.render(40), ["[mono] read ctrl+o", "<mono> config.ts", "[/mono]"]);
382
-
383
- dispose();
384
-
385
- const after = world.tui.chat.render(40);
386
- assert.deepEqual(after, [...world.hostLines()], "no chrome for a style that no longer exists");
387
- const status = printStatus(world.entry);
388
- assert.match(status, /^card style selection: pix\.mono \(missing: not registered\)$/m);
389
- assert.match(status, /^card style frame: pass-through \(missing: a disposed style\)$/m);
390
-
391
- // The next menu build is a new rows array, as a real host builds one per open.
392
- const reopened = startWorld();
393
- assert.equal(
394
- reopened.host
395
- .liveRows()
396
- .some((row) => row.choice?.setting === HOST_SETTING && row.choice?.value === "mono"),
397
- false,
398
- "the value is gone from the line",
399
- );
400
- });
401
-
402
- // ---------------------------------------------------------------------------------------------
403
- // 5b. A host without its own line gets the bridge's fallback line
404
- // ---------------------------------------------------------------------------------------------
405
-
406
- test("a host without its own Card style line gets the fallback line, and it selects styles", async () => {
407
- const calls: string[] = [];
408
- registerStyle(markerStyle("pix.mono", "mono", calls));
409
- const world = startWorld({
410
- rows: [
411
- { category: "Cards", label: "Card bash: builtin", action: () => {} },
412
- { category: "Cards", label: "Card output: builtin", action: () => {} },
413
- { category: "Layout", label: "Layout: builtin", action: () => {} },
414
- ],
415
- });
416
-
417
- assert.equal(
418
- settingLine(world.host.toggleLines(), FALLBACK_SETTING),
419
- "Bridge card style ● native ○ mono",
420
- "one line, native by default plus every registered style",
421
- );
422
- assert.equal(settingLine(world.host.toggleLines(), HOST_SETTING), undefined);
423
-
424
- await world.host.pressRow(
425
- (row) => row.choice?.setting === FALLBACK_SETTING && row.choice?.value === "mono",
426
- );
427
- assert.deepEqual(world.tui.chat.render(40), ["[mono] read ctrl+o", "<mono> config.ts", "[/mono]"]);
428
-
429
- await world.host.pressRow(
430
- (row) => row.choice?.setting === FALLBACK_SETTING && row.choice?.value === "native",
431
- );
432
- assert.equal(
433
- world.tui.chat.render(40),
434
- world.hostLines(),
435
- "native is the pass-through, by identity again",
436
- );
437
- assert.match(printStatus(world.entry), /^card style frame: pass-through \(native\)$/m);
438
- });
439
-
440
- // ---------------------------------------------------------------------------------------------
441
- // 6. Two session starts
442
- // ---------------------------------------------------------------------------------------------
443
-
444
- test("two session_start events never double-wrap the conversation", () => {
445
- const world = startWorld();
446
-
447
- startSession(world.entry, combinedUi(world.host, world.tui, world.notices));
448
-
449
- assert.equal(getCardStyleWiringDiagnostics().wrapped, 1, "one conversation component, one wrapper");
450
- assert.equal(getCardStyleWiringDiagnostics().lastOutcome, "already-wrapped");
451
- });
452
-
453
- // ---------------------------------------------------------------------------------------------
454
- // 7. A Pi-shaped tui without a conversation
455
- // ---------------------------------------------------------------------------------------------
456
-
457
- test("a tui without a conversation is reported, and rows and sections still work", () => {
458
- const notices: string[] = [];
459
- const host = createFakeHost();
460
- // Pi-shaped, but its document has no third child: the shape the bridge refuses to guess about.
461
- const brokenTui = { terminal: {}, children: [{ children: [{ render: () => [] }, { render: () => [] }] }] };
462
- const ui = combinedUi(host, brokenTui, notices);
463
- const entry = loadEntry();
464
- const sectionDispose = api().registerSection({
465
- id: "loon.music",
466
- title: "MUSIC",
467
- lines: () => ["track Blue in Green"],
468
- });
469
- const provider: RowProvider = {
470
- id: "loon.banner-source",
471
- category: "Banner",
472
- rows: () => [{ label: "Loon banner source", action: () => {} }],
473
- };
474
- registerRows(provider);
475
-
476
- startSession(entry, ui);
477
- host.buildMenu();
478
-
479
- assert.match(printStatus(entry), /^card style wiring: not wrapped \(no-conversation\)$/m);
480
- assert.deepEqual(
481
- host.rendered().filter((label) => label.startsWith("Loon banner source")),
482
- ["Loon banner source"],
483
- "the row provider reached the menu",
484
- );
485
- assert.ok(
486
- host.toggleLines().some((line) => line.startsWith("music ")),
487
- "and the section toggle is there too",
488
- );
489
- assert.ok(snapshotProviders().some((registered) => registered.id === "loon.banner-source"));
490
- sectionDispose();
491
- });