gentle-shell-bridge 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +202 -0
  2. package/docs/contract.md +1 -1
  3. package/package.json +13 -1
  4. package/src/api.ts +1 -1
  5. package/.codegraph/.gitignore +0 -5
  6. package/PRD.md +0 -144
  7. package/examples/bash-card.ts +0 -1694
  8. package/gentle-shell-bridge-0.3.0.tgz +0 -0
  9. package/odd/tasks/palette-sections.md +0 -30
  10. package/odd/tasks/plugin-keymap.md +0 -32
  11. package/odd/tasks/plugin-surface-contract.md +0 -55
  12. package/test/api.test.ts +0 -395
  13. package/test/apply-on-move.test.ts +0 -477
  14. package/test/arm.test.ts +0 -220
  15. package/test/bridge-ready.test.ts +0 -309
  16. package/test/bus.test.ts +0 -165
  17. package/test/button-rows.test.ts +0 -409
  18. package/test/capture.test.ts +0 -356
  19. package/test/card-style-rows.test.ts +0 -510
  20. package/test/card-style-transform.test.ts +0 -379
  21. package/test/card-style-wiring.test.ts +0 -258
  22. package/test/card-styles-e2e.test.ts +0 -491
  23. package/test/card-styles.test.ts +0 -324
  24. package/test/card-transcript-path.test.ts +0 -178
  25. package/test/config.test.ts +0 -93
  26. package/test/e2e.test.ts +0 -443
  27. package/test/example-bash-identity.test.ts +0 -372
  28. package/test/example-bash-matrix.test.ts +0 -856
  29. package/test/example-designs.test.ts +0 -734
  30. package/test/example-neon-transform.test.ts +0 -283
  31. package/test/example-plain-style.test.ts +0 -1268
  32. package/test/extension.test.ts +0 -160
  33. package/test/fake-events.ts +0 -58
  34. package/test/fake-host.ts +0 -211
  35. package/test/fake-sidebar.ts +0 -516
  36. package/test/fake-transcript.ts +0 -486
  37. package/test/frame-telemetry.test.ts +0 -417
  38. package/test/hook.test.ts +0 -196
  39. package/test/manifest.test.ts +0 -105
  40. package/test/new-category.test.ts +0 -979
  41. package/test/notices.test.ts +0 -328
  42. package/test/plugin-integration-lines.test.ts +0 -979
  43. package/test/plugin-surface.test.ts +0 -152
  44. package/test/presence.test.ts +0 -459
  45. package/test/rail-surfaces.test.ts +0 -457
  46. package/test/rail-width.test.ts +0 -423
  47. package/test/regression.test.ts +0 -483
  48. package/test/repaint-api.test.ts +0 -200
  49. package/test/reserved-categories.test.ts +0 -361
  50. package/test/row-labels.test.ts +0 -284
  51. package/test/row-names.test.ts +0 -473
  52. package/test/row-origin.test.ts +0 -395
  53. package/test/section-interaction.test.ts +0 -364
  54. package/test/section-panel.test.ts +0 -403
  55. package/test/sections-rows.test.ts +0 -291
  56. package/test/sidebar-backend.test.ts +0 -471
  57. package/test/sidebar-e2e.test.ts +0 -331
  58. package/test/sidebar-hover.test.ts +0 -276
  59. package/test/sidebar-independent.test.ts +0 -349
  60. package/test/sidebar-render.test.ts +0 -281
  61. package/test/sidebar-sections.test.ts +0 -245
  62. package/test/status-card-styles.test.ts +0 -201
  63. package/test/status-section-frame.test.ts +0 -181
  64. package/test/status-sections.test.ts +0 -243
  65. package/test/status.test.ts +0 -209
  66. package/test/store.test.ts +0 -192
  67. package/test/surface-repaint-cost.test.ts +0 -520
  68. package/test/tool-design-chain.test.ts +0 -483
  69. package/test/tool-design-cost.test.ts +0 -473
  70. package/test/tool-design-rows.test.ts +0 -359
  71. package/test/tool-designs.test.ts +0 -745
  72. package/test/transform-pipeline.test.ts +0 -465
  73. package/test/transforms-bus.test.ts +0 -233
  74. package/test/verifier-width-regressions.test.ts +0 -164
  75. package/tsconfig.json +0 -18
