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.
- package/LICENSE +202 -0
- package/docs/contract.md +1 -1
- 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,379 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The line transformer (S3, S6, S12): detection, extraction, repaint, and every pass-through.
|
|
3
|
-
*
|
|
4
|
-
* Everything here runs against `test/fake-transcript.ts`, whose rows are real `float` output, so the
|
|
5
|
-
* detector is proven on the grammar it was written from rather than on a simplified copy of it.
|
|
6
|
-
*
|
|
7
|
-
* The bounded bets in this suite, stated so a reviewer can see them:
|
|
8
|
-
* - a pass-through returns the SAME array instance (S3: "the same array reference, untouched");
|
|
9
|
-
* - a card is a run of float rows: padding, header, optional separator, body, padding — the
|
|
10
|
-
* empty-body card being exactly three rows;
|
|
11
|
-
* - `neon` rows start with `╭`, `│` or `╰` and are therefore never float cards;
|
|
12
|
-
* - a style that throws leaves the WHOLE frame alone and is recorded (S6).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import assert from "node:assert/strict";
|
|
16
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
17
|
-
import { tmpdir } from "node:os";
|
|
18
|
-
import { join } from "node:path";
|
|
19
|
-
import test from "node:test";
|
|
20
|
-
|
|
21
|
-
import { registerCardStyle, resetBusForTests, resetCardStylesForTests } from "../src/bus.ts";
|
|
22
|
-
import {
|
|
23
|
-
getCardStyleTransformFailures,
|
|
24
|
-
resetCardStyleTransformForTests,
|
|
25
|
-
transformCardLines,
|
|
26
|
-
} from "../src/card-style-transform.ts";
|
|
27
|
-
import { setCardStyleSelection } from "../src/card-styles.ts";
|
|
28
|
-
import { resetStoreStateForTests } from "../src/store.ts";
|
|
29
|
-
import type {
|
|
30
|
-
CardFrameBottomContext,
|
|
31
|
-
CardFrameLineContext,
|
|
32
|
-
CardFrameTopContext,
|
|
33
|
-
CardStyleRegistration,
|
|
34
|
-
} from "../src/types.ts";
|
|
35
|
-
import {
|
|
36
|
-
BASH_CARD,
|
|
37
|
-
BLANK_LINE,
|
|
38
|
-
CONVERSATION,
|
|
39
|
-
NEON_CARD,
|
|
40
|
-
PROSE_LINE,
|
|
41
|
-
READ_CARD,
|
|
42
|
-
SUBTITLE_CARD,
|
|
43
|
-
TRUNCATED_CARD,
|
|
44
|
-
stripAnsi,
|
|
45
|
-
} from "./fake-transcript.ts";
|
|
46
|
-
|
|
47
|
-
let configHome: string;
|
|
48
|
-
let previousConfigHome: string | undefined;
|
|
49
|
-
|
|
50
|
-
test.before(() => {
|
|
51
|
-
configHome = mkdtempSync(join(tmpdir(), "gsb-transform-"));
|
|
52
|
-
previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
53
|
-
process.env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test.after(() => {
|
|
57
|
-
if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
58
|
-
else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
|
|
59
|
-
rmSync(configHome, { recursive: true, force: true });
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test.beforeEach(() => {
|
|
63
|
-
rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
|
|
64
|
-
resetBusForTests();
|
|
65
|
-
resetCardStylesForTests();
|
|
66
|
-
resetCardStyleTransformForTests();
|
|
67
|
-
resetStoreStateForTests();
|
|
68
|
-
setCardStyleSelection("native");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/** What one style was asked to draw, in call order. */
|
|
72
|
-
interface Recorded {
|
|
73
|
-
top: CardFrameTopContext[];
|
|
74
|
-
line: CardFrameLineContext[];
|
|
75
|
-
bottom: CardFrameBottomContext[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** A style with a frame a test can read at a glance, and a log of every call it received. */
|
|
79
|
-
function recordingStyle(id = "loon.cute", label = "cute"): {
|
|
80
|
-
style: CardStyleRegistration;
|
|
81
|
-
calls: Recorded;
|
|
82
|
-
} {
|
|
83
|
-
const calls: Recorded = { top: [], line: [], bottom: [] };
|
|
84
|
-
return {
|
|
85
|
-
calls,
|
|
86
|
-
style: {
|
|
87
|
-
id,
|
|
88
|
-
label,
|
|
89
|
-
frame: {
|
|
90
|
-
top: (ctx) => {
|
|
91
|
-
calls.top.push(ctx);
|
|
92
|
-
return `+${ctx.title ?? ""}${ctx.hint === undefined ? "" : `|${ctx.hint}`}`;
|
|
93
|
-
},
|
|
94
|
-
line: (ctx) => {
|
|
95
|
-
calls.line.push(ctx);
|
|
96
|
-
return `> ${ctx.text}`;
|
|
97
|
-
},
|
|
98
|
-
bottom: (ctx) => {
|
|
99
|
-
calls.bottom.push(ctx);
|
|
100
|
-
return "-";
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Visible width of a row, ANSI ignored: the rows this suite produces are narrow single-width. */
|
|
108
|
-
function widthOf(line: string): number {
|
|
109
|
-
return [...stripAnsi(line)].length;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---------------------------------------------------------------------------------------------
|
|
113
|
-
// Pass-through: the same array instance, untouched
|
|
114
|
-
// ---------------------------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
test("native selection is a pass-through: the same array reference (S3)", () => {
|
|
117
|
-
const lines = [...CONVERSATION];
|
|
118
|
-
|
|
119
|
-
const output = transformCardLines(lines, 40);
|
|
120
|
-
|
|
121
|
-
assert.equal(output, lines, "identity, not a copy");
|
|
122
|
-
assert.deepEqual(output, [...CONVERSATION]);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("a selection that names no registered style is a pass-through too (S12)", () => {
|
|
126
|
-
registerCardStyle(recordingStyle().style);
|
|
127
|
-
setCardStyleSelection("loon.gone");
|
|
128
|
-
const lines = [...CONVERSATION];
|
|
129
|
-
|
|
130
|
-
assert.equal(transformCardLines(lines, 40), lines, "missing degrades to native, not to a guess");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("a frame with no card is returned untouched even with a style selected", () => {
|
|
134
|
-
registerCardStyle(recordingStyle().style);
|
|
135
|
-
setCardStyleSelection("loon.cute");
|
|
136
|
-
const lines = [PROSE_LINE, BLANK_LINE, PROSE_LINE];
|
|
137
|
-
|
|
138
|
-
assert.equal(transformCardLines(lines, 40), lines);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// ---------------------------------------------------------------------------------------------
|
|
142
|
-
// Repaint
|
|
143
|
-
// ---------------------------------------------------------------------------------------------
|
|
144
|
-
|
|
145
|
-
test("a float card is reframed by the selected style (S3)", () => {
|
|
146
|
-
const { style, calls } = recordingStyle();
|
|
147
|
-
registerCardStyle(style);
|
|
148
|
-
setCardStyleSelection(style.id);
|
|
149
|
-
|
|
150
|
-
const output = transformCardLines([...READ_CARD], 40);
|
|
151
|
-
|
|
152
|
-
assert.deepEqual(output, ["+read|ctrl+o", "> config.ts", "-"]);
|
|
153
|
-
assert.deepEqual(calls.top, [{ width: 40, title: "read", hint: "ctrl+o" }], "one extraction");
|
|
154
|
-
assert.deepEqual(calls.line, [{ text: "config.ts", width: 40, first: true }]);
|
|
155
|
-
assert.deepEqual(calls.bottom, [{ width: 40 }]);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("the content width the host rendered at is the width the style is told", () => {
|
|
159
|
-
const { style, calls } = recordingStyle();
|
|
160
|
-
registerCardStyle(style);
|
|
161
|
-
setCardStyleSelection(style.id);
|
|
162
|
-
|
|
163
|
-
transformCardLines([...READ_CARD], 39);
|
|
164
|
-
|
|
165
|
-
assert.equal(calls.top[0]?.width, 39);
|
|
166
|
-
assert.equal(calls.line[0]?.width, 39);
|
|
167
|
-
assert.equal(calls.bottom[0]?.width, 39);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("an empty-body card is padding, header, closer: no line calls (S3)", () => {
|
|
171
|
-
const { style, calls } = recordingStyle();
|
|
172
|
-
registerCardStyle(style);
|
|
173
|
-
setCardStyleSelection(style.id);
|
|
174
|
-
|
|
175
|
-
const output = transformCardLines([...BASH_CARD], 40);
|
|
176
|
-
|
|
177
|
-
assert.deepEqual(output, ["+bash", "-"]);
|
|
178
|
-
assert.deepEqual(calls.line, [], "there was no body to draw");
|
|
179
|
-
assert.deepEqual(calls.top, [{ width: 40, title: "bash", hint: undefined }]);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test("a neon card is not a float card: it is left byte-identical", () => {
|
|
183
|
-
const { style, calls } = recordingStyle();
|
|
184
|
-
registerCardStyle(style);
|
|
185
|
-
setCardStyleSelection(style.id);
|
|
186
|
-
const lines = [...NEON_CARD];
|
|
187
|
-
|
|
188
|
-
const output = transformCardLines(lines, 40);
|
|
189
|
-
|
|
190
|
-
assert.equal(output, lines, "nothing changed, so nothing was copied");
|
|
191
|
-
assert.deepEqual(calls.top, [], "and no style was consulted about a card it did not draw");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("non-card rows between and around cards are returned byte-identical (S3.4)", () => {
|
|
195
|
-
const { style } = recordingStyle();
|
|
196
|
-
registerCardStyle(style);
|
|
197
|
-
setCardStyleSelection(style.id);
|
|
198
|
-
|
|
199
|
-
const output = transformCardLines([...CONVERSATION], 40);
|
|
200
|
-
|
|
201
|
-
assert.deepEqual(output.slice(0, 2), [PROSE_LINE, BLANK_LINE], "the prefix is byte-identical");
|
|
202
|
-
assert.equal(
|
|
203
|
-
output.filter((line) => line === BLANK_LINE).length,
|
|
204
|
-
CONVERSATION.filter((line) => line === BLANK_LINE).length,
|
|
205
|
-
"the blank rows between cards survive, and no new one appears",
|
|
206
|
-
);
|
|
207
|
-
// Three cards in the conversation fixture, each drawn as top + body + bottom.
|
|
208
|
-
assert.deepEqual(
|
|
209
|
-
output.filter((line) => line === "-").length,
|
|
210
|
-
3,
|
|
211
|
-
"every float card in the frame was reframed",
|
|
212
|
-
);
|
|
213
|
-
assert.equal(output.at(-1), "-", "the frame ends with the last card's closer, as the host drew it");
|
|
214
|
-
assert.equal(output.includes(PROSE_LINE), true);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("a subtitle is not mistaken for a hint: the whole heading becomes the title", () => {
|
|
218
|
-
const { style, calls } = recordingStyle();
|
|
219
|
-
registerCardStyle(style);
|
|
220
|
-
setCardStyleSelection(style.id);
|
|
221
|
-
|
|
222
|
-
transformCardLines([...SUBTITLE_CARD], 40);
|
|
223
|
-
|
|
224
|
-
assert.deepEqual(
|
|
225
|
-
calls.top,
|
|
226
|
-
[{ width: 40, title: "read · config.ts", hint: undefined }],
|
|
227
|
-
"the muted run is a subtitle, not a right-aligned hint; no split is guessed",
|
|
228
|
-
);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("a truncated body row keeps its visible text and loses nothing", () => {
|
|
232
|
-
const { style, calls } = recordingStyle();
|
|
233
|
-
registerCardStyle(style);
|
|
234
|
-
setCardStyleSelection(style.id);
|
|
235
|
-
|
|
236
|
-
transformCardLines([...TRUNCATED_CARD], 40);
|
|
237
|
-
|
|
238
|
-
assert.equal(calls.line.length, 1);
|
|
239
|
-
assert.equal(calls.line[0]?.text, "npm run typecheck exits zero and …", "the host's ellipsis stays");
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("only the selected style draws; another registered style is never called (S12)", () => {
|
|
243
|
-
const first = recordingStyle("loon.cute", "cute");
|
|
244
|
-
const second = recordingStyle("pix.mono", "mono");
|
|
245
|
-
registerCardStyle(first.style);
|
|
246
|
-
registerCardStyle(second.style);
|
|
247
|
-
setCardStyleSelection("pix.mono");
|
|
248
|
-
|
|
249
|
-
const output = transformCardLines([...READ_CARD], 40);
|
|
250
|
-
|
|
251
|
-
assert.deepEqual(output, ["+read|ctrl+o", "> config.ts", "-"]);
|
|
252
|
-
assert.deepEqual(first.calls, { top: [], line: [], bottom: [] }, "the unselected style stayed idle");
|
|
253
|
-
assert.equal(second.calls.top.length, 1);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// ---------------------------------------------------------------------------------------------
|
|
257
|
-
// Width discipline
|
|
258
|
-
// ---------------------------------------------------------------------------------------------
|
|
259
|
-
|
|
260
|
-
test("a line wider than the host's width is clipped, never emitted as is (S3)", () => {
|
|
261
|
-
const { style } = recordingStyle();
|
|
262
|
-
registerCardStyle({
|
|
263
|
-
...style,
|
|
264
|
-
frame: {
|
|
265
|
-
top: () => "+".repeat(120),
|
|
266
|
-
line: (ctx) => `> ${ctx.text} ${"x".repeat(200)}`,
|
|
267
|
-
bottom: () => "-".repeat(300),
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
setCardStyleSelection(style.id);
|
|
271
|
-
|
|
272
|
-
const output = transformCardLines([...READ_CARD], 40);
|
|
273
|
-
|
|
274
|
-
const clippedBody = `> config.ts ${"x".repeat(200)}`.slice(0, 40);
|
|
275
|
-
assert.deepEqual(output, ["+".repeat(40), clippedBody, "-".repeat(40)]);
|
|
276
|
-
for (const line of output) assert.ok(widthOf(line) <= 40, `${widthOf(line)} <= 40`);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("a style that fits is never padded or rewritten", () => {
|
|
280
|
-
const { style } = recordingStyle();
|
|
281
|
-
registerCardStyle(style);
|
|
282
|
-
setCardStyleSelection(style.id);
|
|
283
|
-
|
|
284
|
-
const output = transformCardLines([...READ_CARD], 40);
|
|
285
|
-
|
|
286
|
-
assert.deepEqual(output, ["+read|ctrl+o", "> config.ts", "-"], "clipping only ever removes");
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// ---------------------------------------------------------------------------------------------
|
|
290
|
-
// Failure is a pass-through (S6)
|
|
291
|
-
// ---------------------------------------------------------------------------------------------
|
|
292
|
-
|
|
293
|
-
test("a throwing top leaves the whole frame identical and is recorded", () => {
|
|
294
|
-
registerCardStyle({
|
|
295
|
-
id: "loon.broken",
|
|
296
|
-
label: "broken",
|
|
297
|
-
frame: {
|
|
298
|
-
top: () => {
|
|
299
|
-
throw new Error("top exploded");
|
|
300
|
-
},
|
|
301
|
-
line: (ctx) => ctx.text,
|
|
302
|
-
bottom: () => "-",
|
|
303
|
-
},
|
|
304
|
-
});
|
|
305
|
-
setCardStyleSelection("loon.broken");
|
|
306
|
-
const lines = [...CONVERSATION];
|
|
307
|
-
|
|
308
|
-
const output = transformCardLines(lines, 40);
|
|
309
|
-
|
|
310
|
-
assert.equal(output, lines, "the original array, not a partially repainted one");
|
|
311
|
-
assert.deepEqual(output, [...CONVERSATION]);
|
|
312
|
-
assert.deepEqual(getCardStyleTransformFailures(), [
|
|
313
|
-
{ styleId: "loon.broken", message: "top exploded" },
|
|
314
|
-
]);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("a throwing line is recorded and passes the frame through", () => {
|
|
318
|
-
registerCardStyle({
|
|
319
|
-
id: "loon.broken",
|
|
320
|
-
label: "broken",
|
|
321
|
-
frame: {
|
|
322
|
-
top: () => "+",
|
|
323
|
-
line: () => {
|
|
324
|
-
throw new Error("line exploded");
|
|
325
|
-
},
|
|
326
|
-
bottom: () => "-",
|
|
327
|
-
},
|
|
328
|
-
});
|
|
329
|
-
setCardStyleSelection("loon.broken");
|
|
330
|
-
const lines = [...READ_CARD];
|
|
331
|
-
|
|
332
|
-
assert.equal(transformCardLines(lines, 40), lines);
|
|
333
|
-
assert.deepEqual(getCardStyleTransformFailures(), [
|
|
334
|
-
{ styleId: "loon.broken", message: "line exploded" },
|
|
335
|
-
]);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("a style that threw does not poison the next style (S6, S12)", () => {
|
|
339
|
-
registerCardStyle({
|
|
340
|
-
id: "loon.broken",
|
|
341
|
-
label: "broken",
|
|
342
|
-
frame: {
|
|
343
|
-
top: () => {
|
|
344
|
-
throw new Error("boom");
|
|
345
|
-
},
|
|
346
|
-
line: (ctx) => ctx.text,
|
|
347
|
-
bottom: () => "-",
|
|
348
|
-
},
|
|
349
|
-
});
|
|
350
|
-
const good = recordingStyle("loon.cute", "cute");
|
|
351
|
-
registerCardStyle(good.style);
|
|
352
|
-
setCardStyleSelection("loon.broken");
|
|
353
|
-
transformCardLines([...READ_CARD], 40);
|
|
354
|
-
|
|
355
|
-
setCardStyleSelection("loon.cute");
|
|
356
|
-
|
|
357
|
-
assert.deepEqual(transformCardLines([...READ_CARD], 40), ["+read|ctrl+o", "> config.ts", "-"]);
|
|
358
|
-
assert.equal(good.calls.top.length, 1, "the healthy style drew, in the same process");
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
test("a failure is remembered across frames until the seam clears it", () => {
|
|
362
|
-
registerCardStyle({
|
|
363
|
-
id: "loon.broken",
|
|
364
|
-
label: "broken",
|
|
365
|
-
frame: {
|
|
366
|
-
top: () => {
|
|
367
|
-
throw new Error("boom");
|
|
368
|
-
},
|
|
369
|
-
line: (ctx) => ctx.text,
|
|
370
|
-
bottom: () => "-",
|
|
371
|
-
},
|
|
372
|
-
});
|
|
373
|
-
setCardStyleSelection("loon.broken");
|
|
374
|
-
transformCardLines([...READ_CARD], 40);
|
|
375
|
-
|
|
376
|
-
resetCardStyleTransformForTests();
|
|
377
|
-
|
|
378
|
-
assert.deepEqual(getCardStyleTransformFailures(), []);
|
|
379
|
-
});
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wiring the transform to a live `tui` (S5, S4): the path `src/card-styles.ts` proved, armed from the
|
|
3
|
-
* one surface that hands this package a real TUI, with the sidebar backend's marker/generation
|
|
4
|
-
* discipline.
|
|
5
|
-
*
|
|
6
|
-
* The fixture models Pi's tree (`test/fake-transcript.ts` documents every citation), so these tests
|
|
7
|
-
* prove the real walk: `tui.children[0]` → the document → `children[2]` first, and
|
|
8
|
-
* `tui.layoutRoot` → its layout entries → the primary `ScrollView` → its child → `children[2]` as the
|
|
9
|
-
* secondary. Every absence is a reported outcome, never a throw and never a guess.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { mkdtempSync, 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 { registerCardStyle, resetBusForTests, resetCardStylesForTests } from "../src/bus.ts";
|
|
19
|
-
import { getCardStyleLastOutcome, resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
|
|
20
|
-
import {
|
|
21
|
-
disableCardStyleTransform,
|
|
22
|
-
getCardStyleWiringDiagnostics,
|
|
23
|
-
installCardStyleTransform,
|
|
24
|
-
resetCardStyleWiringForTests,
|
|
25
|
-
} from "../src/card-style-wiring.ts";
|
|
26
|
-
import { setCardStyleSelection } from "../src/card-styles.ts";
|
|
27
|
-
import { armSidebar } from "../src/extension.ts";
|
|
28
|
-
import { resetNoticesForTests } from "../src/notices.ts";
|
|
29
|
-
import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
|
|
30
|
-
import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
|
|
31
|
-
import { resetStoreStateForTests } from "../src/store.ts";
|
|
32
|
-
import type { CardStyleRegistration } from "../src/types.ts";
|
|
33
|
-
import { READ_CARD, createFakeTranscriptTui } from "./fake-transcript.ts";
|
|
34
|
-
|
|
35
|
-
let configHome: string;
|
|
36
|
-
let previousConfigHome: string | undefined;
|
|
37
|
-
|
|
38
|
-
test.before(() => {
|
|
39
|
-
configHome = mkdtempSync(join(tmpdir(), "gsb-wiring-"));
|
|
40
|
-
previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
41
|
-
process.env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test.after(() => {
|
|
45
|
-
if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
46
|
-
else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
|
|
47
|
-
rmSync(configHome, { recursive: true, force: true });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test.beforeEach(() => {
|
|
51
|
-
rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
|
|
52
|
-
resetBusForTests();
|
|
53
|
-
resetCardStylesForTests();
|
|
54
|
-
resetCardStyleTransformForTests();
|
|
55
|
-
resetCardStyleWiringForTests();
|
|
56
|
-
resetSidebarStateForTests();
|
|
57
|
-
resetSectionFailuresForTests();
|
|
58
|
-
resetNoticesForTests();
|
|
59
|
-
resetStoreStateForTests();
|
|
60
|
-
disableSidebarBackend();
|
|
61
|
-
disableCardStyleTransform();
|
|
62
|
-
setCardStyleSelection("native");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const style = (id: string, label: string): CardStyleRegistration => ({
|
|
66
|
-
id,
|
|
67
|
-
label,
|
|
68
|
-
frame: {
|
|
69
|
-
top: (ctx) => `+${ctx.title ?? ""}${ctx.hint === undefined ? "" : `|${ctx.hint}`}`,
|
|
70
|
-
line: (ctx) => `> ${ctx.text}`,
|
|
71
|
-
bottom: () => "-",
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// ---------------------------------------------------------------------------------------------
|
|
76
|
-
// The proven path
|
|
77
|
-
// ---------------------------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
test("arming wraps the conversation component found through children[0] → document → children[2]", () => {
|
|
80
|
-
const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
81
|
-
const originalRender = tui.chat.render;
|
|
82
|
-
|
|
83
|
-
const result = installCardStyleTransform(tui);
|
|
84
|
-
|
|
85
|
-
assert.deepEqual(result, { installed: true, outcome: "wrapped", component: "chat" });
|
|
86
|
-
assert.notEqual(tui.chat.render, originalRender, "the conversation component is the one wrapped");
|
|
87
|
-
assert.deepEqual(getCardStyleWiringDiagnostics(), { wrapped: 1, component: "chat", lastOutcome: "wrapped" });
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("a selected style repaints the live conversation; native is a pass-through", () => {
|
|
91
|
-
const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
92
|
-
installCardStyleTransform(tui);
|
|
93
|
-
registerCardStyle(style("loon.cute", "cute"));
|
|
94
|
-
|
|
95
|
-
assert.deepEqual(tui.chat.render(40), [...READ_CARD], "native: the host's rows, untouched");
|
|
96
|
-
assert.deepEqual(getCardStyleLastOutcome(), { kind: "pass-through", reason: "native" });
|
|
97
|
-
|
|
98
|
-
setCardStyleSelection("loon.cute");
|
|
99
|
-
|
|
100
|
-
assert.deepEqual(tui.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
|
|
101
|
-
assert.deepEqual(getCardStyleLastOutcome(), { kind: "transformed", styleId: "loon.cute", cards: 1 });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("the layoutRoot hop is the secondary path when children[0] is not the document", () => {
|
|
105
|
-
const mounted = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
106
|
-
// A host whose mounted children are not the document (an older/plainer renderer), but whose
|
|
107
|
-
// layout root still carries the transcript (interactive-mode.js:623-629).
|
|
108
|
-
const tui = { terminal: {}, children: [], layoutRoot: mounted.layoutRoot };
|
|
109
|
-
|
|
110
|
-
const result = installCardStyleTransform(tui);
|
|
111
|
-
|
|
112
|
-
assert.deepEqual(result, { installed: true, outcome: "wrapped", component: "chat" });
|
|
113
|
-
assert.deepEqual(mounted.chat.render(40), [...READ_CARD], "native through the secondary path");
|
|
114
|
-
setCardStyleSelection("loon.cute");
|
|
115
|
-
registerCardStyle(style("loon.cute", "cute"));
|
|
116
|
-
assert.deepEqual(mounted.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("the walk stops at the first shape that is not the document and reports it", () => {
|
|
120
|
-
const mounted = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
121
|
-
// children[0] exists but is not a container, so the primary hop fails and nothing else is there.
|
|
122
|
-
const tui = { terminal: {}, children: [{ render: () => [] }] };
|
|
123
|
-
|
|
124
|
-
assert.deepEqual(installCardStyleTransform(tui), { installed: false, outcome: "no-document" });
|
|
125
|
-
assert.equal(mounted.chat.render, mounted.chat.render, "an unrelated tree is never touched");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ---------------------------------------------------------------------------------------------
|
|
129
|
-
// Every absence is a reported state
|
|
130
|
-
// ---------------------------------------------------------------------------------------------
|
|
131
|
-
|
|
132
|
-
test("no tui is reported, not thrown", () => {
|
|
133
|
-
for (const tui of [undefined, null, 42, "tui", {}]) {
|
|
134
|
-
const result = installCardStyleTransform(tui);
|
|
135
|
-
assert.equal(result.installed, false, `installed must stay false for ${JSON.stringify(tui)}`);
|
|
136
|
-
assert.ok(
|
|
137
|
-
["no-tui", "no-children"].includes(result.outcome),
|
|
138
|
-
`unexpected outcome ${result.outcome} for ${JSON.stringify(tui)}`,
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
assert.deepEqual(getCardStyleWiringDiagnostics(), { wrapped: 0, lastOutcome: "no-children" });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("a tui without a document reports no-document", () => {
|
|
145
|
-
const result = installCardStyleTransform({ terminal: {}, children: [{}] });
|
|
146
|
-
|
|
147
|
-
assert.deepEqual(result, { installed: false, outcome: "no-document" });
|
|
148
|
-
assert.equal(getCardStyleWiringDiagnostics().lastOutcome, "no-document");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("a document without a third child reports no-conversation", () => {
|
|
152
|
-
const result = installCardStyleTransform({ terminal: {}, children: [{ children: [{}, {}] }] });
|
|
153
|
-
|
|
154
|
-
assert.deepEqual(result, { installed: false, outcome: "no-conversation" });
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("a third child that cannot render reports render-missing", () => {
|
|
158
|
-
const result = installCardStyleTransform({
|
|
159
|
-
terminal: {},
|
|
160
|
-
children: [{ children: [{}, {}, { render: "not a function" }] }],
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
assert.deepEqual(result, { installed: false, outcome: "render-missing" });
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("a tui whose children accessor throws is reported with its reason, never thrown", () => {
|
|
167
|
-
const tui = {
|
|
168
|
-
terminal: {},
|
|
169
|
-
get children(): never {
|
|
170
|
-
throw new Error("accessor exploded");
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const result = installCardStyleTransform(tui);
|
|
175
|
-
|
|
176
|
-
assert.equal(result.installed, false);
|
|
177
|
-
assert.equal(result.outcome, "wrong-shape");
|
|
178
|
-
assert.match(result.reason ?? "", /accessor exploded/u);
|
|
179
|
-
assert.match(getCardStyleWiringDiagnostics().lastReason ?? "", /accessor exploded/u);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// ---------------------------------------------------------------------------------------------
|
|
183
|
-
// Idempotency and the generation guard
|
|
184
|
-
// ---------------------------------------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
test("a second arm reports already-wrapped and installs nothing new", () => {
|
|
187
|
-
const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
188
|
-
installCardStyleTransform(tui);
|
|
189
|
-
const wrapped = tui.chat.render;
|
|
190
|
-
|
|
191
|
-
const second = installCardStyleTransform(tui);
|
|
192
|
-
|
|
193
|
-
assert.deepEqual(second, { installed: true, outcome: "already-wrapped", component: "chat" });
|
|
194
|
-
assert.equal(tui.chat.render, wrapped, "one wrapper, not a stack of them");
|
|
195
|
-
assert.equal(getCardStyleWiringDiagnostics().wrapped, 1);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("after disabling, a wrapper foreign code retained is a pass-through (S5)", () => {
|
|
199
|
-
const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
200
|
-
installCardStyleTransform(tui);
|
|
201
|
-
registerCardStyle(style("loon.cute", "cute"));
|
|
202
|
-
setCardStyleSelection("loon.cute");
|
|
203
|
-
const retained = tui.chat.render;
|
|
204
|
-
assert.deepEqual(tui.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
|
|
205
|
-
|
|
206
|
-
const restored = disableCardStyleTransform();
|
|
207
|
-
|
|
208
|
-
assert.equal(restored, 1);
|
|
209
|
-
assert.equal(getCardStyleWiringDiagnostics().wrapped, 0);
|
|
210
|
-
assert.deepEqual(retained(40), [...READ_CARD], "the retained wrapper is inert");
|
|
211
|
-
assert.deepEqual(tui.chat.render(40), [...READ_CARD], "and the host's own render is back");
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// ---------------------------------------------------------------------------------------------
|
|
215
|
-
// Stale selection, end to end (S12)
|
|
216
|
-
// ---------------------------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
test("a disposed style degrades to an untouched frame on the next render (S12)", () => {
|
|
219
|
-
const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
220
|
-
installCardStyleTransform(tui);
|
|
221
|
-
const dispose = registerCardStyle(style("loon.cute", "cute"));
|
|
222
|
-
setCardStyleSelection("loon.cute");
|
|
223
|
-
assert.deepEqual(tui.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
|
|
224
|
-
|
|
225
|
-
dispose();
|
|
226
|
-
|
|
227
|
-
assert.deepEqual(tui.chat.render(40), [...READ_CARD], "no chrome for a style that no longer exists");
|
|
228
|
-
assert.deepEqual(getCardStyleLastOutcome(), { kind: "pass-through", reason: "missing" });
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// ---------------------------------------------------------------------------------------------
|
|
232
|
-
// Where it arms
|
|
233
|
-
// ---------------------------------------------------------------------------------------------
|
|
234
|
-
|
|
235
|
-
test("the widget factory that captures the tui is what arms the transform", () => {
|
|
236
|
-
const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
237
|
-
const factoryCalls: unknown[] = [];
|
|
238
|
-
const ui = {
|
|
239
|
-
setWidget: (key: string, factory: (tuiArg: unknown, theme: unknown) => unknown) => {
|
|
240
|
-
factoryCalls.push(key);
|
|
241
|
-
factory(tui, {});
|
|
242
|
-
},
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
armSidebar(ui);
|
|
246
|
-
|
|
247
|
-
assert.equal(factoryCalls.length, 1, "one factory, as Pi installs it");
|
|
248
|
-
assert.equal(getCardStyleWiringDiagnostics().wrapped, 1, "and the factory armed the transform");
|
|
249
|
-
registerCardStyle(style("loon.cute", "cute"));
|
|
250
|
-
setCardStyleSelection("loon.cute");
|
|
251
|
-
assert.deepEqual(tui.chat.render(40), ["+read|ctrl+o", "> config.ts", "-"]);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("a ui without setWidget arms nothing and does not throw", () => {
|
|
255
|
-
assert.doesNotThrow(() => armSidebar({}));
|
|
256
|
-
assert.equal(getCardStyleWiringDiagnostics().wrapped, 0);
|
|
257
|
-
assert.equal(getCardStyleWiringDiagnostics().lastOutcome, undefined, "never attempted, not failed");
|
|
258
|
-
});
|