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,457 +0,0 @@
1
- /**
2
- * The rail surface (R1–R5): the card style reaches the sidebar cards through the SAME repaint the
3
- * conversation uses, and a write to bridge-owned state repaints its surface before the action
4
- * returns.
5
- *
6
- * What is real here and what is modelled: the two card fixtures the rail renders are bytes produced
7
- * by gentle-shell's own `lib/shell-card.ts` (see `RAIL_HOST_CARD` in `./fake-sidebar.ts`), and the
8
- * `tui` the bridge walks is the host's own layout shape — root `vstack` → `hstack` →
9
- * `[conversation, rail ScrollView]`, the rail's framed rows being that view's child through the
10
- * layout protocol. The running TUI is not available to this package, so the *host component* is
11
- * modelled and the bridge's walk, wrapper and transform are the real code.
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
- import { registerCardStyle, registerSection, registerTransform, resetBusForTests, resetCardStylesForTests, resetSectionsForTests, snapshotProviders } from "../src/bus.ts";
21
- import { getCardStyleLastOutcome, resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
22
- import {
23
- disableCardStyleTransform,
24
- installCardStyleTransform,
25
- installRailCardStyleTransform,
26
- resetCardStyleWiringForTests,
27
- } from "../src/card-style-wiring.ts";
28
- import { setCardStyleSelection } from "../src/card-styles.ts";
29
- import {
30
- getRepaintDiagnostics,
31
- requestSurfaceRepaint,
32
- resetRepaintForTests,
33
- setRepaintHost,
34
- } from "../src/repaint.ts";
35
- import { SECTION_ROWS_PROVIDER_ID, installSectionRows } from "../src/sections-rows.ts";
36
- import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
37
- import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
38
- import { collectStatus, formatStatus } from "../src/status.ts";
39
- import { resetStoreStateForTests } from "../src/store.ts";
40
- import type { CustomizeRow, InlineHelpers, RowContext, SidebarSection } from "../src/types.ts";
41
- import {
42
- RAIL_BRIDGE_CARD,
43
- RAIL_HOST_CARD,
44
- type Rail,
45
- createRailTui,
46
- resetSidebar,
47
- } from "./fake-sidebar.ts";
48
- import { createFakeTranscriptTui, stripAnsi } from "./fake-transcript.ts";
49
-
50
- let configHome: string;
51
- let previousConfigHome: string | undefined;
52
-
53
- test.before(() => {
54
- configHome = mkdtempSync(join(tmpdir(), "gsb-rail-surfaces-"));
55
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
56
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
57
- });
58
-
59
- test.after(() => {
60
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
61
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
62
- rmSync(configHome, { recursive: true, force: true });
63
- });
64
-
65
- test.beforeEach(() => {
66
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
67
- resetBusForTests();
68
- resetSectionsForTests();
69
- resetCardStylesForTests();
70
- resetCardStyleTransformForTests();
71
- resetCardStyleWiringForTests();
72
- resetRepaintForTests();
73
- resetSidebar();
74
- resetSidebarStateForTests();
75
- resetSectionFailuresForTests();
76
- resetStoreStateForTests();
77
- disableCardStyleTransform();
78
- disableSidebarBackend();
79
- });
80
-
81
- test.afterEach(() => {
82
- disableCardStyleTransform();
83
- disableSidebarBackend();
84
- });
85
-
86
- /** A style whose every row proves which function drew it: no float chrome survives. */
87
- const PLAIN_STYLE = {
88
- id: "loon.plain",
89
- label: "plain",
90
- frame: {
91
- top: (ctx: { title?: string }) => `<${ctx.title ?? ""}>`,
92
- line: (ctx: { text: string }) => `| ${ctx.text}`,
93
- bottom: () => "_",
94
- },
95
- };
96
-
97
- /** The rail as the host frames it: the host's own section card, a gap, then the bridge's. */
98
- const RAIL_FRAMES: readonly string[] = [...RAIL_HOST_CARD, "", ...RAIL_BRIDGE_CARD];
99
-
100
- /** A rail component that hands back ONE array instance every frame, like the host's closure. */
101
- const stableRail = (lines: readonly string[]): Rail => ({
102
- render: () => lines as string[],
103
- });
104
-
105
- const railTui = (lines: readonly string[] = RAIL_FRAMES) =>
106
- createRailTui({ rail: stableRail(lines), contentWidth: 40 });
107
-
108
- const section = (id: string, extra: Partial<SidebarSection> = {}): SidebarSection => ({
109
- id,
110
- title: id.toUpperCase(),
111
- lines: () => [`body ${id}`],
112
- ...extra,
113
- });
114
-
115
- const ctx = (): RowContext => ({ theme: undefined, configHome });
116
-
117
- /** What the host hands a row's action; the bridge's toggles use none of it. */
118
- const inlineHelpers: InlineHelpers = {
119
- input: async () => undefined,
120
- confirm: async () => true,
121
- capture: async () => undefined,
122
- };
123
-
124
- const rowsNow = (): CustomizeRow[] => {
125
- const provider = snapshotProviders().find((entry) => entry.id === SECTION_ROWS_PROVIDER_ID);
126
- assert.ok(provider !== undefined, "the bridge registers its own Sections provider");
127
- return provider.rows(ctx());
128
- };
129
-
130
- const cardBlock = (lines: readonly string[]): string[] => {
131
- const start = lines.findIndex((line) => line.startsWith("card styles: "));
132
- if (start === -1) return [];
133
- const rest = lines.slice(start);
134
- const end = rest.findIndex((line) => line.startsWith("card style note: "));
135
- return end === -1 ? rest : rest.slice(0, end + 1);
136
- };
137
-
138
- // --------------------------------------------------------------------------------------------
139
- // R1 — one card style, every card surface, one shared function
140
- // --------------------------------------------------------------------------------------------
141
-
142
- test("R1: the rail's host section and contributed section are both repainted with the style", () => {
143
- registerCardStyle(PLAIN_STYLE);
144
- setCardStyleSelection(PLAIN_STYLE.id);
145
- const tui = railTui();
146
- const installed = installRailCardStyleTransform(tui);
147
- assert.deepEqual(installed, { installed: true, outcome: "wrapped", component: "rail" });
148
-
149
- const painted = tui.renderRail().map((line) => stripAnsi(line));
150
-
151
- assert.ok(painted.includes("<STATS>"), "the HOST section's card is drawn with the style's top");
152
- assert.ok(painted.includes("<Music>"), "so is the CONTRIBUTED section's card");
153
- assert.deepEqual(
154
- painted.slice(1, 3),
155
- ["| native row 1", "| native row 2"],
156
- "the host section's body rows are redrawn by the style",
157
- );
158
- assert.deepEqual(
159
- painted.slice(6, 8),
160
- ["| track one", "| track two"],
161
- "and the contributed section's body rows too",
162
- );
163
- assert.equal(
164
- painted.some((line) => line.includes("▎")),
165
- false,
166
- "no host float chrome is carried into a repainted card",
167
- );
168
- assert.deepEqual(getCardStyleLastOutcome("rail"), {
169
- kind: "transformed",
170
- styleId: PLAIN_STYLE.id,
171
- cards: 2,
172
- });
173
- });
174
-
175
- test("R1: the rail repaint is the conversation's own function, not a second copy", () => {
176
- registerCardStyle(PLAIN_STYLE);
177
- setCardStyleSelection(PLAIN_STYLE.id);
178
- const conversation = createFakeTranscriptTui({ lines: [...RAIL_FRAMES] });
179
- installCardStyleTransform(conversation);
180
- const tui = railTui();
181
- installRailCardStyleTransform(tui);
182
-
183
- assert.deepEqual(
184
- tui.renderRail(),
185
- conversation.chat.render(40),
186
- "identical input, identical rows: one transform serves both surfaces",
187
- );
188
- });
189
-
190
- test("R1: native restores the host's own frames on the rail, by identity", () => {
191
- registerCardStyle(PLAIN_STYLE);
192
- const tui = railTui();
193
- installRailCardStyleTransform(tui);
194
-
195
- setCardStyleSelection(PLAIN_STYLE.id);
196
- assert.notEqual(tui.renderRail(), RAIL_FRAMES, "a selected style rewrites the frame");
197
-
198
- setCardStyleSelection("native");
199
- assert.equal(
200
- tui.renderRail(),
201
- RAIL_FRAMES,
202
- "native returns the rail's own array instance, untouched",
203
- );
204
- });
205
-
206
- test("R1: a second rail attach wraps once, so a frame is never restyled twice", () => {
207
- registerCardStyle(PLAIN_STYLE);
208
- setCardStyleSelection(PLAIN_STYLE.id);
209
- const tui = railTui();
210
- installRailCardStyleTransform(tui);
211
- const once = tui.renderRail();
212
-
213
- assert.deepEqual(installRailCardStyleTransform(tui), {
214
- installed: true,
215
- outcome: "already-wrapped",
216
- component: "rail",
217
- });
218
- assert.deepEqual(tui.renderRail(), once, "the marker certifies the exact wrapper already installed");
219
- });
220
-
221
- test("R1: a rail mounted after the factory is reached on the next bridge-owned write", () => {
222
- // Pi hands the widget factory a tui before gentle-shell has mounted its fullscreen layout, so the
223
- // first attach can legitimately fail. It must not need a timer: the next write re-attempts once.
224
- registerCardStyle(PLAIN_STYLE);
225
- setCardStyleSelection(PLAIN_STYLE.id);
226
- const tui = railTui();
227
- const late = createRailTui({ rail: stableRail(RAIL_FRAMES), contentWidth: 40 });
228
- const bare = { terminal: tui.terminal, requestRender: () => {} };
229
- setRepaintHost(bare);
230
- assert.deepEqual(installRailCardStyleTransform(bare), {
231
- installed: false,
232
- outcome: "no-layout-root",
233
- });
234
-
235
- // The host mounts its layout; the write that needs the rail repaint is the moment it is reached.
236
- setRepaintHost(late);
237
- setCardStyleSelection("native");
238
- requestSurfaceRepaint({ cause: "card style → native", surfaces: ["conversation", "rail"] });
239
- assert.deepEqual(getRepaintDiagnostics().reach.rail, { reached: false, reason: "no-layout-root" });
240
-
241
- const attached = createRailTui({ rail: stableRail(RAIL_FRAMES), contentWidth: 40 });
242
- setRepaintHost(attached);
243
- installRailCardStyleTransform(attached);
244
- assert.deepEqual(getRepaintDiagnostics().reach.rail, { reached: true });
245
- setCardStyleSelection(PLAIN_STYLE.id);
246
- assert.ok(attached.renderRail().some((line) => stripAnsi(line) === "<STATS>"));
247
- });
248
-
249
- test("R1: a registered transcript transform stays on the conversation, never the rail", () => {
250
- registerCardStyle(PLAIN_STYLE);
251
- setCardStyleSelection(PLAIN_STYLE.id);
252
- registerTransform({ id: "loon.gutter", transform: (lines) => lines.map((line) => `> ${line}`) });
253
- const conversation = createFakeTranscriptTui({ lines: [...RAIL_FRAMES] });
254
- installCardStyleTransform(conversation);
255
- const tui = railTui();
256
- installRailCardStyleTransform(tui);
257
-
258
- assert.ok(
259
- conversation.chat.render(40).every((line) => line.startsWith("> ")),
260
- "the pipeline runs on the conversation",
261
- );
262
- assert.equal(
263
- tui.renderRail().some((line) => line.startsWith("> ")),
264
- false,
265
- "the rail sees the card style only: a plugin's conversation rows never reach another surface",
266
- );
267
- });
268
-
269
- // --------------------------------------------------------------------------------------------
270
- // R2 — the repaint cannot corrupt the rail
271
- // --------------------------------------------------------------------------------------------
272
-
273
- test("R2: a frame that is not a card passes through as the same instance", () => {
274
- registerCardStyle(PLAIN_STYLE);
275
- setCardStyleSelection(PLAIN_STYLE.id);
276
- const prose = ["MUSIC", " track one", "", " track two"];
277
- const tui = railTui(prose);
278
- installRailCardStyleTransform(tui);
279
-
280
- assert.equal(tui.renderRail(), prose, "byte-identical, same array instance");
281
- assert.deepEqual(getCardStyleLastOutcome("rail"), { kind: "pass-through", reason: "no-card" });
282
- });
283
-
284
- test("R2: every repainted row is clipped to the width the host rendered at", () => {
285
- registerCardStyle({
286
- id: "loon.wide",
287
- label: "wide",
288
- frame: {
289
- top: () => "T".repeat(200),
290
- line: () => "L".repeat(200),
291
- bottom: () => "B".repeat(200),
292
- },
293
- });
294
- setCardStyleSelection("loon.wide");
295
- const tui = railTui();
296
- installRailCardStyleTransform(tui);
297
-
298
- for (const row of tui.renderRail().map((line) => stripAnsi(line))) {
299
- assert.ok(row.length <= 40, `${JSON.stringify(row)} fits the rail's content width`);
300
- }
301
- });
302
-
303
- // --------------------------------------------------------------------------------------------
304
- // R3 — a write repaints its surface, synchronously, once, and only when something changed
305
- // --------------------------------------------------------------------------------------------
306
-
307
- test("R3: a visibility toggle repaints the rail before the action returns, exactly once", () => {
308
- registerSection(section("loon.music", { title: "MUSIC" }));
309
- installSectionRows();
310
- const tui = railTui();
311
- setRepaintHost(tui);
312
- assert.equal(tui.cacheRevision(), 0);
313
-
314
- const hidden = rowsNow().find(
315
- (row) => row.choice?.setting === "music" && row.choice.value === "hidden",
316
- );
317
- assert.ok(hidden?.action !== undefined, "the bridge derives the action");
318
- hidden.action(inlineHelpers);
319
-
320
- assert.equal(tui.requestRenderCount(), 1, "one frame was requested, inside the action");
321
- assert.equal(
322
- tui.cacheRevision(),
323
- 1,
324
- "the host's terminal-side cache was invalidated, as invalidateSidebar(tui) does",
325
- );
326
- assert.deepEqual(getRepaintDiagnostics().lastTrigger, {
327
- cause: 'section "loon.music" → hidden',
328
- surfaces: ["rail"],
329
- outcome: "repainted",
330
- });
331
- });
332
-
333
- test("R3: a write that changes nothing doesn't ask for a frame", () => {
334
- registerSection(section("loon.music", { title: "MUSIC" }));
335
- installSectionRows();
336
- const tui = railTui();
337
- setRepaintHost(tui);
338
-
339
- const rows = rowsNow();
340
- const shown = rows.find((row) => row.choice?.setting === "music" && row.choice.value === "shown");
341
- assert.ok(shown?.action !== undefined);
342
- shown.action(inlineHelpers);
343
-
344
- assert.equal(tui.requestRenderCount(), 0, "shown was already shown: no frame, no invalidation");
345
- assert.equal(tui.cacheRevision(), 0);
346
- assert.equal(getRepaintDiagnostics().lastTrigger, undefined);
347
- });
348
-
349
- test("R3: a card-style write repaints both surfaces, synchronously", () => {
350
- registerCardStyle(PLAIN_STYLE);
351
- const tui = railTui();
352
- setRepaintHost(tui);
353
- setCardStyleSelection("native");
354
-
355
- requestSurfaceRepaint({ cause: `card style → ${PLAIN_STYLE.id}`, surfaces: ["conversation", "rail"] });
356
-
357
- assert.equal(tui.requestRenderCount(), 1);
358
- assert.deepEqual(getRepaintDiagnostics().lastTrigger, {
359
- cause: `card style → ${PLAIN_STYLE.id}`,
360
- surfaces: ["conversation", "rail"],
361
- outcome: "repainted",
362
- });
363
- });
364
-
365
- test("R3: nothing polls — the frame is requested in the action, and no timer is scheduled", () => {
366
- registerSection(section("loon.music", { title: "MUSIC" }));
367
- registerCardStyle(PLAIN_STYLE);
368
- installSectionRows();
369
- const tui = railTui();
370
- setRepaintHost(tui);
371
- installRailCardStyleTransform(tui);
372
-
373
- const scheduled = { timeouts: 0, intervals: 0 };
374
- const realSetTimeout = globalThis.setTimeout;
375
- const realSetInterval = globalThis.setInterval;
376
- globalThis.setTimeout = ((...args: Parameters<typeof setTimeout>) => {
377
- scheduled.timeouts += 1;
378
- return realSetTimeout(...args);
379
- }) as typeof setTimeout;
380
- globalThis.setInterval = ((...args: Parameters<typeof setInterval>) => {
381
- scheduled.intervals += 1;
382
- return realSetInterval(...args);
383
- }) as typeof setInterval;
384
- try {
385
- const hidden = rowsNow().find(
386
- (row) => row.choice?.setting === "music" && row.choice.value === "hidden",
387
- );
388
- assert.ok(hidden?.action !== undefined);
389
- hidden.action(inlineHelpers);
390
- tui.renderRail();
391
- } finally {
392
- globalThis.setTimeout = realSetTimeout;
393
- globalThis.setInterval = realSetInterval;
394
- }
395
-
396
- assert.deepEqual(scheduled, { timeouts: 0, intervals: 0 }, "no timer carries the repaint");
397
- assert.equal(tui.requestRenderCount(), 1);
398
- });
399
-
400
- // --------------------------------------------------------------------------------------------
401
- // R4 — the report names the surfaces, the frames and the last trigger
402
- // --------------------------------------------------------------------------------------------
403
-
404
- test("R4: /bridge-status reports each surface's reach and frame, and the last trigger", () => {
405
- registerCardStyle(PLAIN_STYLE);
406
- setCardStyleSelection(PLAIN_STYLE.id);
407
- const conversation = createFakeTranscriptTui({ lines: [...RAIL_FRAMES] });
408
- installCardStyleTransform(conversation);
409
- conversation.chat.render(40);
410
-
411
- const tui = railTui();
412
- setRepaintHost(tui);
413
- installRailCardStyleTransform(tui);
414
- tui.renderRail();
415
- requestSurfaceRepaint({ cause: 'section "loon.music" → hidden', surfaces: ["rail"] });
416
-
417
- const lines = formatStatus(collectStatus());
418
-
419
- assert.deepEqual(cardBlock(lines).slice(-4), [
420
- 'card style surface conversation: reached (chat wrapped), last frame transformed 2 cards',
421
- "card style surface rail: reached (rail wrapped), last frame transformed 2 cards",
422
- "card style wiring: rail wrapped, chat wrapped",
423
- "card style note: a downstream post-processor can strip or override this frame; " +
424
- "the bridge cannot prevent it.",
425
- ]);
426
- assert.ok(
427
- lines.includes('last repaint: rail for section "loon.music" → hidden (repainted)'),
428
- "the last trigger is reported with its cause",
429
- );
430
- });
431
-
432
- test("R4: an unreachable rail is reported, never silently skipped", () => {
433
- registerCardStyle(PLAIN_STYLE);
434
- installRailCardStyleTransform({ terminal: {}, children: [] });
435
- const tui = railTui();
436
-
437
- assert.deepEqual(getRepaintDiagnostics().reach.rail, {
438
- reached: false,
439
- reason: "no-layout-root",
440
- });
441
-
442
- // No host has handed a tui to the bridge yet: the write is reported as unavailable.
443
- resetRepaintForTests();
444
- const trigger = requestSurfaceRepaint({ cause: 'section "loon.music" → hidden', surfaces: ["rail"] });
445
- assert.deepEqual(trigger, {
446
- cause: 'section "loon.music" → hidden',
447
- surfaces: ["rail"],
448
- outcome: "unavailable",
449
- reason: "no tui has been captured, so no surface can be repainted",
450
- });
451
- assert.ok(
452
- formatStatus(collectStatus()).includes(
453
- 'last repaint: rail for section "loon.music" → hidden (unavailable: no tui has been captured, so no surface can be repainted)',
454
- ),
455
- );
456
- assert.equal(tui.requestRenderCount(), 0, "an unreached rail is never asked for a frame");
457
- });