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,483 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S10/T14 — the chain test that was missing: the host's resolver runs FIRST and answers bash
|
|
3
|
-
* WITHOUT calling `next()`, so the bridge's resolver is never consulted.
|
|
4
|
-
*
|
|
5
|
-
* Pi composes resolvers in extension load order (`runner.js:550-553`) and offers no way to reorder
|
|
6
|
-
* them (`loader.js:298-302` only pushes). In the user's install gentle-pi is package 0 and this
|
|
7
|
-
* bridge is package 11, and gentle-pi's bash resolver answers `bash` without calling `next()`
|
|
8
|
-
* (`extensions/quiet-tools.ts:910-914`). Every earlier test passed a fake `next()` that always
|
|
9
|
-
* answered, so none of them could observe this.
|
|
10
|
-
*
|
|
11
|
-
* RED (observed before `src/tool-design-adoption.ts` existed): with the design selected, the card
|
|
12
|
-
* still rendered the host's own rows —
|
|
13
|
-
*
|
|
14
|
-
* [
|
|
15
|
-
* "╭─ ✿ bash card ───────────── ctrl+o ╮",
|
|
16
|
-
* "│ \u001b[36m$ ls -la\u001b[0m ",
|
|
17
|
-
* "│ total 4 ",
|
|
18
|
-
* "╰───────────────────────────────────╯",
|
|
19
|
-
* ]
|
|
20
|
-
*
|
|
21
|
-
* instead of the designed heading. That is the user's live report (L3): a selected design, a row
|
|
22
|
-
* that says so, and every card still reading `$ …`.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import assert from "node:assert/strict";
|
|
26
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
27
|
-
import { tmpdir } from "node:os";
|
|
28
|
-
import { join } from "node:path";
|
|
29
|
-
import test from "node:test";
|
|
30
|
-
|
|
31
|
-
import { disableCaptureBackend, resetCaptureStateForTests } from "../src/capture.ts";
|
|
32
|
-
import { resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
|
|
33
|
-
import {
|
|
34
|
-
disableCardStyleTransform,
|
|
35
|
-
installCardStyleTransform,
|
|
36
|
-
resetCardStyleWiringForTests,
|
|
37
|
-
} from "../src/card-style-wiring.ts";
|
|
38
|
-
import { resetHookStateForTests } from "../src/hook.ts";
|
|
39
|
-
import { resetNoticesForTests } from "../src/notices.ts";
|
|
40
|
-
import { resetPluginLinesForTests } from "../src/plugin-lines.ts";
|
|
41
|
-
import { resetRepaintForTests } from "../src/repaint.ts";
|
|
42
|
-
import { isRecord, resetRowBuildStateForTests } from "../src/rows.ts";
|
|
43
|
-
import { collectStatus, formatStatus } from "../src/status.ts";
|
|
44
|
-
import { resetStoreStateForTests } from "../src/store.ts";
|
|
45
|
-
import {
|
|
46
|
-
adoptToolCards,
|
|
47
|
-
getToolDesignAdoptionDiagnostics,
|
|
48
|
-
resetToolDesignAdoptionForTests,
|
|
49
|
-
} from "../src/tool-design-adoption.ts";
|
|
50
|
-
import {
|
|
51
|
-
TOOL_DESIGN_NATIVE,
|
|
52
|
-
getToolDesignFailures,
|
|
53
|
-
registerToolDesign,
|
|
54
|
-
resetToolDesignsForTests,
|
|
55
|
-
setToolDesignSelection,
|
|
56
|
-
} from "../src/tool-designs.ts";
|
|
57
|
-
import {
|
|
58
|
-
getToolDesignWiringDiagnostics,
|
|
59
|
-
resetToolDesignWiringForTests,
|
|
60
|
-
toolDesignResolver,
|
|
61
|
-
} from "../src/tool-design-wiring.ts";
|
|
62
|
-
import type { ToolDesignContext, ToolDesignRegistration } from "../src/types.ts";
|
|
63
|
-
|
|
64
|
-
let configHome: string;
|
|
65
|
-
let previousConfigHome: string | undefined;
|
|
66
|
-
|
|
67
|
-
test.before(() => {
|
|
68
|
-
configHome = mkdtempSync(join(tmpdir(), "gsb-tool-design-chain-"));
|
|
69
|
-
previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
70
|
-
process.env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test.after(() => {
|
|
74
|
-
if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
75
|
-
else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
|
|
76
|
-
rmSync(configHome, { recursive: true, force: true });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const resetAll = (): void => {
|
|
80
|
-
resetToolDesignsForTests();
|
|
81
|
-
resetToolDesignWiringForTests();
|
|
82
|
-
resetToolDesignAdoptionForTests();
|
|
83
|
-
resetRepaintForTests();
|
|
84
|
-
resetStoreStateForTests();
|
|
85
|
-
resetNoticesForTests();
|
|
86
|
-
resetPluginLinesForTests();
|
|
87
|
-
resetRowBuildStateForTests();
|
|
88
|
-
resetCaptureStateForTests();
|
|
89
|
-
resetHookStateForTests();
|
|
90
|
-
resetCardStyleTransformForTests();
|
|
91
|
-
resetCardStyleWiringForTests();
|
|
92
|
-
disableCardStyleTransform();
|
|
93
|
-
disableCaptureBackend();
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
test.beforeEach(resetAll);
|
|
97
|
-
test.afterEach(disableCaptureBackend);
|
|
98
|
-
|
|
99
|
-
const THEME = { name: "fake-theme", fg: (role: string, text: string) => `${role}:${text}` };
|
|
100
|
-
|
|
101
|
-
/** The four rows the host's quiet bash card draws, ANSI and padding included. */
|
|
102
|
-
const BASH_CARD = [
|
|
103
|
-
"╭─ ✿ bash card ───────────── ctrl+o ╮",
|
|
104
|
-
"│ \u001b[36m$ ls -la\u001b[0m ",
|
|
105
|
-
"│ total 4 ",
|
|
106
|
-
"╰───────────────────────────────────╯",
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
/** `BASH_CARD` with only the heading mark rewritten, i.e. what the design draws. */
|
|
110
|
-
const DESIGNED_BASH_CARD = [
|
|
111
|
-
BASH_CARD[0],
|
|
112
|
-
BASH_CARD[1].replace("$ ", "bash "),
|
|
113
|
-
BASH_CARD[2],
|
|
114
|
-
BASH_CARD[3],
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
const BASH_HEADING = "bash heading";
|
|
118
|
-
|
|
119
|
-
/** How many frames the design's `apply` actually saw: two wrappers would double this. */
|
|
120
|
-
let applied = 0;
|
|
121
|
-
|
|
122
|
-
const bashHeadingDesign: ToolDesignRegistration = {
|
|
123
|
-
tool: "bash",
|
|
124
|
-
id: "loon.bash-heading",
|
|
125
|
-
label: BASH_HEADING,
|
|
126
|
-
apply(lines: readonly string[], ctx: ToolDesignContext): string[] {
|
|
127
|
-
applied += 1;
|
|
128
|
-
assert.equal(ctx.tool, "bash");
|
|
129
|
-
return lines.map((line) => (line.includes("$ ") ? line.replace("$ ", "bash ") : line));
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
/** The renderers the host builds for bash: a toolDefinition-shaped object, one component each. */
|
|
134
|
-
function bashRenderers(): { renderShell: string; renderCall: unknown; renderResult: unknown } {
|
|
135
|
-
return {
|
|
136
|
-
renderShell: "default",
|
|
137
|
-
renderCall: () => ({ render: (_width: number) => [...BASH_CARD] }),
|
|
138
|
-
renderResult: () => ({ render: (_width: number) => ["╭─ ✿ bash ─╮", "│ done", "╰──────────╯"] }),
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Pi's resolver chain, exactly as `runner.js:550-553` builds it. */
|
|
143
|
-
type Resolver = (toolName: string, next: () => unknown) => unknown;
|
|
144
|
-
|
|
145
|
-
function resolveChain(resolvers: readonly Resolver[], toolName: string, base: () => unknown): unknown {
|
|
146
|
-
const resolve = (index: number): unknown =>
|
|
147
|
-
index < resolvers.length ? resolvers[index](toolName, () => resolve(index + 1)) : base();
|
|
148
|
-
return resolve(0);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** The host's quiet bash resolver: it answers bash itself and never calls `next()`. */
|
|
152
|
-
const hostQuietResolver: Resolver = (toolName, next) =>
|
|
153
|
-
toolName === "bash" ? bashRenderers() : next();
|
|
154
|
-
|
|
155
|
-
/** The part of pi's `ToolExecutionComponent` this test models. */
|
|
156
|
-
interface FakeCard {
|
|
157
|
-
toolName?: unknown;
|
|
158
|
-
toolDefinition?: unknown;
|
|
159
|
-
component?: { render(width: number): string[] };
|
|
160
|
-
renders: number;
|
|
161
|
-
invalidations: number;
|
|
162
|
-
updateDisplay(): void;
|
|
163
|
-
invalidate(): void;
|
|
164
|
-
render(width: number): string[];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
interface MountOptions {
|
|
168
|
-
toolName?: unknown;
|
|
169
|
-
toolDefinition?: unknown;
|
|
170
|
-
invalidateThrows?: boolean;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Mount a component shaped like pi's `ToolExecutionComponent`: it stores `toolDefinition`, its
|
|
175
|
-
* `updateDisplay()` re-invokes the stored `renderCall`, `invalidate()` calls `updateDisplay()`, and
|
|
176
|
-
* `render()` draws the component the last `updateDisplay()` built.
|
|
177
|
-
*/
|
|
178
|
-
function mountCard(options: MountOptions): FakeCard {
|
|
179
|
-
const card: FakeCard = {
|
|
180
|
-
toolName: options.toolName,
|
|
181
|
-
toolDefinition: options.toolDefinition,
|
|
182
|
-
renders: 0,
|
|
183
|
-
invalidations: 0,
|
|
184
|
-
updateDisplay(): void {
|
|
185
|
-
const renderCall = isRecord(card.toolDefinition) ? card.toolDefinition.renderCall : undefined;
|
|
186
|
-
// SAFETY: the host's `renderCall` is `(args, theme, ctx) => Component`; the test only needs
|
|
187
|
-
// the component it answers.
|
|
188
|
-
card.component =
|
|
189
|
-
typeof renderCall === "function"
|
|
190
|
-
? (renderCall as (...args: unknown[]) => { render(width: number): string[] })(
|
|
191
|
-
{ command: "ls -la" },
|
|
192
|
-
THEME,
|
|
193
|
-
{ args: {} },
|
|
194
|
-
)
|
|
195
|
-
: undefined;
|
|
196
|
-
},
|
|
197
|
-
invalidate(): void {
|
|
198
|
-
card.invalidations += 1;
|
|
199
|
-
if (options.invalidateThrows === true) throw new Error("invalidate exploded");
|
|
200
|
-
card.updateDisplay();
|
|
201
|
-
},
|
|
202
|
-
render(width: number): string[] {
|
|
203
|
-
card.renders += 1;
|
|
204
|
-
return card.component === undefined ? [] : card.component.render(width);
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
card.updateDisplay();
|
|
208
|
-
return card;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ---------------------------------------------------------------------------------------------
|
|
212
|
-
// T14/T15 — the host resolves bash first; adoption is the path that still reaches the card
|
|
213
|
-
// ---------------------------------------------------------------------------------------------
|
|
214
|
-
|
|
215
|
-
test("S10/T14: a design reaches a card when a resolver ahead of the bridge answers bash", () => {
|
|
216
|
-
registerToolDesign(bashHeadingDesign);
|
|
217
|
-
setToolDesignSelection("bash", "loon.bash-heading");
|
|
218
|
-
applied = 0;
|
|
219
|
-
|
|
220
|
-
const bridgeCalls: string[] = [];
|
|
221
|
-
const bridgeResolver: Resolver = (toolName, next) => {
|
|
222
|
-
bridgeCalls.push(toolName);
|
|
223
|
-
return toolDesignResolver(toolName, next as () => never);
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const toolDefinition = resolveChain([hostQuietResolver, bridgeResolver], "bash", () => undefined);
|
|
227
|
-
const card = mountCard({ toolName: "bash", toolDefinition });
|
|
228
|
-
|
|
229
|
-
assert.deepEqual(bridgeCalls, [], "the bridge's resolver is never consulted — the defect");
|
|
230
|
-
assert.deepEqual(card.render(80), BASH_CARD, "the host's own rows, before adoption");
|
|
231
|
-
assert.equal(applied, 0, "no design has been applied yet");
|
|
232
|
-
|
|
233
|
-
const result = adoptToolCards({ children: [card] });
|
|
234
|
-
assert.deepEqual(
|
|
235
|
-
result,
|
|
236
|
-
{ scanned: 1, adopted: 1, already: 0, ignored: 0, failed: 0 },
|
|
237
|
-
"the card after construction is the one adopted",
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
// The SAME already-constructed component, not a fresh one: that is what "already drawn" means.
|
|
241
|
-
assert.deepEqual(card.render(80), DESIGNED_BASH_CARD, "the selected design now reaches the card");
|
|
242
|
-
assert.equal(applied, 1, "the design applied exactly once");
|
|
243
|
-
|
|
244
|
-
// Selecting `native` restores the host's own bytes on the same component (S2).
|
|
245
|
-
setToolDesignSelection("bash", TOOL_DESIGN_NATIVE);
|
|
246
|
-
assert.deepEqual(card.render(80), BASH_CARD, "native restores the host's exact rows");
|
|
247
|
-
assert.equal(applied, 1, "native applies no design at all");
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test("S10/T15: the two paths cooperate — the resolver wraps once, adoption adds nothing", () => {
|
|
251
|
-
registerToolDesign(bashHeadingDesign);
|
|
252
|
-
setToolDesignSelection("bash", "loon.bash-heading");
|
|
253
|
-
applied = 0;
|
|
254
|
-
|
|
255
|
-
// The bridge is FIRST in the chain here, so its resolver wraps the host's renderers.
|
|
256
|
-
const bridgeResolver: Resolver = (toolName, next) => toolDesignResolver(toolName, next as () => never);
|
|
257
|
-
const toolDefinition = resolveChain([bridgeResolver, hostQuietResolver], "bash", () => undefined);
|
|
258
|
-
const card = mountCard({ toolName: "bash", toolDefinition });
|
|
259
|
-
|
|
260
|
-
// Marker on the renderers: the resolver path already wrapped, so adoption must not stack.
|
|
261
|
-
const firstPass = adoptToolCards({ children: [card] });
|
|
262
|
-
assert.deepEqual(
|
|
263
|
-
firstPass,
|
|
264
|
-
{ scanned: 1, adopted: 0, already: 1, ignored: 0, failed: 0 },
|
|
265
|
-
"an already-wrapped card is recognised, not wrapped again",
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
assert.deepEqual(card.render(80), DESIGNED_BASH_CARD, "the design reaches the card exactly once");
|
|
269
|
-
assert.equal(applied, 1, "two wrappers would have applied the design twice");
|
|
270
|
-
|
|
271
|
-
// A second adoption pass (a later frame) is still a no-op for that card.
|
|
272
|
-
const secondPass = adoptToolCards({ children: [card] });
|
|
273
|
-
assert.equal(secondPass.adopted, 0);
|
|
274
|
-
assert.equal(secondPass.already, 1);
|
|
275
|
-
card.render(80);
|
|
276
|
-
assert.equal(applied, 2, "one apply per rendered frame, never two");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("S10/T15: a card built after adoption is adopted on the next pass", () => {
|
|
280
|
-
registerToolDesign(bashHeadingDesign);
|
|
281
|
-
setToolDesignSelection("bash", "loon.bash-heading");
|
|
282
|
-
|
|
283
|
-
const container = { children: [] as FakeCard[] };
|
|
284
|
-
assert.deepEqual(adoptToolCards(container), {
|
|
285
|
-
scanned: 0,
|
|
286
|
-
adopted: 0,
|
|
287
|
-
already: 0,
|
|
288
|
-
ignored: 0,
|
|
289
|
-
failed: 0,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// Pi builds a new card for a new tool call (the same renderers shape, a new component).
|
|
293
|
-
const card = mountCard({ toolName: "bash", toolDefinition: bashRenderers() });
|
|
294
|
-
container.children.push(card);
|
|
295
|
-
assert.equal(adoptToolCards(container).adopted, 1);
|
|
296
|
-
assert.deepEqual(card.render(80), DESIGNED_BASH_CARD, "the new child is adopted on its first pass");
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
test("S10/T15: the conversation render pass adopts BEFORE the host renders its children", () => {
|
|
300
|
-
registerToolDesign(bashHeadingDesign);
|
|
301
|
-
setToolDesignSelection("bash", "loon.bash-heading");
|
|
302
|
-
|
|
303
|
-
// The host's conference component: a container whose children are tool cards, rendered in order.
|
|
304
|
-
const chat = {
|
|
305
|
-
children: [mountCard({ toolName: "bash", toolDefinition: bashRenderers() })],
|
|
306
|
-
render(width: number): string[] {
|
|
307
|
-
return this.children.flatMap((child) => child.render(width));
|
|
308
|
-
},
|
|
309
|
-
};
|
|
310
|
-
const document = { children: [{ render: () => [] }, { render: () => [] }, chat] };
|
|
311
|
-
|
|
312
|
-
assert.deepEqual(installCardStyleTransform({ children: [document] }), {
|
|
313
|
-
installed: true,
|
|
314
|
-
outcome: "wrapped",
|
|
315
|
-
component: "chat",
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// One host render: the wrapper adopts the card, then the host draws it in the selected design.
|
|
319
|
-
assert.deepEqual(chat.render(80), DESIGNED_BASH_CARD);
|
|
320
|
-
assert.deepEqual(getToolDesignAdoptionDiagnostics().adoptedCards, [{ tool: "bash", cards: 1 }]);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// ---------------------------------------------------------------------------------------------
|
|
324
|
-
// T15 — every absence is reported, none is a throw
|
|
325
|
-
// ---------------------------------------------------------------------------------------------
|
|
326
|
-
|
|
327
|
-
test("S10/T15: adoption ignores what is not a card of a tool with designs", () => {
|
|
328
|
-
registerToolDesign(bashHeadingDesign);
|
|
329
|
-
setToolDesignSelection("bash", "loon.bash-heading");
|
|
330
|
-
|
|
331
|
-
const notACard = { children: "nope" };
|
|
332
|
-
assert.deepEqual(adoptToolCards(notACard), {
|
|
333
|
-
scanned: 0,
|
|
334
|
-
adopted: 0,
|
|
335
|
-
already: 0,
|
|
336
|
-
ignored: 0,
|
|
337
|
-
failed: 0,
|
|
338
|
-
});
|
|
339
|
-
assert.deepEqual(adoptToolCards(undefined).ignored, 0, "no container is not a failure");
|
|
340
|
-
|
|
341
|
-
const result = adoptToolCards({
|
|
342
|
-
children: [
|
|
343
|
-
null,
|
|
344
|
-
"row",
|
|
345
|
-
{ toolName: 42, toolDefinition: bashRenderers() },
|
|
346
|
-
{ toolName: "", toolDefinition: bashRenderers() },
|
|
347
|
-
// A real tool with no registered design: never ours to touch.
|
|
348
|
-
{ toolName: "write", toolDefinition: bashRenderers() },
|
|
349
|
-
],
|
|
350
|
-
});
|
|
351
|
-
assert.deepEqual(result, { scanned: 5, adopted: 0, already: 0, ignored: 5, failed: 0 });
|
|
352
|
-
assert.deepEqual(getToolDesignFailures(), [], "ignoring something is not a failure");
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
test("S10/T15: a missing or non-writable toolDefinition is reported, never thrown", () => {
|
|
356
|
-
registerToolDesign(bashHeadingDesign);
|
|
357
|
-
setToolDesignSelection("bash", "loon.bash-heading");
|
|
358
|
-
|
|
359
|
-
const missing = mountCard({ toolName: "bash" });
|
|
360
|
-
const missingResult = adoptToolCards({ children: [missing] });
|
|
361
|
-
assert.equal(missingResult.failed, 1);
|
|
362
|
-
assert.deepEqual(missing.render(80), [], "the host's own (empty) card is what the user sees");
|
|
363
|
-
|
|
364
|
-
const locked = mountCard({ toolName: "bash", toolDefinition: bashRenderers() });
|
|
365
|
-
Object.defineProperty(locked, "toolDefinition", {
|
|
366
|
-
value: locked.toolDefinition,
|
|
367
|
-
writable: false,
|
|
368
|
-
configurable: true,
|
|
369
|
-
enumerable: true,
|
|
370
|
-
});
|
|
371
|
-
const lockedResult = adoptToolCards({ children: [locked] });
|
|
372
|
-
assert.equal(lockedResult.failed, 1, "a refused assignment is reported, not thrown");
|
|
373
|
-
|
|
374
|
-
const failures = getToolDesignFailures();
|
|
375
|
-
assert.equal(failures.length, 2, "one fact per distinct failure message");
|
|
376
|
-
assert.ok(failures.every((failure) => failure.designId === "(adoption)"));
|
|
377
|
-
assert.ok(failures.every((failure) => failure.tool === "bash"));
|
|
378
|
-
assert.match(failures[0].message, /no toolDefinition/u);
|
|
379
|
-
assert.match(failures[1].message, /toolDefinition/u);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
test("S10/T15: a throwing invalidate is reported, and the wrapped card still draws the design", () => {
|
|
383
|
-
registerToolDesign(bashHeadingDesign);
|
|
384
|
-
setToolDesignSelection("bash", "loon.bash-heading");
|
|
385
|
-
applied = 0;
|
|
386
|
-
|
|
387
|
-
const card = mountCard({
|
|
388
|
-
toolName: "bash",
|
|
389
|
-
toolDefinition: bashRenderers(),
|
|
390
|
-
invalidateThrows: true,
|
|
391
|
-
});
|
|
392
|
-
const result = adoptToolCards({ children: [card] });
|
|
393
|
-
assert.equal(result.adopted, 1, "the definition was wrapped before the host's invalidate threw");
|
|
394
|
-
assert.equal(result.failed, 0);
|
|
395
|
-
|
|
396
|
-
const failures = getToolDesignFailures();
|
|
397
|
-
assert.equal(failures.length, 1, "the host's own invalidate failure is reported once");
|
|
398
|
-
assert.match(failures[0].message, /invalidate exploded/u);
|
|
399
|
-
|
|
400
|
-
// The next pass reads the wrapped definition, so the design still reaches the card.
|
|
401
|
-
card.component = undefined;
|
|
402
|
-
card.updateDisplay();
|
|
403
|
-
assert.deepEqual(card.render(80), DESIGNED_BASH_CARD);
|
|
404
|
-
assert.equal(applied, 1);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// ---------------------------------------------------------------------------------------------
|
|
408
|
-
// T20/S12 — the part of the card, on the ADOPTION path
|
|
409
|
-
// ---------------------------------------------------------------------------------------------
|
|
410
|
-
|
|
411
|
-
test("S12/T20: the adoption path hands the design the part it is drawing, on both renderers", () => {
|
|
412
|
-
const parts: string[] = [];
|
|
413
|
-
registerToolDesign({
|
|
414
|
-
...bashHeadingDesign,
|
|
415
|
-
id: "loon.part-probe",
|
|
416
|
-
apply: (lines: readonly string[], ctx: ToolDesignContext): string[] => {
|
|
417
|
-
parts.push(ctx.part);
|
|
418
|
-
return [...lines];
|
|
419
|
-
},
|
|
420
|
-
});
|
|
421
|
-
setToolDesignSelection("bash", "loon.part-probe");
|
|
422
|
-
|
|
423
|
-
const card = mountCard({ toolName: "bash", toolDefinition: bashRenderers() });
|
|
424
|
-
// Adoption wraps the stored definition; the design runs when the host RENDERS the component.
|
|
425
|
-
assert.equal(adoptToolCards({ children: [card] }).adopted, 1);
|
|
426
|
-
card.render(80);
|
|
427
|
-
assert.deepEqual(parts, ["call"], "the adopted card's call renderer says `call`");
|
|
428
|
-
|
|
429
|
-
card.render(80);
|
|
430
|
-
assert.equal(parts.at(-1), "call", "every later call frame says `call` too");
|
|
431
|
-
|
|
432
|
-
// The SAME adopted definition's result renderer says `result`: one wrapper, two parts.
|
|
433
|
-
const renderResult = isRecord(card.toolDefinition) ? card.toolDefinition.renderResult : undefined;
|
|
434
|
-
assert.equal(typeof renderResult, "function", "the result renderer is carried over");
|
|
435
|
-
const component = (renderResult as (...args: unknown[]) => { render(width: number): string[] })(
|
|
436
|
-
{ content: [] },
|
|
437
|
-
{ expanded: false },
|
|
438
|
-
THEME,
|
|
439
|
-
{ args: {} },
|
|
440
|
-
);
|
|
441
|
-
component.render(60);
|
|
442
|
-
|
|
443
|
-
assert.equal(parts.at(-1), "result", "the result renderer says `result`, never `call`");
|
|
444
|
-
assert.deepEqual([...new Set(parts)].sort(), ["call", "result"], "both parts reach the design");
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
// ---------------------------------------------------------------------------------------------
|
|
448
|
-
// T16 — the report says which path ran, per tool
|
|
449
|
-
// ---------------------------------------------------------------------------------------------
|
|
450
|
-
|
|
451
|
-
test("S10/T16: /bridge-status counts resolver-wrapped cards and adopted cards per tool", () => {
|
|
452
|
-
registerToolDesign(bashHeadingDesign);
|
|
453
|
-
setToolDesignSelection("bash", "loon.bash-heading");
|
|
454
|
-
|
|
455
|
-
// Adoption path: a card resolved by the host ahead of the bridge.
|
|
456
|
-
const adoptedCard = mountCard({ toolName: "bash", toolDefinition: bashRenderers() });
|
|
457
|
-
adoptToolCards({ children: [adoptedCard] });
|
|
458
|
-
// Resolver path: a card resolved through the bridge's own wrapper.
|
|
459
|
-
mountCard({
|
|
460
|
-
toolName: "bash",
|
|
461
|
-
toolDefinition: toolDesignResolver("bash", bashRenderers) as unknown,
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
const snapshot = collectStatus();
|
|
465
|
-
assert.ok(snapshot.toolDesigns !== undefined);
|
|
466
|
-
const entry = snapshot.toolDesigns.tools[0];
|
|
467
|
-
assert.equal(entry.wrapper, "mounted", "the resolver path mounted one card");
|
|
468
|
-
assert.equal(entry.cards, 1);
|
|
469
|
-
assert.equal(entry.adopted, 1, "the adoption path adopted the other");
|
|
470
|
-
|
|
471
|
-
// The resolver's diagnostics and the adoption's are separate records of separate paths.
|
|
472
|
-
assert.deepEqual(getToolDesignWiringDiagnostics().wrappedCards, [{ tool: "bash", cards: 1 }]);
|
|
473
|
-
assert.deepEqual(getToolDesignAdoptionDiagnostics().adoptedCards, [{ tool: "bash", cards: 1 }]);
|
|
474
|
-
|
|
475
|
-
const report = formatStatus(snapshot).join("\n");
|
|
476
|
-
assert.match(report, /- bash: loon\.bash-heading, wrapper mounted on 1 card, adopted 1 card/u);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
test("S10/T16: with no design registered the report is unchanged — no tool-design block at all", () => {
|
|
480
|
-
assert.equal(collectStatus().toolDesigns, undefined);
|
|
481
|
-
const report = formatStatus(collectStatus()).join("\n");
|
|
482
|
-
assert.doesNotMatch(report, /tool design/u);
|
|
483
|
-
});
|