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