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.
Files changed (76) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +91 -722
  3. package/docs/contract.md +92 -35
  4. package/package.json +13 -1
  5. package/src/api.ts +1 -1
  6. package/.codegraph/.gitignore +0 -5
  7. package/PRD.md +0 -144
  8. package/examples/bash-card.ts +0 -1694
  9. package/gentle-shell-bridge-0.3.0.tgz +0 -0
  10. package/odd/tasks/palette-sections.md +0 -30
  11. package/odd/tasks/plugin-keymap.md +0 -32
  12. package/odd/tasks/plugin-surface-contract.md +0 -55
  13. package/test/api.test.ts +0 -395
  14. package/test/apply-on-move.test.ts +0 -477
  15. package/test/arm.test.ts +0 -220
  16. package/test/bridge-ready.test.ts +0 -309
  17. package/test/bus.test.ts +0 -165
  18. package/test/button-rows.test.ts +0 -409
  19. package/test/capture.test.ts +0 -356
  20. package/test/card-style-rows.test.ts +0 -510
  21. package/test/card-style-transform.test.ts +0 -379
  22. package/test/card-style-wiring.test.ts +0 -258
  23. package/test/card-styles-e2e.test.ts +0 -491
  24. package/test/card-styles.test.ts +0 -324
  25. package/test/card-transcript-path.test.ts +0 -178
  26. package/test/config.test.ts +0 -93
  27. package/test/e2e.test.ts +0 -443
  28. package/test/example-bash-identity.test.ts +0 -372
  29. package/test/example-bash-matrix.test.ts +0 -856
  30. package/test/example-designs.test.ts +0 -734
  31. package/test/example-neon-transform.test.ts +0 -283
  32. package/test/example-plain-style.test.ts +0 -1268
  33. package/test/extension.test.ts +0 -160
  34. package/test/fake-events.ts +0 -58
  35. package/test/fake-host.ts +0 -211
  36. package/test/fake-sidebar.ts +0 -516
  37. package/test/fake-transcript.ts +0 -486
  38. package/test/frame-telemetry.test.ts +0 -417
  39. package/test/hook.test.ts +0 -196
  40. package/test/manifest.test.ts +0 -105
  41. package/test/new-category.test.ts +0 -979
  42. package/test/notices.test.ts +0 -328
  43. package/test/plugin-integration-lines.test.ts +0 -979
  44. package/test/plugin-surface.test.ts +0 -152
  45. package/test/presence.test.ts +0 -459
  46. package/test/rail-surfaces.test.ts +0 -457
  47. package/test/rail-width.test.ts +0 -423
  48. package/test/regression.test.ts +0 -483
  49. package/test/repaint-api.test.ts +0 -200
  50. package/test/reserved-categories.test.ts +0 -361
  51. package/test/row-labels.test.ts +0 -284
  52. package/test/row-names.test.ts +0 -473
  53. package/test/row-origin.test.ts +0 -395
  54. package/test/section-interaction.test.ts +0 -364
  55. package/test/section-panel.test.ts +0 -403
  56. package/test/sections-rows.test.ts +0 -291
  57. package/test/sidebar-backend.test.ts +0 -471
  58. package/test/sidebar-e2e.test.ts +0 -331
  59. package/test/sidebar-hover.test.ts +0 -276
  60. package/test/sidebar-independent.test.ts +0 -349
  61. package/test/sidebar-render.test.ts +0 -281
  62. package/test/sidebar-sections.test.ts +0 -245
  63. package/test/status-card-styles.test.ts +0 -201
  64. package/test/status-section-frame.test.ts +0 -181
  65. package/test/status-sections.test.ts +0 -243
  66. package/test/status.test.ts +0 -209
  67. package/test/store.test.ts +0 -192
  68. package/test/surface-repaint-cost.test.ts +0 -520
  69. package/test/tool-design-chain.test.ts +0 -483
  70. package/test/tool-design-cost.test.ts +0 -473
  71. package/test/tool-design-rows.test.ts +0 -359
  72. package/test/tool-designs.test.ts +0 -745
  73. package/test/transform-pipeline.test.ts +0 -465
  74. package/test/transforms-bus.test.ts +0 -233
  75. package/test/verifier-width-regressions.test.ts +0 -164
  76. 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
- });