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,473 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S13/T23 — the WORK a design switch does, counted instead of timed.
|
|
3
|
-
*
|
|
4
|
-
* Wall-clock numbers measured on real pi-tui and pi's `ToolExecutionComponent` (T22) say the freeze
|
|
5
|
-
* came from two calls the host's own card-style switch never makes:
|
|
6
|
-
*
|
|
7
|
-
* - `tui.invalidate()` clears every mounted component's frame cache, so the next frame re-parses
|
|
8
|
-
* every Markdown message of the transcript (0.06 ms at 50 children, 1.1 ms at 600 for the walk
|
|
9
|
-
* alone, and +709 ms for the frame that re-parses 300 messages with 45-line code blocks);
|
|
10
|
-
* - `requestRender(force = true)` calls `resetRenderState()`, throwing away the differential state so
|
|
11
|
-
* the whole buffer is cleared and rewritten (6.9 MB at 600 children) instead of only the rows that
|
|
12
|
-
* changed.
|
|
13
|
-
*
|
|
14
|
-
* Neither is needed to change a card that is already drawn: the bridge's wrapper renders the HOST's
|
|
15
|
-
* component and applies the live selection on top of whatever rows it answered with — including the
|
|
16
|
-
* rows the host's own `ToolCardRows` cache hands back (`cards-bash-v2` `extensions/quiet-tools.ts`
|
|
17
|
-
* 612-624 keys its frame on width + style + running, and the design is NOT in that key). This file
|
|
18
|
-
* proves that against a host component that caches exactly like that one, and bounds the switch:
|
|
19
|
-
*
|
|
20
|
-
* - the tree is never invalidated, and no prose component re-parses;
|
|
21
|
-
* - exactly one frame is requested, and it is differential;
|
|
22
|
-
* - at most one persisted-selection read per card per frame;
|
|
23
|
-
* - none of those counts grows with the number of Markdown components in the transcript.
|
|
24
|
-
*
|
|
25
|
-
* The bound is call counts, never milliseconds, so it cannot flake on a busy machine.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import assert from "node:assert/strict";
|
|
29
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
30
|
-
import { tmpdir } from "node:os";
|
|
31
|
-
import { join } from "node:path";
|
|
32
|
-
import test from "node:test";
|
|
33
|
-
|
|
34
|
-
import { resetNoticesForTests } from "../src/notices.ts";
|
|
35
|
-
import { resetPluginLinesForTests } from "../src/plugin-lines.ts";
|
|
36
|
-
import {
|
|
37
|
-
getRepaintDiagnostics,
|
|
38
|
-
requestSurfaceRepaint,
|
|
39
|
-
resetRepaintForTests,
|
|
40
|
-
setRepaintHost,
|
|
41
|
-
} from "../src/repaint.ts";
|
|
42
|
-
import { buildRows, resetRowBuildStateForTests } from "../src/rows.ts";
|
|
43
|
-
import { resetStoreStateForTests } from "../src/store.ts";
|
|
44
|
-
import {
|
|
45
|
-
TOOL_DESIGN_NATIVE,
|
|
46
|
-
getToolDesignSelectionReads,
|
|
47
|
-
registerToolDesign,
|
|
48
|
-
resetToolDesignSelectionReads,
|
|
49
|
-
resetToolDesignsForTests,
|
|
50
|
-
setToolDesignSelection,
|
|
51
|
-
} from "../src/tool-designs.ts";
|
|
52
|
-
import {
|
|
53
|
-
installToolDesignRows,
|
|
54
|
-
resetToolDesignRowStateForTests,
|
|
55
|
-
} from "../src/tool-design-rows.ts";
|
|
56
|
-
import { resetToolDesignWiringForTests, toolDesignResolver } from "../src/tool-design-wiring.ts";
|
|
57
|
-
import type { CustomizeRow, ToolDesignContext, ToolDesignRegistration } from "../src/types.ts";
|
|
58
|
-
|
|
59
|
-
let configHome: string;
|
|
60
|
-
let previousConfigHome: string | undefined;
|
|
61
|
-
|
|
62
|
-
test.before(() => {
|
|
63
|
-
configHome = mkdtempSync(join(tmpdir(), "gsb-tool-design-cost-"));
|
|
64
|
-
previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
65
|
-
process.env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test.after(() => {
|
|
69
|
-
if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
70
|
-
else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
|
|
71
|
-
rmSync(configHome, { recursive: true, force: true });
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test.beforeEach(() => {
|
|
75
|
-
rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
|
|
76
|
-
resetToolDesignsForTests();
|
|
77
|
-
resetToolDesignRowStateForTests();
|
|
78
|
-
resetToolDesignWiringForTests();
|
|
79
|
-
resetRepaintForTests();
|
|
80
|
-
resetStoreStateForTests();
|
|
81
|
-
resetNoticesForTests();
|
|
82
|
-
resetPluginLinesForTests();
|
|
83
|
-
resetRowBuildStateForTests();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const WIDTH = 80;
|
|
87
|
-
const THEME = { name: "fake-theme" };
|
|
88
|
-
const BASH_HEADING = "bash heading";
|
|
89
|
-
|
|
90
|
-
/** The host's own rows for one bash card, as `quiet-tools.ts` draws them. */
|
|
91
|
-
const HOST_CARD = [
|
|
92
|
-
"╭─ ✿ bash card ───────────── ctrl+o ╮",
|
|
93
|
-
"│ \u001b[36m$ ls -la\u001b[0m │",
|
|
94
|
-
"╰───────────────────────────────────╯",
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
const DESIGNED_CARD = [
|
|
98
|
-
HOST_CARD[0],
|
|
99
|
-
HOST_CARD[1].replace("$ ", "bash "),
|
|
100
|
-
HOST_CARD[2],
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
/** How many times the design applied since the last reset (one per card per frame while selected). */
|
|
104
|
-
let applied = 0;
|
|
105
|
-
|
|
106
|
-
const bashHeadingDesign: ToolDesignRegistration = {
|
|
107
|
-
tool: "bash",
|
|
108
|
-
id: "loon.bash-heading",
|
|
109
|
-
label: BASH_HEADING,
|
|
110
|
-
apply(lines: readonly string[], ctx: ToolDesignContext): string[] {
|
|
111
|
-
applied += 1;
|
|
112
|
-
assert.equal(ctx.tool, "bash");
|
|
113
|
-
return lines.map((line) => line.replace("$ ", "bash "));
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------------------------
|
|
118
|
-
// The instrumented host: a frame counter on every component, exactly like pi's TUI
|
|
119
|
-
// ---------------------------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
interface Counters {
|
|
122
|
-
/** `tui.invalidate()`: the call that cleared every cache in the tree. */
|
|
123
|
-
treeInvalidations: number;
|
|
124
|
-
/** What each `requestRender()` was called with: `plain` or `forced`. */
|
|
125
|
-
frameRequests: Array<"plain" | "forced">;
|
|
126
|
-
/** A prose row re-parsed its source because its cache was dropped. */
|
|
127
|
-
proseCacheMisses: number;
|
|
128
|
-
/** A prose row had its cache dropped. */
|
|
129
|
-
proseInvalidations: number;
|
|
130
|
-
/** The host card's renderer ran (it may answer from its own cache). */
|
|
131
|
-
hostRenders: number;
|
|
132
|
-
/** The host card answered from its own frame cache (the rows the design must post-process). */
|
|
133
|
-
hostCacheHits: number;
|
|
134
|
-
/** The host card rebuilt its rows (width/style/running changed). */
|
|
135
|
-
hostRebuilds: number;
|
|
136
|
-
/** Cards whose `invalidate()` the bridge called. */
|
|
137
|
-
cardInvalidations: number;
|
|
138
|
-
/** Design applications. */
|
|
139
|
-
designApplies: number;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** A `Markdown`-shaped row: it caches its lines and re-parses only after `invalidate()`. */
|
|
143
|
-
function mountProse(text: string, counters: Counters): { render(width: number): string[]; invalidate(): void } {
|
|
144
|
-
let cached: string[] | undefined;
|
|
145
|
-
return {
|
|
146
|
-
render(width: number): string[] {
|
|
147
|
-
if (cached !== undefined) return cached;
|
|
148
|
-
counters.proseCacheMisses += 1;
|
|
149
|
-
cached = [`${text} `.repeat(4).trim().padEnd(width, " ")];
|
|
150
|
-
return cached;
|
|
151
|
-
},
|
|
152
|
-
invalidate(): void {
|
|
153
|
-
counters.proseInvalidations += 1;
|
|
154
|
-
cached = undefined;
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* A host bash card whose renderer caches its frame the way `ToolCardRows` does
|
|
161
|
-
* (`extensions/quiet-tools.ts:612-624`): keyed on width, the host's own style and whether it is
|
|
162
|
-
* running. The DESIGN is not in that key — which is why the rows it answers with are cached under
|
|
163
|
-
* the previous selection, and why the wrapper must post-process them instead of the tree being
|
|
164
|
-
* invalidated.
|
|
165
|
-
*/
|
|
166
|
-
function hostCardDefinition(counters: Counters): Record<string, unknown> {
|
|
167
|
-
const frame = { width: -1, style: "", lines: [...HOST_CARD] };
|
|
168
|
-
const hostStyle = "float";
|
|
169
|
-
return {
|
|
170
|
-
renderShell: "default",
|
|
171
|
-
renderCall: (..._args: unknown[]) => ({
|
|
172
|
-
render(width: number): string[] {
|
|
173
|
-
counters.hostRenders += 1;
|
|
174
|
-
if (frame.width === width && frame.style === hostStyle) {
|
|
175
|
-
counters.hostCacheHits += 1;
|
|
176
|
-
return frame.lines;
|
|
177
|
-
}
|
|
178
|
-
counters.hostRebuilds += 1;
|
|
179
|
-
frame.width = width;
|
|
180
|
-
frame.style = hostStyle;
|
|
181
|
-
frame.lines = [...HOST_CARD];
|
|
182
|
-
return frame.lines;
|
|
183
|
-
},
|
|
184
|
-
invalidate(): void {
|
|
185
|
-
frame.width = -1;
|
|
186
|
-
},
|
|
187
|
-
}),
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** The part of pi's `ToolExecutionComponent` this test needs: it stores and re-invokes the renderer. */
|
|
192
|
-
interface FakeCard {
|
|
193
|
-
toolName: string;
|
|
194
|
-
toolDefinition: Record<string, unknown>;
|
|
195
|
-
renders: number;
|
|
196
|
-
component?: { render(width: number): string[] };
|
|
197
|
-
updateDisplay(): void;
|
|
198
|
-
invalidate(): void;
|
|
199
|
-
render(width: number): string[];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function mountCard(counters: Counters): FakeCard {
|
|
203
|
-
const card: FakeCard = {
|
|
204
|
-
toolName: "bash",
|
|
205
|
-
toolDefinition: toolDesignResolver("bash", () => hostCardDefinition(counters)) as Record<
|
|
206
|
-
string,
|
|
207
|
-
unknown
|
|
208
|
-
>,
|
|
209
|
-
renders: 0,
|
|
210
|
-
updateDisplay(): void {
|
|
211
|
-
const renderCall = card.toolDefinition.renderCall;
|
|
212
|
-
card.component =
|
|
213
|
-
typeof renderCall === "function"
|
|
214
|
-
? (renderCall as (...args: unknown[]) => { render(width: number): string[] })(
|
|
215
|
-
{ command: "ls -la" },
|
|
216
|
-
THEME,
|
|
217
|
-
{ args: {} },
|
|
218
|
-
)
|
|
219
|
-
: undefined;
|
|
220
|
-
},
|
|
221
|
-
invalidate(): void {
|
|
222
|
-
counters.cardInvalidations += 1;
|
|
223
|
-
card.updateDisplay();
|
|
224
|
-
},
|
|
225
|
-
render(width: number): string[] {
|
|
226
|
-
card.renders += 1;
|
|
227
|
-
return card.component === undefined ? [] : card.component.render(width);
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
card.updateDisplay();
|
|
231
|
-
return card;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
interface Transcript {
|
|
235
|
-
counters: Counters;
|
|
236
|
-
/** The conversation render pass: every mounted child, in order (pi's `Container.render`). */
|
|
237
|
-
drawFrame(): string[][];
|
|
238
|
-
cards: FakeCard[];
|
|
239
|
-
proseInvalidations: () => number;
|
|
240
|
-
proseCacheMisses: () => number;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function transcript(options: { children: number; markdown: number }): Transcript {
|
|
244
|
-
const counters: Counters = {
|
|
245
|
-
treeInvalidations: 0,
|
|
246
|
-
frameRequests: [],
|
|
247
|
-
proseCacheMisses: 0,
|
|
248
|
-
proseInvalidations: 0,
|
|
249
|
-
hostRenders: 0,
|
|
250
|
-
hostCacheHits: 0,
|
|
251
|
-
hostRebuilds: 0,
|
|
252
|
-
cardInvalidations: 0,
|
|
253
|
-
designApplies: 0,
|
|
254
|
-
// The count of design applications is kept by the registered design itself; see `press`.
|
|
255
|
-
} as Counters;
|
|
256
|
-
|
|
257
|
-
const nodes: Array<{ render(width: number): string[]; invalidate?(): void }> = [];
|
|
258
|
-
const cards: FakeCard[] = [];
|
|
259
|
-
const bash = options.children - options.markdown;
|
|
260
|
-
for (let index = 0; index < options.markdown; index += 1) {
|
|
261
|
-
nodes.push(mountProse(`assistant message ${index}`, counters));
|
|
262
|
-
}
|
|
263
|
-
for (let index = 0; index < bash; index += 1) {
|
|
264
|
-
const card = mountCard(counters);
|
|
265
|
-
cards.push(card);
|
|
266
|
-
nodes.push(card);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
setRepaintHost({
|
|
270
|
-
terminal: {},
|
|
271
|
-
invalidate: (): void => {
|
|
272
|
-
counters.treeInvalidations += 1;
|
|
273
|
-
for (const node of nodes) node.invalidate?.();
|
|
274
|
-
},
|
|
275
|
-
requestRender: (force?: boolean): void => {
|
|
276
|
-
counters.frameRequests.push(force === true ? "forced" : "plain");
|
|
277
|
-
},
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
counters,
|
|
282
|
-
drawFrame: () => nodes.map((node) => node.render(WIDTH)),
|
|
283
|
-
cards,
|
|
284
|
-
proseInvalidations: () => counters.proseInvalidations,
|
|
285
|
-
proseCacheMisses: () => counters.proseCacheMisses,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** The menu's live rows, built through the host contract the menu reads. */
|
|
290
|
-
function rows(): CustomizeRow[] {
|
|
291
|
-
installToolDesignRows();
|
|
292
|
-
return buildRows({ theme: undefined, configHome }).rows;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** Press one value of the bash line, exactly as the menu runs a row action. */
|
|
296
|
-
async function press(value: string): Promise<void> {
|
|
297
|
-
const row = rows().find((candidate) => candidate.choice?.value === value);
|
|
298
|
-
assert.ok(row?.action !== undefined, `the menu has a ${JSON.stringify(value)} value`);
|
|
299
|
-
await row.action({
|
|
300
|
-
input: async () => undefined,
|
|
301
|
-
confirm: async () => true,
|
|
302
|
-
capture: async () => undefined,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** Everything one switch costs, as counts. */
|
|
307
|
-
interface SwitchCost {
|
|
308
|
-
treeInvalidations: number;
|
|
309
|
-
frameRequests: Array<"plain" | "forced">;
|
|
310
|
-
selectionReadsInFrame: number;
|
|
311
|
-
designAppliesInFrame: number;
|
|
312
|
-
proseCacheMissesInFrame: number;
|
|
313
|
-
hostCacheHitsInFrame: number;
|
|
314
|
-
cardInvalidations: number;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function measureSwitch(options: { children: number; markdown: number }): Promise<{
|
|
318
|
-
cost: SwitchCost;
|
|
319
|
-
drawn: string[][];
|
|
320
|
-
transcript: Transcript;
|
|
321
|
-
}> {
|
|
322
|
-
registerToolDesign(bashHeadingDesign);
|
|
323
|
-
setToolDesignSelection("bash", TOOL_DESIGN_NATIVE);
|
|
324
|
-
const world = transcript(options);
|
|
325
|
-
// Warm every cache with one frame of the host's own rows.
|
|
326
|
-
world.drawFrame();
|
|
327
|
-
|
|
328
|
-
await press(BASH_HEADING);
|
|
329
|
-
|
|
330
|
-
resetToolDesignSelectionReads();
|
|
331
|
-
const appliesBefore = applied;
|
|
332
|
-
const proseBefore = world.proseCacheMisses();
|
|
333
|
-
const hitsBefore = world.counters.hostCacheHits;
|
|
334
|
-
const drawn = world.drawFrame();
|
|
335
|
-
|
|
336
|
-
return {
|
|
337
|
-
cost: {
|
|
338
|
-
treeInvalidations: world.counters.treeInvalidations,
|
|
339
|
-
frameRequests: [...world.counters.frameRequests],
|
|
340
|
-
selectionReadsInFrame: getToolDesignSelectionReads(),
|
|
341
|
-
designAppliesInFrame: applied - appliesBefore,
|
|
342
|
-
proseCacheMissesInFrame: world.proseCacheMisses() - proseBefore,
|
|
343
|
-
hostCacheHitsInFrame: world.counters.hostCacheHits - hitsBefore,
|
|
344
|
-
cardInvalidations: world.counters.cardInvalidations,
|
|
345
|
-
},
|
|
346
|
-
drawn,
|
|
347
|
-
transcript: world,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ---------------------------------------------------------------------------------------------
|
|
352
|
-
// T23 — the bound
|
|
353
|
-
// ---------------------------------------------------------------------------------------------
|
|
354
|
-
|
|
355
|
-
test("T23/S13: a switch asks for ONE differential frame and invalidates nothing", async () => {
|
|
356
|
-
registerToolDesign(bashHeadingDesign);
|
|
357
|
-
const world = transcript({ children: 6, markdown: 3 });
|
|
358
|
-
world.drawFrame();
|
|
359
|
-
|
|
360
|
-
await press(BASH_HEADING);
|
|
361
|
-
|
|
362
|
-
assert.equal(
|
|
363
|
-
world.counters.treeInvalidations,
|
|
364
|
-
0,
|
|
365
|
-
"the tree is not invalidated: every Markdown row would re-parse and every card would rebuild",
|
|
366
|
-
);
|
|
367
|
-
assert.deepEqual(
|
|
368
|
-
world.counters.frameRequests,
|
|
369
|
-
["plain"],
|
|
370
|
-
"one frame, differential: no forced full redraw of the whole buffer",
|
|
371
|
-
);
|
|
372
|
-
assert.equal(world.counters.cardInvalidations, 0, "no card is invalidated by a selection change");
|
|
373
|
-
|
|
374
|
-
const drawn = world.drawFrame();
|
|
375
|
-
assert.deepEqual(drawn.filter((lines) => lines.length === HOST_CARD.length), [
|
|
376
|
-
DESIGNED_CARD,
|
|
377
|
-
DESIGNED_CARD,
|
|
378
|
-
DESIGNED_CARD,
|
|
379
|
-
], "every already-constructed card draws the design");
|
|
380
|
-
assert.equal(
|
|
381
|
-
world.counters.hostCacheHits,
|
|
382
|
-
3,
|
|
383
|
-
"the host answered from its OWN frame cache; the wrapper post-processed those rows",
|
|
384
|
-
);
|
|
385
|
-
assert.equal(world.counters.hostRebuilds, 3, "the host rebuilt its rows once, at warm-up");
|
|
386
|
-
assert.equal(getRepaintDiagnostics().lastTrigger?.outcome, "repainted");
|
|
387
|
-
|
|
388
|
-
// S2's other half: `native` restores the host's exact bytes on the SAME components.
|
|
389
|
-
await press(TOOL_DESIGN_NATIVE);
|
|
390
|
-
assert.deepEqual(
|
|
391
|
-
world.drawFrame().filter((lines) => lines.length === HOST_CARD.length),
|
|
392
|
-
[HOST_CARD, HOST_CARD, HOST_CARD],
|
|
393
|
-
"native restores the host's own rows, byte for byte",
|
|
394
|
-
);
|
|
395
|
-
assert.equal(world.counters.treeInvalidations, 0, "the way back costs no invalidation either");
|
|
396
|
-
assert.deepEqual(world.counters.frameRequests, ["plain", "plain"], "one frame per press");
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test("T23/S13: the work of a switch does not grow with the number of Markdown rows", async () => {
|
|
400
|
-
const small = await measureSwitch({ children: 6, markdown: 3 });
|
|
401
|
-
await press(TOOL_DESIGN_NATIVE);
|
|
402
|
-
const large = await measureSwitch({ children: 203, markdown: 200 });
|
|
403
|
-
|
|
404
|
-
assert.deepEqual(
|
|
405
|
-
large.cost,
|
|
406
|
-
{
|
|
407
|
-
...small.cost,
|
|
408
|
-
// The host renders once per card per frame in both; that is the switch's real work, and it is
|
|
409
|
-
// per CARD, never per Markdown row. Nothing else may differ.
|
|
410
|
-
},
|
|
411
|
-
"same cards, same frame, same reads: 200 prose rows cost a switch exactly what 3 do",
|
|
412
|
-
);
|
|
413
|
-
assert.equal(large.cost.treeInvalidations, 0);
|
|
414
|
-
assert.deepEqual(large.cost.frameRequests, ["plain"]);
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
test("T23/S13: a switch performs at most one selection read per card per frame", async () => {
|
|
418
|
-
registerToolDesign(bashHeadingDesign);
|
|
419
|
-
const world = transcript({ children: 10, markdown: 6 });
|
|
420
|
-
world.drawFrame();
|
|
421
|
-
|
|
422
|
-
await press(BASH_HEADING);
|
|
423
|
-
|
|
424
|
-
resetToolDesignSelectionReads();
|
|
425
|
-
world.drawFrame();
|
|
426
|
-
const firstFrame = getToolDesignSelectionReads();
|
|
427
|
-
assert.equal(firstFrame, 4, "one read per card, no more");
|
|
428
|
-
|
|
429
|
-
resetToolDesignSelectionReads();
|
|
430
|
-
world.drawFrame();
|
|
431
|
-
assert.equal(getToolDesignSelectionReads(), 4, "and the same on every later frame");
|
|
432
|
-
|
|
433
|
-
// A frame with no design still reads the selection once per card: the bound is about the CARD,
|
|
434
|
-
// not about which value is selected.
|
|
435
|
-
await press(TOOL_DESIGN_NATIVE);
|
|
436
|
-
resetToolDesignSelectionReads();
|
|
437
|
-
world.drawFrame();
|
|
438
|
-
assert.equal(getToolDesignSelectionReads(), 4);
|
|
439
|
-
|
|
440
|
-
// Reads never grow with the transcript's prose, only with its cards.
|
|
441
|
-
const wide = await measureSwitch({ children: 104, markdown: 100 });
|
|
442
|
-
assert.equal(wide.cost.selectionReadsInFrame, 4, "100 prose rows still cost four selection reads");
|
|
443
|
-
assert.ok(
|
|
444
|
-
wide.cost.selectionReadsInFrame <= 4,
|
|
445
|
-
"the bound: at most one read per card per frame",
|
|
446
|
-
);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
test("T23/S13: the prose rows never re-parse for a switch", async () => {
|
|
450
|
-
const { cost } = await measureSwitch({ children: 30, markdown: 25 });
|
|
451
|
-
assert.equal(cost.proseCacheMissesInFrame, 0, "no Markdown row re-parses on a design switch");
|
|
452
|
-
assert.equal(cost.cardInvalidations, 0);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
// S19 replaced the PRESERVE test that pinned the heavy path (`tui.invalidate()` + a forced frame) for
|
|
456
|
-
// bridge-owned rows. Measured with real pi components, the rows are byte-identical without them on the
|
|
457
|
-
// conversation, the rail's host panels and the bridge's sections, and the invalidation was the
|
|
458
|
-
// expensive half (697 ms vs 420 ms for `plain`, 99 ms vs 3 ms for a rail section toggle at 600
|
|
459
|
-
// children). test/surface-repaint-cost.test.ts holds the full bound; this pins the same request here.
|
|
460
|
-
test("T36/S19: the bridge-owned surface repaint is one differential frame too", () => {
|
|
461
|
-
const world = transcript({ children: 2, markdown: 1 });
|
|
462
|
-
world.counters.treeInvalidations = 0;
|
|
463
|
-
world.counters.frameRequests.length = 0;
|
|
464
|
-
|
|
465
|
-
requestSurfaceRepaint({ cause: "card style → neon", surfaces: ["conversation", "rail"] });
|
|
466
|
-
|
|
467
|
-
assert.equal(world.counters.treeInvalidations, 0, "no tree invalidation: the Markdown caches survive");
|
|
468
|
-
assert.deepEqual(world.counters.frameRequests, ["plain"], "one differential frame, not a forced one");
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// Keep the compiler honest about the design type used above.
|
|
472
|
-
const _contextProbe: ToolDesignContext = { width: WIDTH, tool: "bash", theme: THEME, part: "call" };
|
|
473
|
-
void _contextProbe;
|