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.
- package/LICENSE +202 -0
- package/README.md +91 -722
- package/docs/contract.md +92 -35
- package/package.json +13 -1
- package/src/api.ts +1 -1
- package/.codegraph/.gitignore +0 -5
- package/PRD.md +0 -144
- package/examples/bash-card.ts +0 -1694
- package/gentle-shell-bridge-0.3.0.tgz +0 -0
- package/odd/tasks/palette-sections.md +0 -30
- package/odd/tasks/plugin-keymap.md +0 -32
- package/odd/tasks/plugin-surface-contract.md +0 -55
- package/test/api.test.ts +0 -395
- package/test/apply-on-move.test.ts +0 -477
- package/test/arm.test.ts +0 -220
- package/test/bridge-ready.test.ts +0 -309
- package/test/bus.test.ts +0 -165
- package/test/button-rows.test.ts +0 -409
- package/test/capture.test.ts +0 -356
- package/test/card-style-rows.test.ts +0 -510
- package/test/card-style-transform.test.ts +0 -379
- package/test/card-style-wiring.test.ts +0 -258
- package/test/card-styles-e2e.test.ts +0 -491
- package/test/card-styles.test.ts +0 -324
- package/test/card-transcript-path.test.ts +0 -178
- package/test/config.test.ts +0 -93
- package/test/e2e.test.ts +0 -443
- package/test/example-bash-identity.test.ts +0 -372
- package/test/example-bash-matrix.test.ts +0 -856
- package/test/example-designs.test.ts +0 -734
- package/test/example-neon-transform.test.ts +0 -283
- package/test/example-plain-style.test.ts +0 -1268
- package/test/extension.test.ts +0 -160
- package/test/fake-events.ts +0 -58
- package/test/fake-host.ts +0 -211
- package/test/fake-sidebar.ts +0 -516
- package/test/fake-transcript.ts +0 -486
- package/test/frame-telemetry.test.ts +0 -417
- package/test/hook.test.ts +0 -196
- package/test/manifest.test.ts +0 -105
- package/test/new-category.test.ts +0 -979
- package/test/notices.test.ts +0 -328
- package/test/plugin-integration-lines.test.ts +0 -979
- package/test/plugin-surface.test.ts +0 -152
- package/test/presence.test.ts +0 -459
- package/test/rail-surfaces.test.ts +0 -457
- package/test/rail-width.test.ts +0 -423
- package/test/regression.test.ts +0 -483
- package/test/repaint-api.test.ts +0 -200
- package/test/reserved-categories.test.ts +0 -361
- package/test/row-labels.test.ts +0 -284
- package/test/row-names.test.ts +0 -473
- package/test/row-origin.test.ts +0 -395
- package/test/section-interaction.test.ts +0 -364
- package/test/section-panel.test.ts +0 -403
- package/test/sections-rows.test.ts +0 -291
- package/test/sidebar-backend.test.ts +0 -471
- package/test/sidebar-e2e.test.ts +0 -331
- package/test/sidebar-hover.test.ts +0 -276
- package/test/sidebar-independent.test.ts +0 -349
- package/test/sidebar-render.test.ts +0 -281
- package/test/sidebar-sections.test.ts +0 -245
- package/test/status-card-styles.test.ts +0 -201
- package/test/status-section-frame.test.ts +0 -181
- package/test/status-sections.test.ts +0 -243
- package/test/status.test.ts +0 -209
- package/test/store.test.ts +0 -192
- package/test/surface-repaint-cost.test.ts +0 -520
- package/test/tool-design-chain.test.ts +0 -483
- package/test/tool-design-cost.test.ts +0 -473
- package/test/tool-design-rows.test.ts +0 -359
- package/test/tool-designs.test.ts +0 -745
- package/test/transform-pipeline.test.ts +0 -465
- package/test/transforms-bus.test.ts +0 -233
- package/test/verifier-width-regressions.test.ts +0 -164
- 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
|
-
});
|