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,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
- });