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,510 +0,0 @@
1
- /**
2
- * The `Card style` line (S1, S12): the host's own line, extended in place, with a separate-line
3
- * fallback that is reported rather than silent.
4
- *
5
- * These tests drive the REAL capture path (`armBridge` on the fake host), because the whole point
6
- * of the design is that the bridge does not add a second line: it splices the host's rows out of
7
- * the captured array and puts them back at the same index, immediately followed by one row per
8
- * registered style, so the host coalesces them into one line. What a test asserts is what the
9
- * renderer draws (`fakeHost.toggleLines()`), plus the live row objects themselves.
10
- */
11
-
12
- import assert from "node:assert/strict";
13
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
14
- import { tmpdir } from "node:os";
15
- import { join } from "node:path";
16
- import test from "node:test";
17
-
18
- import { armBridge } from "../src/arm.ts";
19
- import {
20
- registerCardStyle,
21
- resetBusForTests,
22
- resetCardStylesForTests,
23
- resetSectionsForTests,
24
- snapshotProviders,
25
- } from "../src/bus.ts";
26
- import {
27
- CARD_STYLE_FALLBACK_SETTING,
28
- CARD_STYLE_ROWS_PROVIDER_ID,
29
- CARD_STYLE_ROW_LABEL,
30
- cardStyleRowMode,
31
- extendCardStyleLine,
32
- installCardStyleRows,
33
- resetCardStyleRowStateForTests,
34
- } from "../src/card-style-rows.ts";
35
- import { HOST_CARD_STYLE_SETTING } from "../src/card-style-line.ts";
36
- import { CARD_STYLE_STORE_ID, getCardStyleSelection, setCardStyleSelection } from "../src/card-styles.ts";
37
- import { disableCaptureBackend, resetCaptureStateForTests } from "../src/capture.ts";
38
- import { resetHookStateForTests } from "../src/hook.ts";
39
- import { resetRowBuildStateForTests } from "../src/rows.ts";
40
- import { snapshotNotices, resetNoticesForTests } from "../src/notices.ts";
41
- import { collectStatus } from "../src/status.ts";
42
- import { transformCardLines } from "../src/card-style-transform.ts";
43
- import { resetStoreStateForTests } from "../src/store.ts";
44
- import type { CardStyleRegistration, RowProvider } from "../src/types.ts";
45
- import { createFakeHost, type FakeRow } from "./fake-host.ts";
46
- import { READ_CARD } from "./fake-transcript.ts";
47
-
48
- /** The host's own state: which native card style its config file holds. */
49
- let hostStyle: "neon" | "float";
50
- let hostActions: string[];
51
-
52
- let configHome: string;
53
- let previousConfigHome: string | undefined;
54
-
55
- test.before(() => {
56
- configHome = mkdtempSync(join(tmpdir(), "gsb-card-line-"));
57
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
58
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
59
- });
60
-
61
- test.after(() => {
62
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
63
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
64
- rmSync(configHome, { recursive: true, force: true });
65
- });
66
-
67
- test.beforeEach(() => {
68
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
69
- resetBusForTests();
70
- resetSectionsForTests();
71
- resetCardStylesForTests();
72
- resetCardStyleRowStateForTests();
73
- resetCaptureStateForTests();
74
- resetHookStateForTests();
75
- resetRowBuildStateForTests();
76
- resetStoreStateForTests();
77
- disableCaptureBackend();
78
- hostStyle = "float";
79
- hostActions = [];
80
- });
81
-
82
- const frame = () => ({
83
- top: (ctx: { width: number }) => "▛".repeat(ctx.width),
84
- line: (ctx: { text: string }) => ctx.text,
85
- bottom: (ctx: { width: number }) => "▙".repeat(ctx.width),
86
- });
87
-
88
- const style = (id: string, extra: Partial<CardStyleRegistration> = {}): CardStyleRegistration => ({
89
- id,
90
- label: id,
91
- frame: frame(),
92
- ...extra,
93
- });
94
-
95
- /** The host's own `Card style` row, shaped like `extensions/gentle-shell.ts:3405-3419`. */
96
- const hostStyleRow = (value: "neon" | "float", action?: () => Promise<void>): FakeRow => ({
97
- category: "Cards",
98
- label: () => `Card style: ${value}`,
99
- preview: () => ({ title: `Cards · ${value}`, sample: `${value} chrome + prompt` }),
100
- action:
101
- action ??
102
- (async () => {
103
- hostStyle = value;
104
- hostActions.push(value);
105
- }),
106
- choice: {
107
- setting: HOST_CARD_STYLE_SETTING,
108
- value,
109
- current: () => hostStyle === value,
110
- hint: "How conversation cards are drawn",
111
- },
112
- });
113
-
114
- /** The host's menu, with its own `Card style` line inside `Cards`. */
115
- const rowsWithHostLine = (floatAction?: () => Promise<void>): FakeRow[] => [
116
- { category: "Cards", label: "Card bash: builtin", action: () => {} },
117
- { category: "Cards", label: "Card output: builtin", action: () => {} },
118
- {
119
- category: "Cards",
120
- label: "Card content: default",
121
- action: () => {},
122
- choice: { setting: "Card content", value: "default", current: () => true },
123
- },
124
- hostStyleRow("neon"),
125
- hostStyleRow("float", floatAction),
126
- { category: "Layout", label: "Layout: builtin", action: () => {} },
127
- ];
128
-
129
- /** The same menu WITHOUT the host's line: the fallback path (a renamed setting, a narrower host). */
130
- const rowsWithoutHostLine = (): FakeRow[] => [
131
- { category: "Cards", label: "Card bash: builtin", action: () => {} },
132
- { category: "Cards", label: "Card output: builtin", action: () => {} },
133
- { category: "Layout", label: "Layout: builtin", action: () => {} },
134
- ];
135
-
136
- const settingLines = (lines: readonly string[], setting: string): string[] =>
137
- lines.filter((line) => line.startsWith(`${setting} `));
138
-
139
- // ---------------------------------------------------------------------------------------------
140
- // Extended mode: the host's line grows, in the host's position
141
- // ---------------------------------------------------------------------------------------------
142
-
143
- test("capture extends the host's own Card style line instead of adding a second one", () => {
144
- const host = createFakeHost({ rows: rowsWithHostLine() });
145
- armBridge(host.ui);
146
- installCardStyleRows();
147
- registerCardStyle(style("loon.cute", { label: "cute" }));
148
-
149
- host.buildMenu();
150
-
151
- assert.equal(cardStyleRowMode(), "extended");
152
- assert.deepEqual(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING), [
153
- "Card style ○ neon ● float ○ cute",
154
- ]);
155
- assert.deepEqual(
156
- settingLines(host.toggleLines(), CARD_STYLE_FALLBACK_SETTING),
157
- [],
158
- "no second line was contributed",
159
- );
160
- });
161
-
162
- test("the host's rows stay at their index; only their current() is wrapped (S13)", () => {
163
- const rows = rowsWithHostLine();
164
- const [neonRow, floatRow] = [rows[3] as FakeRow, rows[4] as FakeRow];
165
- const host = createFakeHost({ rows });
166
- armBridge(host.ui);
167
- installCardStyleRows();
168
- registerCardStyle(style("loon.cute", { label: "cute" }));
169
-
170
- host.buildMenu();
171
-
172
- const live = host.liveRows();
173
- const wrapped = live[3] as FakeRow;
174
- assert.notEqual(wrapped, neonRow, "a shallow clone, because current() must answer differently");
175
- assert.equal(live[4] === floatRow, false, "both host rows are wrapped, not just the first");
176
-
177
- // Presentation and identity are the host's own; only the two behaviour hooks are wrapped.
178
- for (const field of ["label", "preview", "category", "keyhint", "visible"] as const) {
179
- assert.equal(wrapped[field], neonRow[field], `${field} is the host's own`);
180
- }
181
- const before = neonRow.choice!;
182
- const after = wrapped.choice!;
183
- for (const field of ["setting", "value", "hint", "applyOnMove"] as const) {
184
- assert.equal(after[field], before[field], `choice.${field} is untouched`);
185
- }
186
- assert.notEqual(after.current, before.current, "current() is wrapped (S13)");
187
- assert.equal(after.current(), before.current(), "and it delegates while no bridge style is selected");
188
- assert.notEqual(wrapped.action, neonRow.action, "action() is wrapped: pressing a host value exits (S13)");
189
-
190
- assert.equal(live[5]?.choice?.value, "cute", "the registered value follows immediately");
191
- assert.deepEqual(
192
- live.filter((row) => row.category === "Cards").map((row) => row.choice?.setting ?? "—"),
193
- ["—", "—", "Card content", HOST_CARD_STYLE_SETTING, HOST_CARD_STYLE_SETTING, HOST_CARD_STYLE_SETTING],
194
- "the line stays where the host put it, after Card content",
195
- );
196
- });
197
-
198
- test("while a bridge style is selected the host's own values are NOT current (S13)", async () => {
199
- const host = createFakeHost({ rows: rowsWithHostLine() });
200
- armBridge(host.ui);
201
- installCardStyleRows();
202
- registerCardStyle(style("loon.cute", { label: "cute" }));
203
- host.buildMenu();
204
-
205
- assert.deepEqual(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING), [
206
- "Card style ○ neon ● float ○ cute",
207
- ], "native: the host's own answer, delegated, and no bridge value is current");
208
-
209
- await host.pressRow(
210
- (row) => row.choice?.setting === HOST_CARD_STYLE_SETTING && row.choice?.value === "cute",
211
- );
212
-
213
- assert.deepEqual(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING), [
214
- "Card style ○ neon ○ float ● cute",
215
- ], "one current value, and it is the bridge's");
216
-
217
- // Pressing a host value IS the way back (S13): the host's own action runs, and the wrapped action
218
- // clears the bridge selection, so the host renders again.
219
- await host.press("Card style: float");
220
- assert.equal(hostActions.at(-1), "float", "the host's setter ran");
221
- assert.equal(getCardStyleSelection(), "native", "and the bridge selection is native again");
222
- assert.deepEqual(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING), [
223
- "Card style ○ neon ● float ○ cute",
224
- ], "exactly one bullet, on the host's value");
225
-
226
- const nextFrame = [...READ_CARD];
227
- assert.equal(
228
- transformCardLines(nextFrame, 40),
229
- nextFrame,
230
- "and the next frame is not transformed: the user is back to the host's rendering",
231
- );
232
- });
233
-
234
- test("a stale selection lets the host's own values speak again (S12, S13)", () => {
235
- const dispose = registerCardStyle(style("loon.cute", { label: "cute" }));
236
- const host = createFakeHost({ rows: rowsWithHostLine() });
237
- armBridge(host.ui);
238
- installCardStyleRows();
239
- setCardStyleSelection("loon.cute");
240
- host.buildMenu();
241
- assert.match(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING)[0] ?? "", /● cute$/u);
242
-
243
- dispose();
244
- const reopened = createFakeHost({ rows: rowsWithHostLine() });
245
- resetCaptureStateForTests();
246
- armBridge(reopened.ui);
247
- reopened.buildMenu();
248
-
249
- assert.deepEqual(settingLines(reopened.toggleLines(), HOST_CARD_STYLE_SETTING), [
250
- "Card style ○ neon ● float",
251
- ], "the selection names a style nobody owns: native behaviour, no dead bullet");
252
- });
253
-
254
- test("a throwing host action still returns to native and never throws out of the menu (S13)", async () => {
255
- const host = createFakeHost({
256
- rows: rowsWithHostLine(async () => {
257
- throw new Error("host setter exploded");
258
- }),
259
- });
260
- armBridge(host.ui);
261
- installCardStyleRows();
262
- registerCardStyle(style("loon.cute", { label: "cute" }));
263
- host.buildMenu();
264
- await host.pressRow(
265
- (row) => row.choice?.setting === HOST_CARD_STYLE_SETTING && row.choice?.value === "cute",
266
- );
267
- assert.equal(getCardStyleSelection(), "loon.cute");
268
-
269
- await assert.doesNotReject(async () => host.press("Card style: float"), "the menu keeps working");
270
-
271
- assert.equal(
272
- getCardStyleSelection(),
273
- "native",
274
- "a failed host action must not leave the user trapped in a bridge style",
275
- );
276
- const noticed = snapshotNotices().entries.map((entry) => entry.message).join("\n");
277
- assert.match(noticed, /host setter exploded/u, "and the failure is recorded, not swallowed silently");
278
- });
279
-
280
- test("pressing a native value calls the host's own action; a registered value is the bridge's", async () => {
281
- const host = createFakeHost({ rows: rowsWithHostLine() });
282
- armBridge(host.ui);
283
- installCardStyleRows();
284
- registerCardStyle(style("loon.cute", { label: "cute" }));
285
- host.buildMenu();
286
-
287
- await host.press("Card style: neon");
288
- assert.deepEqual(hostActions, ["neon"], "the host's setter ran; the bridge did not replace it");
289
- assert.equal(getCardStyleSelection(), "native", "and the bridge store was not touched");
290
-
291
- await host.pressRow(
292
- (row) => row.choice?.setting === HOST_CARD_STYLE_SETTING && row.choice?.value === "cute",
293
- );
294
- assert.equal(getCardStyleSelection(), "loon.cute", "the bridge persisted first");
295
- assert.deepEqual(hostActions, ["neon"], "and the host's own setter never ran for our value");
296
- const file = join(configHome, "gentle-shell-bridge", `${CARD_STYLE_STORE_ID}.json`);
297
- assert.equal(
298
- (JSON.parse(readFileSync(file, "utf8")) as { values: Record<string, string> }).values.cardStyle,
299
- "loon.cute",
300
- );
301
- assert.deepEqual(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING), [
302
- "Card style ○ neon ○ float ● cute",
303
- ], "the bridge's value is the only current one (S13)");
304
- });
305
-
306
- test("three plugins, three styles, one line, deterministic order", () => {
307
- const host = createFakeHost({ rows: rowsWithHostLine() });
308
- armBridge(host.ui);
309
- installCardStyleRows();
310
- // Three independent plugins registering in an arbitrary order; `order` decides among two of them.
311
- registerCardStyle(style("loon.loud", { label: "loud", order: 20 }));
312
- registerCardStyle(style("pix.mono", { label: "mono", order: 10 }));
313
- registerCardStyle(style("zephyr.wave", { label: "wave" }));
314
-
315
- host.buildMenu();
316
-
317
- assert.deepEqual(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING), [
318
- "Card style ○ neon ● float ○ mono ○ loud ○ wave",
319
- ]);
320
- });
321
-
322
- test("nothing capped or hidden: the row carries every registered value", () => {
323
- const host = createFakeHost({ rows: rowsWithHostLine() });
324
- armBridge(host.ui);
325
- installCardStyleRows();
326
- const registered = Array.from({ length: 12 }, (_, index) => `plugin.${index}`);
327
- for (const id of registered) registerCardStyle(style(id));
328
-
329
- host.buildMenu();
330
-
331
- const values = host
332
- .liveRows()
333
- .filter((row) => row.choice?.setting === HOST_CARD_STYLE_SETTING)
334
- .map((row) => row.choice?.value);
335
- assert.deepEqual(values, ["neon", "float", ...registered]);
336
- });
337
-
338
- test("with no style registered the host's line is left exactly as it was", () => {
339
- const host = createFakeHost({ rows: rowsWithHostLine() });
340
- armBridge(host.ui);
341
- installCardStyleRows();
342
-
343
- host.buildMenu();
344
-
345
- assert.equal(cardStyleRowMode(), "extended");
346
- assert.deepEqual(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING), [
347
- "Card style ○ neon ● float",
348
- ]);
349
- assert.equal(host.liveRows().length, 6, "not a row more");
350
- });
351
-
352
- test("a registered value previews literally, and a throwing preview cannot break the menu", () => {
353
- const host = createFakeHost({ rows: rowsWithHostLine() });
354
- armBridge(host.ui);
355
- installCardStyleRows();
356
- registerCardStyle(
357
- style("loon.cute", {
358
- label: "cute",
359
- preview: () => ({ title: "Cute", sample: "╭─ ✿ cute ─╮" }),
360
- }),
361
- );
362
- registerCardStyle(
363
- style("loon.broken", {
364
- label: "broken",
365
- preview: () => {
366
- throw new Error("preview exploded");
367
- },
368
- }),
369
- );
370
- host.buildMenu();
371
-
372
- const live = host.liveRows();
373
- const cute = live.find((row) => row.choice?.value === "cute");
374
- const broken = live.find((row) => row.choice?.value === "broken");
375
-
376
- assert.deepEqual(cute?.preview?.(), { title: "Cute", sample: "╭─ ✿ cute ─╮" });
377
- const fallback = broken?.preview?.();
378
- assert.equal(fallback?.title, "Cards · broken");
379
- assert.match(fallback?.sample ?? "", /broken/u, "the literal fallback names the style");
380
- });
381
-
382
- test("a disposed style leaves the line and its row, and the rest keep working (S12)", () => {
383
- const host = createFakeHost({ rows: rowsWithHostLine() });
384
- armBridge(host.ui);
385
- installCardStyleRows();
386
- const dispose = registerCardStyle(style("loon.cute", { label: "cute" }));
387
- registerCardStyle(style("pix.mono", { label: "mono" }));
388
- host.buildMenu();
389
- assert.match(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING)[0] ?? "", /cute/u);
390
-
391
- dispose();
392
- // The host rebuilds its rows array on every menu open (that is why the splice runs per capture);
393
- // a fresh fake host is the honest model of "the next open".
394
- const reopened = createFakeHost({ rows: rowsWithHostLine() });
395
- resetCaptureStateForTests();
396
- armBridge(reopened.ui);
397
- reopened.buildMenu();
398
-
399
- assert.deepEqual(settingLines(reopened.toggleLines(), HOST_CARD_STYLE_SETTING), [
400
- "Card style ○ neon ● float ○ mono",
401
- ]);
402
- });
403
-
404
- // ---------------------------------------------------------------------------------------------
405
- // Fallback: one separate line, reported
406
- // ---------------------------------------------------------------------------------------------
407
-
408
- test("without a host line the bridge contributes its own line: native plus every style", () => {
409
- const host = createFakeHost({ rows: rowsWithoutHostLine() });
410
- armBridge(host.ui);
411
- installCardStyleRows();
412
- registerCardStyle(style("loon.cute", { label: "cute" }));
413
-
414
- host.buildMenu();
415
-
416
- assert.equal(cardStyleRowMode(), "separate");
417
- assert.deepEqual(settingLines(host.toggleLines(), CARD_STYLE_FALLBACK_SETTING), [
418
- "Bridge card style ● native ○ cute",
419
- ]);
420
- assert.deepEqual(settingLines(host.toggleLines(), HOST_CARD_STYLE_SETTING), []);
421
- });
422
-
423
- test("the fallback setting and label never collide with the host's own Card style", () => {
424
- const host = createFakeHost({ rows: rowsWithoutHostLine() });
425
- armBridge(host.ui);
426
- installCardStyleRows();
427
- registerCardStyle(style("loon.cute", { label: "cute" }));
428
- host.buildMenu();
429
-
430
- const rows = host.liveRows().filter((row) => row.choice?.setting === CARD_STYLE_FALLBACK_SETTING);
431
- assert.equal(rows.length, 2);
432
- assert.notEqual(CARD_STYLE_FALLBACK_SETTING, HOST_CARD_STYLE_SETTING);
433
- const labels = rows.map((row) => (typeof row.label === "function" ? row.label() : row.label));
434
- assert.deepEqual(labels, [CARD_STYLE_ROW_LABEL, "cute"], "the line's name, then the style's own");
435
- for (const row of rows) {
436
- assert.equal(row.category, "Cards");
437
- assert.equal((labels.find((label) => label?.startsWith("Card style")) ?? undefined), undefined);
438
- }
439
- });
440
-
441
- test("in fallback mode native is the default and pressing it persists 'native'", async () => {
442
- const host = createFakeHost({ rows: rowsWithoutHostLine() });
443
- armBridge(host.ui);
444
- installCardStyleRows();
445
- registerCardStyle(style("loon.cute", { label: "cute" }));
446
- host.buildMenu();
447
- assert.deepEqual(settingLines(host.toggleLines(), CARD_STYLE_FALLBACK_SETTING), [
448
- "Bridge card style ● native ○ cute",
449
- ]);
450
-
451
- await host.pressRow(
452
- (row) => row.choice?.setting === CARD_STYLE_FALLBACK_SETTING && row.choice?.value === "cute",
453
- );
454
- assert.equal(getCardStyleSelection(), "loon.cute");
455
- await host.pressRow(
456
- (row) => row.choice?.setting === CARD_STYLE_FALLBACK_SETTING && row.choice?.value === "native",
457
- );
458
- assert.equal(getCardStyleSelection(), "native");
459
- });
460
-
461
- test("neither mode is reported as a duplicate setting by /bridge-status", () => {
462
- const host = createFakeHost({ rows: rowsWithHostLine() });
463
- armBridge(host.ui);
464
- installCardStyleRows();
465
- registerCardStyle(style("loon.cute", { label: "cute" }));
466
- host.buildMenu();
467
- assert.deepEqual(collectStatus().duplicates, [], "extended: one line, one contributor");
468
-
469
- const fallbackHost = createFakeHost({ rows: rowsWithoutHostLine() });
470
- resetCaptureStateForTests();
471
- resetRowBuildStateForTests();
472
- armBridge(fallbackHost.ui);
473
- fallbackHost.buildMenu();
474
- assert.deepEqual(collectStatus().duplicates, [], "separate: our own line, one contributor");
475
- });
476
-
477
- test("an array that cannot be spliced degrades to the fallback instead of throwing", () => {
478
- registerCardStyle(style("loon.cute", { label: "cute" }));
479
- installCardStyleRows();
480
- const rows = Object.freeze(rowsWithHostLine()) as FakeRow[];
481
-
482
- assert.equal(extendCardStyleLine(rows), "separate", "the outcome says which line the user got");
483
- assert.deepEqual(
484
- rows.filter((row) => row.choice?.setting === HOST_CARD_STYLE_SETTING).map((row) => row.choice?.value),
485
- ["neon", "float"],
486
- "and the host's own rows were left exactly where they were",
487
- );
488
- assert.equal(getCardStyleSelection(), "native");
489
- });
490
-
491
- // ---------------------------------------------------------------------------------------------
492
- // Which provider exists
493
- // ---------------------------------------------------------------------------------------------
494
-
495
- test("the fallback provider is registered only while the separate line is what the user sees", () => {
496
- const host = createFakeHost({ rows: rowsWithHostLine() });
497
- armBridge(host.ui);
498
- installCardStyleRows();
499
- const providerIds = (): string[] =>
500
- snapshotProviders()
501
- .map((provider: RowProvider) => provider.id)
502
- .filter((id) => id === CARD_STYLE_ROWS_PROVIDER_ID);
503
-
504
- assert.deepEqual(providerIds(), [CARD_STYLE_ROWS_PROVIDER_ID], "wired, before any capture");
505
- host.buildMenu();
506
- assert.deepEqual(providerIds(), [], "the host line won: our separate line is gone");
507
-
508
- extendCardStyleLine(rowsWithoutHostLine());
509
- assert.deepEqual(providerIds(), [CARD_STYLE_ROWS_PROVIDER_ID], "and comes back with it");
510
- });