@@ -1,520 +0,0 @@
1
- /**
2
- * S19/T35–T36 — the WORK a bridge-owned switch does, counted instead of timed.
3
- *
4
- * `requestSurfaceRepaint` used to be `tui.invalidate()` + `requestRender(force = true)`. Measuring
5
- * that against a differential frame on REAL pi components (T35, the tables in `src/repaint.ts`) says
6
- * the invalidation is the expensive half — it clears every mounted component's frame cache, so the
7
- * next frame re-parses every Markdown message of the transcript — and the force is the other half —
8
- * it resets the renderer's differential state and rewrites the whole buffer. 600 children, one
9
- * switch: 697 ms for the example `plain` style and 306 ms for the way back to a host style, against
10
- * 420 ms and 16 ms without them; a section toggle on the rail: 99 ms against 3 ms. And the rows the
11
- * two heavy halves draw are byte-identical on every surface:
12
- *
13
- * - the CONVERSATION repaints the style in the wrapper, on the host's ALREADY rendered rows, on
14
- * every pass (`src/card-style-wiring.ts` → `src/card-style-transform.ts`), so a component that
15
- * answers from its own cache is restyled just the same;
16
- * - the RAIL is framed by the same wrapper over the host's rail component, so a host memo answering
17
- * from its cache is restyled too; and a SECTION toggle moves the bridge's own section digest,
18
- * which the host's memo already keys on (`src/sidebar-render.ts` `sectionRailDigest`), while the
19
- * revision bump the request keeps is the host's documented part-owner step;
20
- * - a HOST style is inside the host card's own frame key (`cards-bash-v2`
21
- * `extensions/quiet-tools.ts:942-965`), which is why the host's own setter never invalidates
22
- * either (`extensions/gentle-shell.ts:3431-3437`).
23
- *
24
- * This file bounds the switch by CALL COUNTS, never milliseconds, so it cannot flake on a busy
25
- * machine, and proves the bound does not grow with the number of Markdown rows:
26
- *
27
- * - the tree is never invalidated and no prose component re-parses;
28
- * - no card is invalidated;
29
- * - exactly one frame is requested, and it is differential;
30
- * - and the same counts hold with 3 Markdown rows and with 200.
31
- *
32
- * The last test is the control that keeps those zeros honest: with a host frame key that does NOT
33
- * include the style, the differential path really does draw the previous card, so the instrumented
34
- * caches are real and the zero counts above mean something.
35
- */
36
-
37
- import assert from "node:assert/strict";
38
- import { mkdtempSync, rmSync } from "node:fs";
39
- import { tmpdir } from "node:os";
40
- import { join } from "node:path";
41
- import test from "node:test";
42
-
43
- import {
44
- registerCardStyle,
45
- registerSection,
46
- resetBusForTests,
47
- resetCardStylesForTests,
48
- resetSectionsForTests,
49
- snapshotProviders,
50
- } from "../src/bus.ts";
51
- import {
52
- CARD_STYLE_ROWS_PROVIDER_ID,
53
- installCardStyleRows,
54
- resetCardStyleRowStateForTests,
55
- } from "../src/card-style-rows.ts";
56
- import { resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
57
- import {
58
- disableCardStyleTransform,
59
- installCardStyleTransform,
60
- resetCardStyleWiringForTests,
61
- } from "../src/card-style-wiring.ts";
62
- import { setCardStyleSelection } from "../src/card-styles.ts";
63
- import {
64
- getRepaintDiagnostics,
65
- resetRepaintForTests,
66
- setRepaintHost,
67
- } from "../src/repaint.ts";
68
- import { SECTION_ROWS_PROVIDER_ID, installSectionRows } from "../src/sections-rows.ts";
69
- import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
70
- import { resetStoreStateForTests } from "../src/store.ts";
71
- import type { CustomizeRow, InlineHelpers, RowContext } from "../src/types.ts";
72
- import { setSectionVisible } from "../src/visibility.ts";
73
- import { BASH_MINIMAL_FLOAT_CALL, BASH_MINIMAL_FLOAT_RESULT, stripAnsi } from "./fake-transcript.ts";
74
-
75
- let configHome: string;
76
- let previousConfigHome: string | undefined;
77
-
78
- test.before(() => {
79
- configHome = mkdtempSync(join(tmpdir(), "gsb-surface-repaint-cost-"));
80
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
81
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
82
- });
83
-
84
- test.after(() => {
85
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
86
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
87
- rmSync(configHome, { recursive: true, force: true });
88
- });
89
-
90
- test.beforeEach(() => {
91
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
92
- resetBusForTests();
93
- resetSectionsForTests();
94
- resetCardStylesForTests();
95
- resetCardStyleRowStateForTests();
96
- resetCardStyleTransformForTests();
97
- resetCardStyleWiringForTests();
98
- resetRepaintForTests();
99
- resetSectionFailuresForTests();
100
- resetStoreStateForTests();
101
- disableCardStyleTransform();
102
- setCardStyleSelection("native");
103
- // The style and the section the switches below act on, registered the way a plugin does.
104
- registerCardStyle(MEDIA_STYLE);
105
- registerSection({ id: "loon.music", title: "Music", lines: () => ["track one", "track two"] });
106
- });
107
-
108
- test.afterEach(() => {
109
- disableCardStyleTransform();
110
- });
111
-
112
- const WIDTH = 60;
113
- const GUTTER = "\u258c";
114
-
115
- /** A style whose every row proves which function drew it, inside the width it was handed. */
116
- const MEDIA_STYLE = {
117
- id: "loon.plain",
118
- label: "plain",
119
- rowLabel: "plain",
120
- frame: {
121
- top: (ctx: { title?: string; width: number }) => `${GUTTER} ${(ctx.title ?? "").slice(0, ctx.width - 2)}`,
122
- line: (ctx: { text: string; width: number }) => `${GUTTER} ${ctx.text.slice(0, ctx.width - 2)}`,
123
- bottom: () => GUTTER,
124
- },
125
- };
126
-
127
- /** What the host's own card renderer answers with, unchanged: its real `float` bytes, at width 60. */
128
- const HOST_CALL = [...BASH_MINIMAL_FLOAT_CALL];
129
- const HOST_RESULT = [...BASH_MINIMAL_FLOAT_RESULT];
130
-
131
- interface Counters {
132
- /** `tui.invalidate()`: the call that cleared every cache in the tree. */
133
- treeInvalidations: number;
134
- /** What each `requestRender()` was called with. */
135
- frameRequests: Array<"plain" | "forced">;
136
- /** A prose row dropped its parsed lines. */
137
- proseInvalidations: number;
138
- /** A prose row re-parsed because its cache was empty. */
139
- proseCacheMisses: number;
140
- /** A card's `invalidate()` ran (the bridge's old path called it on every mounted card). */
141
- cardInvalidations: number;
142
- /** The host card's renderer ran (it may answer from its own cache). */
143
- hostRenders: number;
144
- /** The host card answered from its own frame cache. */
145
- hostCacheHits: number;
146
- /** The host card rebuilt its frame (width or the HOST style changed). */
147
- hostRebuilds: number;
148
- }
149
-
150
- function counters(): Counters {
151
- return {
152
- treeInvalidations: 0,
153
- frameRequests: [],
154
- proseInvalidations: 0,
155
- proseCacheMisses: 0,
156
- cardInvalidations: 0,
157
- hostRenders: 0,
158
- hostCacheHits: 0,
159
- hostRebuilds: 0,
160
- };
161
- }
162
-
163
- /** A `Markdown`-shaped row: it caches its lines and re-parses only after `invalidate()`. */
164
- function proseRow(text: string, counted: Counters): { render(width: number): string[]; invalidate(): void } {
165
- let cached: string[] | undefined;
166
- return {
167
- render(width: number): string[] {
168
- if (cached !== undefined) return cached;
169
- counted.proseCacheMisses += 1;
170
- cached = [`${text} `.repeat(4).trim().padEnd(width, " ")];
171
- return cached;
172
- },
173
- invalidate(): void {
174
- counted.proseInvalidations += 1;
175
- cached = undefined;
176
- },
177
- };
178
- }
179
-
180
- /**
181
- * A host bash card whose renderer caches its frame the way `ToolCardRows` does
182
- * (`cards-bash-v2` `extensions/quiet-tools.ts:942-965`): keyed on width and the HOST's own style.
183
- * The bridge's style is not in that key — which is why the wrapper post-processes whatever rows the
184
- * host answers with instead of the tree being invalidated.
185
- */
186
- function hostCard(counted: Counters, world: { hostStyle: string }): {
187
- render(width: number): string[];
188
- invalidate(): void;
189
- } {
190
- const frame = { width: -1, style: "", lines: [] as string[] };
191
- const renderCall = (): { render(width: number): string[]; invalidate(): void } => {
192
- const own = { width: -1, lines: [] as string[] };
193
- return {
194
- render(width: number): string[] {
195
- if (own.width === width) return own.lines;
196
- own.width = width;
197
- own.lines = [...HOST_CALL];
198
- return own.lines;
199
- },
200
- invalidate(): void {
201
- own.width = -1;
202
- },
203
- };
204
- };
205
- const renderResult = (): { render(width: number): string[]; invalidate(): void } => {
206
- const own = { width: -1, lines: [] as string[] };
207
- return {
208
- render(width: number): string[] {
209
- if (own.width === width) return own.lines;
210
- own.width = width;
211
- own.lines = [...HOST_RESULT];
212
- return own.lines;
213
- },
214
- invalidate(): void {
215
- own.width = -1;
216
- },
217
- };
218
- };
219
- let call: { render(width: number): string[] } | undefined;
220
- let result: { render(width: number): string[] } | undefined;
221
- return {
222
- render(width: number): string[] {
223
- counted.hostRenders += 1;
224
- if (frame.width === width && frame.style === world.hostStyle) {
225
- counted.hostCacheHits += 1;
226
- return frame.lines;
227
- }
228
- counted.hostRebuilds += 1;
229
- call = renderCall();
230
- result = renderResult();
231
- frame.width = width;
232
- frame.style = world.hostStyle;
233
- frame.lines = [...call.render(width), ...result.render(width)];
234
- return frame.lines;
235
- },
236
- invalidate(): void {
237
- counted.cardInvalidations += 1;
238
- frame.width = -1;
239
- frame.lines = [];
240
- },
241
- };
242
- }
243
-
244
- interface FakeTui {
245
- terminal: Record<string, unknown>;
246
- children: unknown[];
247
- invalidate(): void;
248
- requestRender(force?: boolean): void;
249
- }
250
-
251
- interface World {
252
- counted: Counters;
253
- hostStyle: string;
254
- chat: { render(width: number): string[]; invalidate(): void };
255
- draw(): string[];
256
- tui: FakeTui;
257
- prose: number;
258
- cards: number;
259
- }
260
-
261
- /**
262
- * A Pi-shaped transcript: a document whose third child is the chat container (the component
263
- * `src/card-style-wiring.ts` wraps), holding prose rows and host bash cards. The wrapper, the
264
- * transform and the repaint path are the real code.
265
- */
266
- function transcript(options: { prose: number; cards: number }): World {
267
- const counted = counters();
268
- const world: World = {
269
- counted,
270
- hostStyle: "float",
271
- chat: {
272
- render: () => [],
273
- invalidate: () => {},
274
- },
275
- draw: () => [],
276
- tui: { terminal: {}, children: [], invalidate: () => {}, requestRender: () => {} },
277
- prose: options.prose,
278
- cards: options.cards,
279
- };
280
- const nodes: Array<{ render(width: number): string[]; invalidate?(): void }> = [];
281
- for (let index = 0; index < options.prose; index += 1) {
282
- nodes.push(proseRow(`assistant message ${index}`, counted));
283
- }
284
- for (let index = 0; index < options.cards; index += 1) {
285
- nodes.push(hostCard(counted, world));
286
- }
287
- world.chat = {
288
- render: (width: number): string[] => nodes.flatMap((node) => node.render(width)),
289
- invalidate: (): void => {
290
- for (const node of nodes) node.invalidate?.();
291
- },
292
- };
293
- const document = { children: [{}, {}, world.chat] };
294
- const tui: FakeTui = {
295
- terminal: {},
296
- children: [document],
297
- invalidate: (): void => {
298
- counted.treeInvalidations += 1;
299
- world.chat.invalidate();
300
- },
301
- requestRender: (force?: boolean): void => {
302
- counted.frameRequests.push(force === true ? "forced" : "plain");
303
- },
304
- };
305
- world.tui = tui;
306
- world.draw = () => world.chat.render(WIDTH);
307
- return world;
308
- }
309
-
310
- /** One frame as the host's render loop would draw it. */
311
- const frame = (world: World): string[] => world.draw();
312
-
313
- const inlineHelpers: InlineHelpers = {
314
- input: async () => undefined,
315
- confirm: async () => true,
316
- capture: async () => undefined,
317
- };
318
-
319
- const ctx = (): RowContext => ({ theme: undefined, configHome });
320
-
321
- /** The card-style row's own action, exactly as the menu runs it. */
322
- async function pressStyle(value: string): Promise<void> {
323
- installCardStyleRows();
324
- const provider = snapshotProviders().find((entry) => entry.id === CARD_STYLE_ROWS_PROVIDER_ID);
325
- assert.ok(provider !== undefined, "the bridge registers the fallback Card style line");
326
- const row: CustomizeRow | undefined = provider
327
- .rows(ctx())
328
- .find((candidate) => candidate.choice?.value === value);
329
- assert.ok(row?.action !== undefined, `the line has a ${JSON.stringify(value)} value`);
330
- await row.action(inlineHelpers);
331
- }
332
-
333
- /** The section toggle row's own action, exactly as the menu runs it. */
334
- async function pressSection(value: "shown" | "hidden"): Promise<void> {
335
- installSectionRows();
336
- const provider = snapshotProviders().find((entry) => entry.id === SECTION_ROWS_PROVIDER_ID);
337
- assert.ok(provider !== undefined, "the bridge registers its own Sections provider");
338
- const row: CustomizeRow | undefined = provider
339
- .rows(ctx())
340
- .find((candidate) => candidate.choice?.value === value);
341
- assert.ok(row?.action !== undefined, `the toggle has a ${value} value`);
342
- await row.action(inlineHelpers);
343
- }
344
-
345
- interface Cost {
346
- treeInvalidations: number;
347
- frameRequests: Array<"plain" | "forced">;
348
- proseInvalidations: number;
349
- proseCacheMisses: number;
350
- cardInvalidations: number;
351
- hostCacheHits: number;
352
- }
353
-
354
- /** Forget everything counted so far, so a cost is the SWITCH's, not the warm-up frame's. */
355
- function zero(world: World): void {
356
- world.counted.treeInvalidations = 0;
357
- world.counted.frameRequests.length = 0;
358
- world.counted.proseInvalidations = 0;
359
- world.counted.proseCacheMisses = 0;
360
- world.counted.cardInvalidations = 0;
361
- world.counted.hostRenders = 0;
362
- world.counted.hostCacheHits = 0;
363
- world.counted.hostRebuilds = 0;
364
- }
365
-
366
- function readCost(world: World): Cost {
367
- return {
368
- treeInvalidations: world.counted.treeInvalidations,
369
- frameRequests: [...world.counted.frameRequests],
370
- proseInvalidations: world.counted.proseInvalidations,
371
- proseCacheMisses: world.counted.proseCacheMisses,
372
- cardInvalidations: world.counted.cardInvalidations,
373
- hostCacheHits: world.counted.hostCacheHits,
374
- };
375
- }
376
-
377
- /** Everything one switch costs, as counts, on a transcript of the given size. */
378
- async function styleSwitchCost(prose: number, cards: number): Promise<{ cost: Cost; drawn: string[] }> {
379
- const world = transcript({ prose, cards });
380
- setRepaintHost(world.tui);
381
- installCardStyleTransform(world.tui);
382
- setCardStyleSelection("native");
383
- frame(world);
384
- zero(world);
385
-
386
- await pressStyle("plain");
387
-
388
- const drawn = frame(world);
389
- return { cost: readCost(world), drawn };
390
- }
391
-
392
- async function sectionToggleCost(prose: number, cards: number): Promise<{ cost: Cost; trigger: unknown }> {
393
- const world = transcript({ prose, cards });
394
- setRepaintHost(world.tui);
395
- installCardStyleTransform(world.tui);
396
- // Every toggle starts from the same state, so the second measurement is the same work.
397
- setSectionVisible("loon.music", true);
398
- frame(world);
399
- zero(world);
400
-
401
- await pressSection("hidden");
402
-
403
- return { cost: readCost(world), trigger: getRepaintDiagnostics().lastTrigger };
404
- }
405
-
406
- // ---------------------------------------------------------------------------------------------
407
- // The bound
408
- // ---------------------------------------------------------------------------------------------
409
-
410
- test("T36/S19: a style switch asks for ONE differential frame and invalidates nothing", async () => {
411
- const { cost, drawn } = await styleSwitchCost(3, 2);
412
-
413
- assert.equal(
414
- cost.treeInvalidations,
415
- 0,
416
- "the tree is not invalidated: every Markdown row would re-parse and every card would rebuild",
417
- );
418
- assert.deepEqual(cost.frameRequests, ["plain"], "one frame, differential: no forced full redraw");
419
- assert.equal(cost.proseInvalidations, 0, "progress: no prose cache dropped");
420
- assert.equal(cost.cardInvalidations, 0, "no card is invalidated by a style switch");
421
- assert.equal(
422
- cost.proseCacheMisses,
423
- 0,
424
- "the switch re-parses nothing: the wrapper restyles the rows the host already drew",
425
- );
426
- assert.equal(cost.hostCacheHits, 2, "each host card answered from its OWN frame cache");
427
- assert.deepEqual(getRepaintDiagnostics().lastTrigger, {
428
- cause: "card style → loon.plain",
429
- surfaces: ["conversation", "rail"],
430
- outcome: "repainted",
431
- });
432
- assert.ok(
433
- drawn.some((line) => stripAnsi(line).startsWith(GUTTER)),
434
- "and the switch is VISIBLE: the drawn card took the style's frame",
435
- );
436
- });
437
-
438
- test("T36/S19: a section toggle asks for ONE differential frame and invalidates nothing", async () => {
439
- const { cost, trigger } = await sectionToggleCost(3, 2);
440
-
441
- assert.equal(cost.treeInvalidations, 0, "the rail's repaint does not rebuild the transcript");
442
- assert.deepEqual(cost.frameRequests, ["plain"], "one frame, differential");
443
- assert.equal(cost.proseInvalidations, 0);
444
- assert.equal(cost.proseCacheMisses, 0);
445
- assert.equal(cost.cardInvalidations, 0);
446
- assert.deepEqual(trigger, {
447
- cause: 'section "loon.music" → hidden',
448
- surfaces: ["rail"],
449
- outcome: "repainted",
450
- });
451
- });
452
-
453
- test("T36/S19: the work of a switch does not grow with the number of Markdown rows", async () => {
454
- const small = await styleSwitchCost(3, 2);
455
- const large = await styleSwitchCost(200, 2);
456
- assert.deepEqual(
457
- large.cost,
458
- small.cost,
459
- "200 prose rows cost a switch exactly what 3 do: nothing is invalidated, nothing re-parses",
460
- );
461
-
462
- const smallToggle = await sectionToggleCost(3, 2);
463
- const largeToggle = await sectionToggleCost(200, 2);
464
- assert.deepEqual(
465
- largeToggle.cost,
466
- smallToggle.cost,
467
- "and a section toggle is independent of the transcript's prose too",
468
- );
469
- });
470
-
471
- test("T36/S19: the way back to a host style is one differential frame as well", async () => {
472
- const world = transcript({ prose: 5, cards: 2 });
473
- setRepaintHost(world.tui);
474
- installCardStyleTransform(world.tui);
475
- frame(world);
476
- zero(world);
477
- await pressStyle("plain");
478
- frame(world);
479
-
480
- await pressStyle("native");
481
-
482
- assert.equal(world.counted.treeInvalidations, 0, "no invalidation on the way back");
483
- assert.deepEqual(world.counted.frameRequests, ["plain", "plain"], "one frame per press");
484
- assert.equal(world.counted.cardInvalidations, 0);
485
- assert.equal(world.counted.proseCacheMisses, 0);
486
- assert.ok(
487
- frame(world).some((line) => line === HOST_CALL[0]),
488
- "and the host's own rows are drawn again, byte for byte",
489
- );
490
- });
491
-
492
- // ---------------------------------------------------------------------------------------------
493
- // The control: the instrumented caches are real, so the zeros above mean something
494
- // ---------------------------------------------------------------------------------------------
495
-
496
- test("T36/S19 CONTROL: the instrument counts exactly what the old invalidation cost", () => {
497
- const world = transcript({ prose: 2, cards: 1 });
498
- setRepaintHost(world.tui);
499
- installCardStyleTransform(world.tui);
500
- frame(world);
501
-
502
- const misses = world.counted.proseCacheMisses;
503
- frame(world);
504
- assert.equal(world.counted.proseCacheMisses, misses, "a warm frame re-parses nothing");
505
- assert.equal(world.counted.hostCacheHits, 1, "the host card answered from its own frame cache");
506
-
507
- // The call the old `requestSurfaceRepaint` made: it is what the counts above would have recorded.
508
- world.tui.invalidate();
509
- assert.equal(world.counted.treeInvalidations, 1);
510
- assert.equal(world.counted.cardInvalidations, 1, "the invalidation reaches every mounted card");
511
- assert.equal(world.counted.proseInvalidations, 2, "and every prose row, which is its real cost");
512
-
513
- const before = world.counted.proseCacheMisses;
514
- frame(world);
515
- assert.equal(
516
- world.counted.proseCacheMisses,
517
- before + 2,
518
- "which is why the next frame would have re-parsed the whole transcript",
519
- );
520
- });