diff --git a/.agents/architecture-rules.md b/.agents/architecture-rules.md index b45e58a1..e1b38445 100644 --- a/.agents/architecture-rules.md +++ b/.agents/architecture-rules.md @@ -18,10 +18,16 @@ the system itself; this file governs how new architecture is designed. 9. Immutable view data flows down and typed semantic actions flow up; the root alone owns route intent, selection and rebuildable application state. 10. A component lifetime may own a node; a render body may not create or own one. 11. View data and placement cross the direct parent-child boundary and no other. -12. Normalized host input is decided once and dispatched through the target's own ancestry; a host names a target and an event shape, never an action. +12. Normalized host input is decided once and dispatched through the target's own ancestry; a host names a target and an event shape, never an action. Text is its own event shape carrying what the terminal decoded, and a modified chord is dropped whole rather than read off its physical key code. 13. Freedom alone owns focus; an overlay is a mounted branch, not a second tree. 14. Keyed descriptions reconcile into one mounted tree; validate the complete desired set before mutating any of it. 15. An absent description leaves no node, input, focus, frame contribution or output behind. +16. The mounted tree is the only source of renderable nodes and frame targets; a frame map names the live node behind each drawn element and is valid only for the tree revision that drew it. +17. One acknowledged host frame stream carries presentation time; subscriptions are owned by lifetimes, an advance waits for every subscriber to acknowledge applying the last one rather than receiving it, and a settled tree schedules nothing. +18. The nearest common mounted ancestor owns an animation that spans components. +19. Renderer output is copied out of the engine before returning, and a refused frame is redrawn on a renderer rebuilt for that frame's dimensions without changing selection, focus, actions or the frame itself. +20. A contextual adapter owns terminal modes, listeners and readers, registers their release before taking them, and restores them exactly once on every exit. +21. A narrow surface displays the one route selected; every other surface is absent from the frame, its map and its targets rather than hidden within them. The Architect applies these rules without another approval when they settle a design. A new concept, ambiguous fit, conflict or proposed exception returns to diff --git a/architecture.md b/architecture.md index 0a699cf1..3b6ccce5 100644 --- a/architecture.md +++ b/architecture.md @@ -5344,6 +5344,73 @@ Freedom is vendored from a pinned unpublished commit under `packages/cli/src/repl/vendor/freedom/`, as a pristine copy and a patched copy whose every difference belongs to one named patch. See its `PROVENANCE.md`. +### From a frame to a terminal, and back + +Below the tree, one more direction: + +```text +committed frame → placement → engine ops → bytes → the terminal + ↓ + element id + geometry → the live node it came from + ↑ + bytes → normalized key or position → target → the same ancestry walk +``` + +Placement is presentation and nothing else: it reads the committed frame and +tells the tree nothing, so resizing moves things and changes no selection, no +model object and no action's meaning. Four sizes are decided — wide, medium, +narrow, and a refusal below the narrow minimum that recovers when the window +grows — and a control the refusal hides is drawn nowhere and therefore +targetable nowhere. + +Only mounted nodes are drawn. Each rendered frame carries an immutable map from +the element the layout engine measured to the live node that asked for it, and +that map is valid for exactly the tree revision that produced it: a pointer +resolved against it travels with that revision's number, so a pointer from a +frame the tree has moved past, one naming a node that has since gone, and one +landing behind an open modal all reach nothing. A pointer that does resolve +becomes the same normalized event a key produces and walks the same ancestry, so +clicking a control and pressing Enter on it are one action. + +Text is its own member of that normalized union, not a key with a payload: a +named key — submit, dismiss, traverse, erase — is a command whoever claims it +recognizes, and text is content that goes wherever content goes. It carries what +the terminal decoded, so shifted, accented and multi-byte characters arrive +already assembled, and a newline inside a paste arrives as one. A chord carries +a letter in its physical key code, and reading that code would type the letter +somebody held Control with, so a chord is dropped whole — payload included. + +Every render input is retained as a frozen snapshot before it crosses into the +engine, and the bytes that come back are copied immediately, because the engine +hands out a view into its own memory that the next render invalidates. A frame +the engine's arenas cannot hold is redrawn on an engine rebuilt for that frame's +dimensions; recovery changes nothing above it. + +Placement at narrow shows the one surface the route selected. Every other +surface stays mounted and stays off the frame, which is what makes it +unreachable rather than merely hidden: it is in no cell, in no frame map, and no +pointer resolves to it. + +Presentation time has one owner. There is a single acknowledged frame stream: +holding a subscription is what asks for frames, an advance waits until every +subscriber has **applied** the last timestamp — receiving one is not applying +it, because drawing with a timestamp suspends — and a tree with no subscriber +schedules no timer at all. A cancelled subscriber's demand goes with it and its +acknowledgement never arrives, because it did not finish the frame it held. An animation spanning components belongs to their +nearest common mounted ancestor, whose lifetime is the animation's; nothing +below it keeps a clock of its own. The stream carries presentation time only — +Journal records, model selection, routes and execution pause state are outside +it. + +The terminal itself is a contextual Api, and a runtime-named adapter installs +it. Shared code never asks which runtime it is on; it asks for the size, bytes +in, bytes out, raw mode and resize notifications. Whoever opens the terminal +registers the release of each of those before taking it, so a cancellation +between the two still gives the terminal back — and every exit, whether the run +finished, refused, failed, was cancelled or reached end of input, stops the +reader, removes every listener, restores the modes and writes the final reset +exactly once. + ## Changing these rules Spec, tests, and mechanics move together, in the same PR. If a workaround diff --git a/deno.lock b/deno.lock index ec96c999..da4b608c 100644 --- a/deno.lock +++ b/deno.lock @@ -41,6 +41,7 @@ "npm:@agentclientprotocol/sdk@1.3.0": "1.3.0_zod@4.4.3", "npm:@babel/core@^7.28.0": "7.29.7", "npm:@babel/preset-react@^7.27.1": "7.29.7_@babel+core@7.29.7", + "npm:@bomb.sh/tty@0.9.0": "0.9.0", "npm:@durable-streams/client@~0.2.2": "0.2.6", "npm:@durable-streams/server@~0.3.8": "0.3.8", "npm:@effectionx/context-api@0.6.0": "0.6.0_effection@4.1.0", @@ -467,6 +468,9 @@ "@babel/helper-validator-identifier" ] }, + "@bomb.sh/tty@0.9.0": { + "integrity": "sha512-1fX9lgwdc+kGRQeVEYwv7cJb5i855ysvB/TMCC3/wnnjMiKLZjWew6LLnsAMq4pLzkweivYw+zwYNZRUVhjoBA==" + }, "@clack/core@1.4.3": { "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", "dependencies": [ @@ -4047,11 +4051,13 @@ }, "packages/cli": { "dependencies": [ + "npm:@bomb.sh/tty@0.9.0", "npm:@standard-schema/spec@1", "npm:zod@^4.3.6" ], "packageJson": { "dependencies": [ + "npm:@bomb.sh/tty@0.9.0", "npm:@effectionx/stream-helpers@0.8.3", "npm:@standard-schema/spec@1", "npm:configliere@0.4", diff --git a/packages/cli/deno.json b/packages/cli/deno.json index c0495c53..5594c615 100644 --- a/packages/cli/deno.json +++ b/packages/cli/deno.json @@ -4,6 +4,7 @@ "license": "MIT", "exports": "./src/deno.ts", "imports": { + "@bomb.sh/tty": "npm:@bomb.sh/tty@0.9.0", "@standard-schema/spec": "npm:@standard-schema/spec@^1.0.0", "zod": "npm:zod@^4.3.6" } diff --git a/packages/cli/package.json b/packages/cli/package.json index 36cd9bef..a4c7a2af 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -11,6 +11,7 @@ "default": "./src/node.ts" }, "dependencies": { + "@bomb.sh/tty": "0.9.0", "@effectionx/stream-helpers": "0.8.3", "@executablemd/acp": "workspace:*", "@executablemd/core": "workspace:*", diff --git a/packages/cli/src/bun-repl-terminal.ts b/packages/cli/src/bun-repl-terminal.ts new file mode 100644 index 00000000..b4bf3f5e --- /dev/null +++ b/packages/cli/src/bun-repl-terminal.ts @@ -0,0 +1,52 @@ +/** + * The REPL's terminal on Bun. + * + * Named for its runtime because every line of it is a Node-compatible capability Bun implements: the + * stream's own reported width and height, `setRawMode` on the tty read stream, + * the `SIGWINCH` process event, and the two writes. Nothing registers it — + * `bun.ts` installs it when the REPL opens, so an ordinary `xmd run` never + * touches raw mode. + * + * Standard input is a single source, so one subscriber consumes it; a second + * would be a second decoder racing the first for the same bytes, which is not + * something this REPL does. + */ + +import { writeSync } from "node:fs"; +import process from "node:process"; +import type { Operation } from "effection"; +import { decodedChunks, installReplTerminal, writeThrough } from "./repl/terminal-host.ts"; +import type { ReplTerminalSize } from "./repl/terminal.ts"; + +/** Install the Bun-backed terminal for the calling scope. */ +export function useBunReplTerminal(): Operation { + return installReplTerminal({ + size(): ReplTerminalSize { + return { columns: process.stdout.columns, rows: process.stdout.rows }; + }, + write(bytes: Uint8Array): Promise { + return writeThrough((chunk, done) => process.stdout.write(chunk, done), bytes); + }, + writeNow(bytes: Uint8Array): void { + // Synchronous on purpose: the final reset must land as one uninterrupted + // act, and a suspension here would let another teardown step write over a + // half-restored terminal. Written to the descriptor rather than the stream + // because the stream may still be holding buffered output of its own. + // oxlint-disable-next-line local/no-sync-filesystem + writeSync(1, bytes); + }, + setRaw(raw: boolean): void { + process.stdin.setRawMode(raw); + }, + bytes(): AsyncIterable { + process.stdin.resume(); + return decodedChunks(process.stdin); + }, + onResize(listener: () => void): () => void { + process.on("SIGWINCH", listener); + return () => { + process.off("SIGWINCH", listener); + }; + }, + }); +} diff --git a/packages/cli/src/compiled-repl-terminal.ts b/packages/cli/src/compiled-repl-terminal.ts new file mode 100644 index 00000000..48baf6ee --- /dev/null +++ b/packages/cli/src/compiled-repl-terminal.ts @@ -0,0 +1,87 @@ +/** + * The REPL's terminal inside the compiled binary. + * + * The compiled binary is Deno, so the capabilities are Deno's — but it is its + * own named factory because what installs a terminal is a distribution + * decision, and a binary that shipped without a REPL should not be reached + * through the factory the source runtime uses. + * + * Every line of it is a Deno capability: the + * console size, raw mode on the standard input descriptor, `SIGWINCH`, and the + * two writes. Nothing registers it — `compiled.ts` installs it when the REPL opens, + * so an ordinary `xmd run` never touches raw mode or the terminal's modes at + * all. + * + * The capabilities are read off the host rather than named, because this package + * is typechecked under Node as well and a module that names `Deno` does not + * compile there even though nothing would ever load it. + * + * Standard input is a single source, so one subscriber consumes it; a second + * would be a second decoder racing the first for the same bytes, which is not + * something this REPL does. + */ + +import type { Operation } from "effection"; +import { denoTerminalSurface } from "./deno-terminal-surface.ts"; +import { installReplTerminal } from "./repl/terminal-host.ts"; +import type { ReplTerminalSize } from "./repl/terminal.ts"; + +/** Install the compiled binary's terminal for the calling scope. */ +export function* useCompiledReplTerminal(): Operation { + const host = denoTerminalSurface(); + if (host === undefined) { + throw new Error("this host is not Deno, so it has no Deno terminal to install"); + } + yield* installReplTerminal({ + size(): ReplTerminalSize { + return host.consoleSize(); + }, + write(bytes: Uint8Array): Promise { + return writeAll(host, bytes); + }, + writeNow(bytes: Uint8Array): void { + let written = 0; + while (written < bytes.length) { + // Synchronous on purpose: the final reset must land as one uninterrupted + // act, and a suspension here would let another teardown step write over + // a half-restored terminal. + const count = host.writeSync(bytes.subarray(written)); + if (count <= 0) { + return; + } + written += count; + } + }, + setRaw(raw: boolean): void { + host.setRaw(raw); + }, + bytes: () => host.bytes(), + onResize: (listener: () => void) => host.onResize(listener), + }); +} + +/** + * Write every byte, however few one call takes. + * + * A partial write nobody continued leaves a frame half drawn, which on a + * terminal means escape sequences cut in the middle. + */ +function writeAll( + host: { write(bytes: Uint8Array): Promise }, + bytes: Uint8Array, +): Promise { + let written = 0; + const step = (): Promise => { + if (written >= bytes.length) { + return Promise.resolve(); + } + return host.write(bytes.subarray(written)).then((count) => { + if (count <= 0) { + return Promise.reject(new Error("the terminal accepted none of the bytes it was given")); + } + written += count; + return step(); + }); + }; + return step(); +} diff --git a/packages/cli/src/deno-repl-terminal.ts b/packages/cli/src/deno-repl-terminal.ts new file mode 100644 index 00000000..d5024aea --- /dev/null +++ b/packages/cli/src/deno-repl-terminal.ts @@ -0,0 +1,82 @@ +/** + * The REPL's terminal on Deno. + * + * Named for its runtime because every line of it is a Deno capability: the + * console size, raw mode on the standard input descriptor, `SIGWINCH`, and the + * two writes. Nothing registers it — `deno.ts` installs it when the REPL opens, + * so an ordinary `xmd run` never touches raw mode or the terminal's modes at + * all. + * + * The capabilities are read off the host rather than named, because this package + * is typechecked under Node as well and a module that names `Deno` does not + * compile there even though nothing would ever load it. + * + * Standard input is a single source, so one subscriber consumes it; a second + * would be a second decoder racing the first for the same bytes, which is not + * something this REPL does. + */ + +import type { Operation } from "effection"; +import { denoTerminalSurface } from "./deno-terminal-surface.ts"; +import { installReplTerminal } from "./repl/terminal-host.ts"; +import type { ReplTerminalSize } from "./repl/terminal.ts"; + +/** Install the Deno-backed terminal for the calling scope. */ +export function* useDenoReplTerminal(): Operation { + const host = denoTerminalSurface(); + if (host === undefined) { + throw new Error("this host is not Deno, so it has no Deno terminal to install"); + } + yield* installReplTerminal({ + size(): ReplTerminalSize { + return host.consoleSize(); + }, + write(bytes: Uint8Array): Promise { + return writeAll(host, bytes); + }, + writeNow(bytes: Uint8Array): void { + let written = 0; + while (written < bytes.length) { + // Synchronous on purpose: the final reset must land as one uninterrupted + // act, and a suspension here would let another teardown step write over + // a half-restored terminal. + const count = host.writeSync(bytes.subarray(written)); + if (count <= 0) { + return; + } + written += count; + } + }, + setRaw(raw: boolean): void { + host.setRaw(raw); + }, + bytes: () => host.bytes(), + onResize: (listener: () => void) => host.onResize(listener), + }); +} + +/** + * Write every byte, however few one call takes. + * + * A partial write nobody continued leaves a frame half drawn, which on a + * terminal means escape sequences cut in the middle. + */ +function writeAll( + host: { write(bytes: Uint8Array): Promise }, + bytes: Uint8Array, +): Promise { + let written = 0; + const step = (): Promise => { + if (written >= bytes.length) { + return Promise.resolve(); + } + return host.write(bytes.subarray(written)).then((count) => { + if (count <= 0) { + return Promise.reject(new Error("the terminal accepted none of the bytes it was given")); + } + written += count; + return step(); + }); + }; + return step(); +} diff --git a/packages/cli/src/deno-terminal-surface.ts b/packages/cli/src/deno-terminal-surface.ts new file mode 100644 index 00000000..18332fe0 --- /dev/null +++ b/packages/cli/src/deno-terminal-surface.ts @@ -0,0 +1,159 @@ +/** + * The pieces of Deno's terminal this REPL uses, read off the host. + * + * Read rather than imported, and named nowhere in this module's types, because + * the repository typechecks every package under Node as well — where the `Deno` + * global does not exist and a module that names it fails to compile even though + * nothing there would ever load it. Reaching for the host through `globalThis` + * and parsing what comes back is how the other runtime-named adapters in this + * repository stay compilable everywhere and constructible only where they work. + * + * Every member is validated before it is offered. A host that is missing one is + * a host with no terminal, which the caller reports rather than discovering + * halfway through taking one. + */ + +/** What a Deno host offers a terminal. */ +export interface DenoTerminalSurface { + consoleSize(): { readonly columns: number; readonly rows: number }; + /** Hand bytes over, resolving with how many it took. */ + write(bytes: Uint8Array): Promise; + /** Hand bytes over with no suspension point inside the call. */ + writeSync(bytes: Uint8Array): number; + setRaw(raw: boolean): void; + /** The byte source. One source, so one subscriber consumes it. */ + bytes(): AsyncIterable; + /** Watch for size changes; the returned function stops watching. */ + onResize(listener: () => void): () => void; +} + +/** One member of the host, when it is the function this needs. */ +function callable(host: object, name: string): ((...args: unknown[]) => unknown) | undefined { + const member: unknown = Reflect.get(host, name); + if (typeof member !== "function") { + return undefined; + } + return (...args: unknown[]) => Reflect.apply(member, host, args); +} + +/** A number the host reported, or none when it reported something else. */ +function numeric(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +/** + * This host's terminal, or none when it is not a Deno host. + * + * `undefined` rather than a throw: whether this runtime has one is a question + * with an answer, and the caller is the one that knows whether an absent + * terminal is a problem. + */ +export function denoTerminalSurface(): DenoTerminalSurface | undefined { + const host: unknown = Reflect.get(globalThis, "Deno"); + if (typeof host !== "object" || host === null) { + return undefined; + } + const consoleSize = callable(host, "consoleSize"); + const addSignalListener = callable(host, "addSignalListener"); + const removeSignalListener = callable(host, "removeSignalListener"); + const stdout: unknown = Reflect.get(host, "stdout"); + const stdin: unknown = Reflect.get(host, "stdin"); + if ( + consoleSize === undefined || + addSignalListener === undefined || + removeSignalListener === undefined || + typeof stdout !== "object" || + stdout === null || + typeof stdin !== "object" || + stdin === null + ) { + return undefined; + } + + const write = callable(stdout, "write"); + const writeSync = callable(stdout, "writeSync"); + const setRaw = callable(stdin, "setRaw"); + const readable: unknown = Reflect.get(stdin, "readable"); + if ( + write === undefined || + writeSync === undefined || + setRaw === undefined || + typeof readable !== "object" || + readable === null || + !(Symbol.asyncIterator in readable) + ) { + return undefined; + } + const source = readable; + + return { + consoleSize(): { readonly columns: number; readonly rows: number } { + const reported: unknown = consoleSize(); + if (typeof reported !== "object" || reported === null) { + throw new Error("this host did not report a console size"); + } + const columns = numeric(Reflect.get(reported, "columns")); + const rows = numeric(Reflect.get(reported, "rows")); + if (columns === undefined || rows === undefined) { + throw new Error("this host reported a console size with no columns or rows"); + } + return { columns, rows }; + }, + write(bytes: Uint8Array): Promise { + return Promise.resolve(write(bytes)).then((taken) => numeric(taken) ?? 0); + }, + writeSync(bytes: Uint8Array): number { + return numeric(writeSync(bytes)) ?? 0; + }, + setRaw(raw: boolean): void { + setRaw(raw); + }, + bytes(): AsyncIterable { + // Checked above: this object declares the async-iterable protocol, which + // is the whole of what the shared reader asks of it. + return { + [Symbol.asyncIterator](): AsyncIterator { + const method: unknown = Reflect.get(source, Symbol.asyncIterator); + if (typeof method !== "function") { + throw new Error("this host's standard input is not iterable after all"); + } + const iterator: unknown = Reflect.apply(method, source, []); + if (typeof iterator !== "object" || iterator === null) { + throw new Error("this host's standard input produced no iterator"); + } + const next = callable(iterator, "next"); + const back = callable(iterator, "return"); + if (next === undefined) { + throw new Error("this host's standard input produced no reader"); + } + return { + next(): Promise> { + return Promise.resolve(next()).then((result) => { + if (typeof result !== "object" || result === null) { + return { done: true, value: undefined }; + } + const value: unknown = Reflect.get(result, "value"); + return Reflect.get(result, "done") === true || !(value instanceof Uint8Array) + ? { done: true, value: undefined } + : { done: false, value }; + }); + }, + ...(back === undefined + ? {} + : { + return(): Promise> { + return Promise.resolve(back()).then(() => ({ done: true, value: undefined })); + }, + }), + }; + }, + }; + }, + onResize(listener: () => void): () => void { + addSignalListener("SIGWINCH", listener); + return () => { + removeSignalListener("SIGWINCH", listener); + }; + }, + }; +} diff --git a/packages/cli/src/node-repl-terminal.ts b/packages/cli/src/node-repl-terminal.ts new file mode 100644 index 00000000..58d55e96 --- /dev/null +++ b/packages/cli/src/node-repl-terminal.ts @@ -0,0 +1,52 @@ +/** + * The REPL's terminal on Node. + * + * Named for its runtime because every line of it is a Node capability: the + * stream's own reported width and height, `setRawMode` on the tty read stream, + * the `SIGWINCH` process event, and the two writes. Nothing registers it — + * `node.ts` installs it when the REPL opens, so an ordinary `xmd run` never + * touches raw mode. + * + * Standard input is a single source, so one subscriber consumes it; a second + * would be a second decoder racing the first for the same bytes, which is not + * something this REPL does. + */ + +import { writeSync } from "node:fs"; +import process from "node:process"; +import type { Operation } from "effection"; +import { decodedChunks, installReplTerminal, writeThrough } from "./repl/terminal-host.ts"; +import type { ReplTerminalSize } from "./repl/terminal.ts"; + +/** Install the Node-backed terminal for the calling scope. */ +export function useNodeReplTerminal(): Operation { + return installReplTerminal({ + size(): ReplTerminalSize { + return { columns: process.stdout.columns, rows: process.stdout.rows }; + }, + write(bytes: Uint8Array): Promise { + return writeThrough((chunk, done) => process.stdout.write(chunk, done), bytes); + }, + writeNow(bytes: Uint8Array): void { + // Synchronous on purpose: the final reset must land as one uninterrupted + // act, and a suspension here would let another teardown step write over a + // half-restored terminal. Written to the descriptor rather than the stream + // because the stream may still be holding buffered output of its own. + // oxlint-disable-next-line local/no-sync-filesystem + writeSync(1, bytes); + }, + setRaw(raw: boolean): void { + process.stdin.setRawMode(raw); + }, + bytes(): AsyncIterable { + process.stdin.resume(); + return decodedChunks(process.stdin); + }, + onResize(listener: () => void): () => void { + process.on("SIGWINCH", listener); + return () => { + process.off("SIGWINCH", listener); + }; + }, + }); +} diff --git a/packages/cli/src/repl/description.ts b/packages/cli/src/repl/description.ts index 595c3d07..feafb576 100644 --- a/packages/cli/src/repl/description.ts +++ b/packages/cli/src/repl/description.ts @@ -49,8 +49,13 @@ export type ReplViewData = /** Placement a parent decides for one direct child. Opaque to everyone else. */ export type ReplPlacement = { readonly [key: string]: string | number | boolean }; -/** The named keys this composition layer understands. */ -export type ReplKey = "Enter" | "Escape" | "Tab" | "Backtab"; +/** + * The named keys this composition layer understands. + * + * Named because each one means something structural — submit, dismiss, move + * focus, erase — as opposed to text, which means only itself. + */ +export type ReplKey = "Enter" | "Escape" | "Tab" | "Backtab" | "Backspace"; /** * One normalized event, as a host hands it over. @@ -58,9 +63,16 @@ export type ReplKey = "Enter" | "Escape" | "Tab" | "Backtab"; * A discriminated union, so a key event carrying a frame and a pointer event * carrying a key name are not values anybody can build. The host says what * happened and where; it never says what it means. + * + * Text is its own member rather than a key with a payload, because the two are + * answered differently: a named key is a command whoever claims it recognizes, + * and text is content that goes wherever content goes. It carries the string the + * terminal decoded — one grapheme as typed, or `"\n"` for a newline inside a + * paste — and never a raw terminal event. */ export type ReplInputEvent = | { readonly kind: "key"; readonly key: ReplKey } + | { readonly kind: "text"; readonly text: string } | { readonly kind: "pointer"; readonly target: string; readonly frame: number }; /** What a mounted child may do, for as long as it is mounted. */ diff --git a/packages/cli/src/repl/frame.ts b/packages/cli/src/repl/frame.ts new file mode 100644 index 00000000..51041590 --- /dev/null +++ b/packages/cli/src/repl/frame.ts @@ -0,0 +1,295 @@ +/** + * One clock for the whole screen, and nobody drawing ahead of anybody else. + * + * Animation needs a timestamp, and a timestamp that different parts of the + * screen read at different moments is two animations pretending to be one. So + * there is exactly one frame stream, the host owns it, and it is + * **acknowledged**: the next timestamp is not published until every subscriber + * has applied the current one. Nothing can run a frame ahead, and nothing can + * fall a frame behind without holding the rest back — which is the visible, + * debuggable failure rather than a silent drift. + * + * ## Subscription is the demand + * + * There is no start and no stop. A subscription is a resource, and holding one + * is what asks for frames; releasing it withdraws the request. When the last + * one goes, the stream is settled and **no timer is scheduled at all** — an + * idle REPL costs nothing, which a stream that kept ticking into an empty + * subscriber set would not. + * + * ## Who owns an animation that spans components + * + * The nearest common ancestor of everything taking part, and only it. A + * participant that subscribed for itself would keep its own clock running after + * a sibling left, and a distant ancestor would hold the timer alive for a + * subtree that stopped animating. The ancestor in between is the one whose + * lifetime matches the animation's, so a claim from anywhere else is refused. + * + * `delta` is in **seconds**, because that is what the layout engine's + * transitions take; handing it milliseconds runs every transition 1000x fast. + */ + +import { type Api, createApi } from "@effectionx/context-api"; +import { + Err, + ensure, + Ok, + type Operation, + resource, + type Result, + sleep, + spawn, + withResolvers, +} from "effection"; + +/** Sixty frames a second, as seconds. */ +export const FRAME_INTERVAL = 1 / 60; + +/** The host clock, so a test can hold time still and count what was scheduled. */ +export interface ReplClockApi { + /** Now, in seconds. */ + now(): Operation; + /** Wait this many seconds. */ + wait(seconds: number): Operation; +} + +export const ReplClock: Api = createApi("ReplClock", { + // deno-lint-ignore require-yield + *now(): Operation { + return performance.now() / 1000; + }, + *wait(seconds: number): Operation { + yield* sleep(Math.max(0, Math.round(seconds * 1000))); + }, +}); + +/** One published frame. */ +export interface ReplFrameTick { + /** Monotonic from one, so a subscriber can say which frame it applied. */ + readonly id: number; + /** The host clock, in seconds. */ + readonly at: number; + /** Seconds since the previous frame. Zero for the first. */ + readonly delta: number; +} + +/** + * An ancestry as the composition kernel reports one: the node itself first, + * its root last. + */ +export type ReplAncestry = readonly string[]; + +/** Who wants frames, and for what. */ +export interface ReplFrameRequest { + /** The node claiming ownership. */ + readonly owner: string; + /** The ancestry of each participant, one entry per participating node. */ + readonly participants: readonly ReplAncestry[]; +} + +/** A claim that is not the nearest common ancestor of its participants. */ +export class ReplFrameOwnerError extends Error { + constructor(owner: string, expected: string | undefined) { + super( + expected === undefined + ? `${owner} claimed an animation whose participants share no ancestor, so nothing can own it` + : `${owner} claimed an animation owned by ${expected}, the nearest common ancestor of its ` + + `participants`, + ); + this.name = "ReplFrameOwnerError"; + } +} + +/** One subscriber's view of the stream. */ +export interface ReplFrameSubscription { + /** + * The next frame. + * + * Receiving one is not applying it. Applying a timestamp means laying out and + * rendering with it, and that suspends — so a stream that counted the return + * of `next()` as the acknowledgement would publish the following frame while + * this one was still being drawn, which is the drift the acknowledgement + * exists to prevent. + */ + next(): Operation; + /** + * Say the received frame has been applied. + * + * Until every subscriber says so, the stream publishes nothing further. + */ + acknowledge(): void; + /** The last frame this subscriber acknowledged. */ + applied(): number; + /** The last frame this subscriber received, applied or not. */ + received(): number; +} + +/** The one stream. */ +export interface ReplFrames { + /** + * Ask for frames for as long as the result is held. + * + * A refused claim registers no demand at all, so holding an `Err` keeps no + * timer alive. + */ + subscribe(request: ReplFrameRequest): Operation>; + /** Whether anything is currently asking for frames. */ + demanded(): boolean; + /** How many frames the host has published. */ + published(): number; + /** Subscribers that have received or owe the current frame but not applied it. */ + outstanding(): number; +} + +/** + * The nearest common ancestor of some participants, or none. + * + * Ancestries arrive node-first, so the shared *suffix* is the shared ancestry; + * reversing makes it a prefix, and the last element of the common prefix is the + * nearest one. + */ +export function nearestCommonAncestor(participants: readonly ReplAncestry[]): string | undefined { + if (participants.length === 0) { + return undefined; + } + const paths = participants.map((ancestry) => [...ancestry].reverse()); + const [first, ...rest] = paths; + let shared = 0; + while (shared < first.length && rest.every((path) => path[shared] === first[shared])) { + shared += 1; + } + return shared === 0 ? undefined : first[shared - 1]; +} + +interface Subscriber { + /** The last frame acknowledged. */ + applied: number; + /** The last frame handed over, which may still be being drawn. */ + received: number; + waiting: { resolve(tick: ReplFrameTick): void } | undefined; +} + +/** Open the frame stream for the calling scope. */ +export function useReplFrames(interval: number = FRAME_INTERVAL): Operation { + return resource(function* (provide) { + let ticks = 0; + let current: ReplFrameTick | undefined; + let previous: number | undefined; + const live = new Set(); + let outstanding = new Set(); + let gate = withResolvers(); + + /** Let the driver reconsider: demand or acknowledgement changed. */ + function nudge(): void { + gate.resolve(); + } + + yield* spawn(function* driver(): Operation { + while (true) { + if (live.size === 0 || outstanding.size > 0) { + // Settled, or still waiting on somebody. Either way there is nothing + // to schedule, so this suspends on the gate rather than on a timer. + yield* gate.operation; + gate = withResolvers(); + continue; + } + yield* ReplClock.operations.wait(interval); + if (live.size === 0) { + continue; + } + const at = yield* ReplClock.operations.now(); + ticks += 1; + const tick: ReplFrameTick = Object.freeze({ + id: ticks, + at, + delta: previous === undefined ? 0 : at - previous, + }); + previous = at; + current = tick; + outstanding = new Set(live); + for (const subscriber of live) { + const waiter = subscriber.waiting; + subscriber.waiting = undefined; + waiter?.resolve(tick); + } + } + }); + + yield* provide({ + subscribe(request) { + return resource>(function* (give) { + const expected = nearestCommonAncestor(request.participants); + if (expected !== request.owner) { + // Nothing registered, so an unheld claim cannot keep the stream + // awake while its holder waits for a frame that will never come. + yield* give(Err(new ReplFrameOwnerError(request.owner, expected))); + return; + } + + const subscriber: Subscriber = { applied: 0, received: 0, waiting: undefined }; + // Registered before the subscriber is anywhere the driver can see it. + // Adding first would put a subscriber in the demand set with no way to + // take it back out, and a cancellation there leaves the stream awake + // for something that has already gone. + yield* ensure(function* () { + live.delete(subscriber); + // Its demand goes; its acknowledgement does not arrive. A cancelled + // subscriber did not apply the frame it was holding, and saying it + // did would be the stream lying about what is on screen. + outstanding.delete(subscriber); + // Leaving can be what finally completes a frame, and it is always + // what settles the stream. + nudge(); + }); + + live.add(subscriber); + if (current !== undefined) { + // Joining mid-flight owes the current frame like everyone else. + outstanding.add(subscriber); + } + nudge(); + + yield* give( + Ok({ + *next(): Operation { + if (current !== undefined && subscriber.received !== current.id) { + const tick = current; + subscriber.received = tick.id; + return tick; + } + const waiter = withResolvers(); + subscriber.waiting = waiter; + const tick = yield* waiter.operation; + subscriber.received = tick.id; + return tick; + }, + acknowledge(): void { + if (subscriber.received === subscriber.applied) { + return; + } + subscriber.applied = subscriber.received; + outstanding.delete(subscriber); + nudge(); + }, + applied(): number { + return subscriber.applied; + }, + received(): number { + return subscriber.received; + }, + }), + ); + }); + }, + demanded(): boolean { + return live.size > 0; + }, + published(): number { + return ticks; + }, + outstanding(): number { + return outstanding.size; + }, + }); + }); +} diff --git a/packages/cli/src/repl/input.ts b/packages/cli/src/repl/input.ts new file mode 100644 index 00000000..5f8f5375 --- /dev/null +++ b/packages/cli/src/repl/input.ts @@ -0,0 +1,214 @@ +/** + * Turning terminal bytes into the events the tree already understands. + * + * The tty scanner does the decoding; this decides which decoded events the + * composition layer has a meaning for and hands them over in its own closed + * shape. Nothing here knows an action name: a normalized event says what + * happened and where, and what it means is decided by whichever mounted node + * claims it. + * + * ## A lone Escape is not yet Escape + * + * `ESC` begins most of the sequences a terminal sends, so a scanner that saw + * one and nothing else cannot know whether a person pressed Escape or an arrow + * key is still arriving. It says so, with a latency to wait. Two things can + * happen next. More bytes arrive, and they are rescanned *with the pending + * prefix still held*, so one sequence is never split into a false Escape + * followed by text — that one the scanner does itself. Or the latency expires + * with nothing following, and the held byte was the Escape key. + * + * The scanner in `@bomb.sh/tty` 0.9.0 does not resolve that second case: + * rescanning with no bytes, as its documentation describes, returns the same + * pending report and no event, however many times it is called. So the decision + * is made here, where it is a normalization decision rather than a decoding + * one: a scan with nothing new that still reports a held Escape *is* the + * Escape, because "pending" means the scanner is holding a lone `ESC` and + * nothing followed it within the latency it asked for. + * + * Emitting it leaves the spent `ESC` in the scanner's buffer, where it would + * join the next keystroke and turn an `a` into an Alt-`a`. There is no way to + * clear that buffer, so the decoder is replaced instead. That costs one + * instantiation, and only when somebody actually presses Escape by itself. + */ + +import { createInput, type Input, type InputEvent, type ScanResult } from "@bomb.sh/tty"; +import { type Operation, resource, until } from "effection"; + +import type { ReplInputEvent, ReplKey } from "./description.ts"; + +/** Where a pointer event landed, before any frame has been consulted. */ +export interface ReplPointerAt { + readonly column: number; + readonly row: number; +} + +/** A normalized event together with where a pointer one landed. */ +export interface ReplNormalized { + readonly event: ReplInputEvent | undefined; + readonly at: ReplPointerAt | undefined; +} + +/** + * The keys this composition layer has a meaning for. + * + * Everything else a terminal can send is decoded and then dropped here rather + * than invented into an action: a key nothing claims is not an event the tree + * should be asked about. + * + * `Backspace` is the terminal's `DEL` (`0x7f`), which is what a keyboard's + * Backspace key actually sends. `0x08` decodes as Control-H and is a chord, not + * this key. + */ +const KEYS: ReadonlyMap = new Map([ + ["Enter", "Enter"], + ["Escape", "Escape"], + ["Tab", "Tab"], + ["Backtab", "Backtab"], + ["Backspace", "Backspace"], +]); + +/** + * The physical key a pasted newline arrives as. + * + * A terminal sends `LF` inside a paste, and the decoder reports `LF` as + * Control-J with no text of its own. Pasting a multi-line document is the whole + * point of an XMD draft, so this one chord is text — and it is the only chord + * that is. + */ +const NEWLINE_CHORD = "j"; + +/** + * Normalize one decoded event. + * + * A pointer event carries where it landed but no target: which node is there is + * the committed frame's answer, and asking the decoder to know it would put the + * frame's geometry in two places. + * + * Text comes from the payload the decoder produced, never from the physical key + * code. A chord's code is a letter — Control-C is `c`, Alt-a is `a` — so reading + * the code would type the letter somebody pressed Control with, which is how an + * editor ends up inserting a `c` when a person asked it to stop. + */ +export function normalize(event: InputEvent): ReplNormalized { + if (event.type === "keydown" || event.type === "keyrepeat") { + if (event.ctrl === true && event.alt !== true && event.code === NEWLINE_CHORD) { + return { event: Object.freeze({ kind: "text", text: "\n" }), at: undefined }; + } + if (event.ctrl === true || event.alt === true) { + // A chord nothing here claims. Dropped whole, payload included. + return { event: undefined, at: undefined }; + } + const key = KEYS.get(event.code); + if (key !== undefined) { + return { event: Object.freeze({ kind: "key", key }), at: undefined }; + } + const text = event.text; + if (text !== undefined && text.length > 0) { + // As decoded: shifted, accented and multi-byte text all arrive here + // already assembled, and a string cannot be changed by whoever receives it. + return { event: Object.freeze({ kind: "text", text }), at: undefined }; + } + return { event: undefined, at: undefined }; + } + if (event.type === "mousedown" && event.button === "left") { + // The target is filled in by whoever holds the current frame; until then + // this is a position and nothing more. + return { event: undefined, at: { column: event.x, row: event.y } }; + } + return { event: undefined, at: undefined }; +} + +/** What one scan of the terminal's bytes produced. */ +export interface ReplInputScan { + /** Normalized keys and text, in the order the terminal sent them. */ + readonly events: readonly ReplInputEvent[]; + readonly pointers: readonly ReplPointerAt[]; + readonly resized: boolean; + /** + * Milliseconds to wait before rescanning with no bytes, when a lone Escape is + * held. Absent when nothing is pending. + */ + readonly pendingFor: number | undefined; +} + +/** + * Scan bytes, or rescan with none to settle a pending Escape. + * + * Resize arrives here because the scanner decodes it, and leaves again as a + * flag rather than an event: it is a host fact about the terminal, not + * something a mounted node claims. + */ +export function scanInput(input: Input, bytes?: Uint8Array): ReplInputScan { + const result: ScanResult = bytes === undefined ? input.scan() : input.scan(bytes); + const events: ReplInputEvent[] = []; + const pointers: ReplPointerAt[] = []; + let resized = false; + + for (const event of result.events) { + if (event.type === "resize") { + resized = true; + continue; + } + const normalized = normalize(event); + if (normalized.event !== undefined) { + events.push(normalized.event); + } + if (normalized.at !== undefined) { + pointers.push(normalized.at); + } + } + + return Object.freeze({ + events: Object.freeze(events), + pointers: Object.freeze(pointers), + resized, + pendingFor: result.pending?.delay, + }); +} + +/** The one Escape a flush can decide on. */ +const ESCAPE: ReplInputEvent = Object.freeze({ kind: "key", key: "Escape" }); + +/** A scanner that also settles a held Escape. */ +export interface ReplDecoder { + /** + * Scan bytes, or settle a held Escape by scanning with none. + * + * An operation because settling one replaces the scanner, and replacing it + * means building one. + */ + scan(bytes?: Uint8Array): Operation; +} + +/** Open a decoder owned by the calling scope. */ +export function useReplDecoder( + options: { readonly escLatency?: number } = {}, +): Operation { + const settings = options.escLatency === undefined ? {} : { escLatency: options.escLatency }; + return resource(function* (provide) { + let input: Input = yield* until(createInput(settings)); + yield* provide({ + *scan(bytes?: Uint8Array): Operation { + const scanned = scanInput(input, bytes); + const settling = + bytes === undefined && + scanned.pendingFor !== undefined && + scanned.events.length === 0 && + scanned.pointers.length === 0; + if (!settling) { + return scanned; + } + // Nothing followed the held ESC, so it was the key. A fresh scanner, + // because the spent byte is still in this one's buffer and would + // otherwise become the first byte of whatever is typed next. + input = yield* until(createInput(settings)); + return Object.freeze({ + events: Object.freeze([ESCAPE]), + pointers: Object.freeze([]), + resized: scanned.resized, + pendingFor: undefined, + }); + }, + }); + }); +} diff --git a/packages/cli/src/repl/layout.ts b/packages/cli/src/repl/layout.ts new file mode 100644 index 00000000..91f845b3 --- /dev/null +++ b/packages/cli/src/repl/layout.ts @@ -0,0 +1,323 @@ +/** + * Where everything goes, at whatever size the terminal happens to be. + * + * Presentation and nothing else. Layout decides placement; it never changes + * which model objects are selected, which node a control belongs to or what an + * action means. Resizing a terminal therefore moves things and changes nothing + * else — which is why the frame it produces names the live node behind every + * cell rather than carrying a copy of what that node holds. + * + * ## Four profiles, and a refusal + * + * `160x36` is wide, `120x30` is medium, `72x20` is narrow, and anything smaller + * than narrow is a refusal rather than a squeezed layout. The refusal is + * explicit and recovers on resize: nothing is torn down, and no control it hides + * can be activated while it is showing. + * + * Wide and medium carry the sidebar, the transcript, the bindings/history + * inspection column, the drawer layer and the fixed full-width footer. Narrow + * keeps the drawer and the footer and gives the rest of the screen to one routed + * content surface. The History band is five rows at every size; when the labels + * of several markers cannot fit, their *visual* labels are grouped and every + * marker keeps its own identity in the frame. + * + * Geometry is integer and deterministic: the same size and the same surface + * produce the same frame, cell for cell and bound for bound. + */ + +import type { ReplTerminalSize } from "./terminal.ts"; + +/** Which layout one size gets. */ +export type ReplProfile = "wide" | "medium" | "narrow" | "too-small"; + +/** The named areas a frame can have. */ +export type ReplRegion = + | "sidebar" + | "transcript" + | "inspection" + | "content" + | "drawer" + | "footer" + | "refusal"; + +/** Integer cell geometry. */ +export interface ReplBounds { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} + +/** One thing the application wants shown, and the node it belongs to. */ +export interface ReplSurfaceCell { + /** The live node this came from. Presentation never invents one. */ + readonly node: string; + readonly text: string; + /** Whether a pointer may activate it. */ + readonly targetable?: true; +} + +/** One selectable history position, as the band would label it. */ +export interface ReplSurfaceMarker { + readonly marker: string; + readonly label: string; +} + +/** What the application wants on screen, before anything decides where. */ +export interface ReplSurface { + /** + * The one surface a narrow frame shows, chosen by the route. + * + * Named separately rather than derived from the regions below, because which + * surface a narrow screen is showing is a routing decision the application + * makes and not one placement is entitled to invent. + */ + readonly content: readonly ReplSurfaceCell[]; + /** Present and possibly empty: an empty Sessions section is still a section. */ + readonly sessions: readonly ReplSurfaceCell[]; + readonly entries: readonly ReplSurfaceCell[]; + readonly transcript: readonly ReplSurfaceCell[]; + readonly inspection: readonly ReplSurfaceCell[]; + /** Absent when no drawer is open. */ + readonly drawer: readonly ReplSurfaceCell[]; + readonly footer: readonly ReplSurfaceCell[]; + readonly history: readonly ReplSurfaceMarker[]; +} + +/** One placed cell: what to draw, where, and which node it is. */ +export interface ReplPlacedCell { + /** The rendered element's id. Stable for one node in one region. */ + readonly id: string; + readonly node: string; + readonly region: ReplRegion; + readonly text: string; + readonly bounds: ReplBounds; + readonly targetable: boolean; +} + +/** One marker as the band shows it, grouped or not. */ +export interface ReplPlacedMarker { + readonly marker: string; + /** The label shown. Several markers may share one when space is short. */ + readonly label: string; + /** The other markers sharing that label, this one excluded. */ + readonly grouped: readonly string[]; +} + +/** One frame, decided and nothing more. */ +export interface ReplSemanticFrame { + readonly profile: ReplProfile; + readonly size: ReplTerminalSize; + readonly regions: readonly { readonly region: ReplRegion; readonly bounds: ReplBounds }[]; + readonly cells: readonly ReplPlacedCell[]; + /** Exactly five rows, whatever the size. */ + readonly historyRows: readonly string[]; + readonly markers: readonly ReplPlacedMarker[]; + /** The sentence a too-small frame shows, and nothing else it shows. */ + readonly refusal: string | undefined; +} + +/** The narrow profile's size, which is also the minimum this REPL draws at. */ +export const NARROW: ReplTerminalSize = { columns: 72, rows: 20 }; +const MEDIUM: ReplTerminalSize = { columns: 120, rows: 30 }; +const WIDE: ReplTerminalSize = { columns: 160, rows: 36 }; + +/** The footer is one border row, five History rows and one input row. */ +const FOOTER_ROWS = 7; +/** The History band's own five rows. */ +export const HISTORY_ROWS = 5; + +/** How wide a column is at each profile that has one. */ +interface ColumnWidths { + readonly wide: number; + readonly medium: number; +} + +const SIDEBAR: ColumnWidths = { wide: 32, medium: 28 }; +const INSPECTION: ColumnWidths = { wide: 36, medium: 28 }; + +/** Which profile a size gets. */ +export function profileFor(size: ReplTerminalSize): ReplProfile { + if (size.columns < NARROW.columns || size.rows < NARROW.rows) { + return "too-small"; + } + if (size.columns >= WIDE.columns && size.rows >= WIDE.rows) { + return "wide"; + } + if (size.columns >= MEDIUM.columns && size.rows >= MEDIUM.rows) { + return "medium"; + } + return "narrow"; +} + +/** Lay one surface out at one size. */ +export function layout(size: ReplTerminalSize, surface: ReplSurface): ReplSemanticFrame { + const profile = profileFor(size); + if (profile === "too-small") { + return refuse(size); + } + + const body = size.rows - FOOTER_ROWS; + const footer: ReplBounds = { x: 0, y: body, width: size.columns, height: FOOTER_ROWS }; + const regions: { region: ReplRegion; bounds: ReplBounds }[] = []; + const cells: ReplPlacedCell[] = []; + + if (profile === "narrow") { + const content: ReplBounds = { x: 0, y: 0, width: size.columns, height: body }; + regions.push({ region: "content", bounds: content }); + // Exactly the routed surface, and nothing else. A narrow screen that stacked + // every region would be a wide screen with the columns removed: the reader + // would scroll past three lists to reach the one they asked for, and every + // row of the other two would still be a target. + place(cells, "content", content, surface.content); + } else { + const sidebarWidth = profile === "wide" ? SIDEBAR.wide : SIDEBAR.medium; + const inspectionWidth = profile === "wide" ? INSPECTION.wide : INSPECTION.medium; + const sidebar: ReplBounds = { x: 0, y: 0, width: sidebarWidth, height: body }; + const inspection: ReplBounds = { + x: size.columns - inspectionWidth, + y: 0, + width: inspectionWidth, + height: body, + }; + const transcript: ReplBounds = { + x: sidebarWidth, + y: 0, + width: size.columns - sidebarWidth - inspectionWidth, + height: body, + }; + regions.push( + { region: "sidebar", bounds: sidebar }, + { region: "transcript", bounds: transcript }, + { region: "inspection", bounds: inspection }, + ); + place(cells, "sidebar", sidebar, [...surface.sessions, ...surface.entries]); + place(cells, "transcript", transcript, surface.transcript); + place(cells, "inspection", inspection, surface.inspection); + } + + if (surface.drawer.length > 0) { + // Over the body, inset, and above the footer rather than across it. + const drawer: ReplBounds = { + x: Math.floor(size.columns / 8), + y: Math.floor(body / 8), + width: size.columns - 2 * Math.floor(size.columns / 8), + height: body - 2 * Math.floor(body / 8), + }; + regions.push({ region: "drawer", bounds: drawer }); + place(cells, "drawer", drawer, surface.drawer); + } + + regions.push({ region: "footer", bounds: footer }); + place(cells, "footer", footer, surface.footer); + + const band = grouped(surface.history, size.columns); + return Object.freeze({ + profile, + size: Object.freeze({ ...size }), + regions: Object.freeze( + regions.map((region) => Object.freeze({ ...region, bounds: Object.freeze(region.bounds) })), + ), + cells: Object.freeze(cells), + historyRows: Object.freeze(rowsFor(band, size.columns)), + markers: Object.freeze(band), + refusal: undefined, + }); +} + +/** The one frame a terminal too small to draw in gets. */ +function refuse(size: ReplTerminalSize): ReplSemanticFrame { + const bounds: ReplBounds = { x: 0, y: 0, width: size.columns, height: size.rows }; + return Object.freeze({ + profile: "too-small", + size: Object.freeze({ ...size }), + regions: Object.freeze([Object.freeze({ region: "refusal", bounds: Object.freeze(bounds) })]), + // No cell at all, so nothing this frame hides can be pointed at: a control + // that is not in the frame is not in its target map either. + cells: Object.freeze([]), + historyRows: Object.freeze(new Array(HISTORY_ROWS).fill("")), + markers: Object.freeze([]), + refusal: + `This REPL needs at least ${NARROW.columns}x${NARROW.rows}; this terminal is ` + + `${size.columns}x${size.rows}. Make the window larger.`, + }); +} + +/** Stack one region's cells, one row each, clipped to the region's height. */ +function place( + into: ReplPlacedCell[], + region: ReplRegion, + bounds: ReplBounds, + contents: readonly ReplSurfaceCell[], +): void { + contents.slice(0, bounds.height).forEach((content, row) => { + into.push( + Object.freeze({ + id: `${region}:${content.node}`, + node: content.node, + region, + text: content.text, + bounds: Object.freeze({ x: bounds.x, y: bounds.y + row, width: bounds.width, height: 1 }), + targetable: content.targetable === true, + }), + ); + }); +} + +/** + * Label the markers, grouping their labels when they cannot all fit. + * + * Grouping is visual. Every marker stays in the frame under its own name, and + * the ones sharing a label say which others they share it with — so a compact + * band never costs a reader the ability to select an exact position. + */ +function grouped(markers: readonly ReplSurfaceMarker[], columns: number): ReplPlacedMarker[] { + // Five rows wide as the band is wide. Grouping is about the band's whole + // budget, not one row's: a label that will not fit row three may fit row four. + const budget = HISTORY_ROWS * Math.max(1, columns - 2); + const separate = markers.reduce((width, marker) => width + marker.label.length + 1, 0); + if (markers.length === 0 || separate <= budget) { + return markers.map((marker) => Object.freeze({ ...marker, grouped: Object.freeze([]) })); + } + + // A grouped label is its first member's plus a count, so budget for that. + const widest = markers.reduce((width, marker) => Math.max(width, marker.label.length), 1) + 4; + const affordable = Math.max(1, Math.floor(budget / (widest + 1))); + // At least two: reaching here means one-per-group would group nothing and the + // band would overflow exactly as before. + const perGroup = Math.max(2, Math.ceil(markers.length / affordable)); + const placed: ReplPlacedMarker[] = []; + for (let start = 0; start < markers.length; start += perGroup) { + const group = markers.slice(start, start + perGroup); + const label = group.length === 1 ? group[0].label : `${group[0].label} +${group.length - 1}`; + for (const marker of group) { + placed.push( + Object.freeze({ + marker: marker.marker, + label, + grouped: Object.freeze( + group.filter((other) => other !== marker).map((other) => other.marker), + ), + }), + ); + } + } + return placed; +} + +/** The five rows the History band shows. */ +function rowsFor(markers: readonly ReplPlacedMarker[], columns: number): string[] { + const rows = new Array(HISTORY_ROWS).fill(""); + const labels: string[] = []; + for (const marker of markers) { + if (!labels.includes(marker.label)) { + labels.push(marker.label); + } + } + labels.forEach((label, index) => { + const row = index % HISTORY_ROWS; + const next = rows[row] === "" ? label : `${rows[row]} ${label}`; + rows[row] = next.length <= columns ? next : rows[row]; + }); + return rows; +} diff --git a/packages/cli/src/repl/renderer.ts b/packages/cli/src/repl/renderer.ts new file mode 100644 index 00000000..76ea2357 --- /dev/null +++ b/packages/cli/src/repl/renderer.ts @@ -0,0 +1,370 @@ +/** + * Drawing one decided frame, and answering where things ended up. + * + * The renderer is the only thing that talks to `@bomb.sh/tty`'s layout engine, + * and it does two jobs: it turns a laid-out frame into terminal bytes, and it + * reports the geometry that came back so a pointer landing at a column and row + * can be answered with **the exact live node** that occupies it. It never + * invents a node: every target it publishes came from a cell that named one, + * and a cell whose node is not currently mounted is not drawn at all. + * + * ## Why a snapshot + * + * Every render input is retained as a frozen snapshot, because the layout + * engine has fixed arenas and can refuse a frame that is too large for them. + * Recovering from that means throwing the engine away and building a new one at + * the dimensions the frame was laid out for — those arenas are sized from the + * terminal's width and height, so a frame that overflows an engine built for a + * smaller terminal fits one built for this frame's own size. Then the same + * logical frame is drawn again. Without the snapshot, "the same frame" would + * have to be reassembled from live state that may already have moved on, and a + * recovery would quietly change what the user is looking at. With it, recovery + * is invisible: the same selection, the same focus, the same actions, the same + * frame map. + * + * Output bytes are copied out of the engine's memory the moment they arrive. + * `render()` hands back a view into WASM memory that the next render + * invalidates, so anything held past that call has to own its bytes. + */ + +import { Err, Ok, type Operation, resource, type Result, until } from "effection"; +import { close, createTerm, fixed, type Op, open, type PointerEvent, text } from "@bomb.sh/tty"; +import type { Term } from "@bomb.sh/tty"; +import type { ReplInputEvent } from "./description.ts"; +import type { ReplBounds, ReplPlacedCell, ReplSemanticFrame } from "./layout.ts"; + +/** The layout-engine failures that a bigger arena would fix. */ +const CAPACITY = new Set([ + "ARENA_CAPACITY_EXCEEDED", + "ELEMENTS_CAPACITY_EXCEEDED", + "TEXT_MEASUREMENT_CAPACITY_EXCEEDED", +]); + +/** A render that could not be completed. */ +export class ReplRenderError extends Error { + override name = "ReplRenderError"; +} + +/** Everything one render needs, frozen so a recovery can repeat it exactly. */ +export interface ReplRenderSnapshot { + /** Where everything goes. */ + readonly frame: ReplSemanticFrame; + /** The Slice C frame id this was laid out from, carried for identity. */ + readonly tree: number; + /** Every currently mounted node id. A cell naming anything else is dropped. */ + readonly mounted: readonly string[]; + /** Seconds since the last frame, for the engine's transitions. */ + readonly deltaTime: number; + /** The pointer position and button state, when there is one. */ + readonly pointer: { readonly x: number; readonly y: number; readonly down: boolean } | undefined; +} + +/** One drawn element and the live node behind it. */ +export interface ReplTarget { + /** The element id the engine measured. */ + readonly id: string; + /** The live node. Never fabricated. */ + readonly node: string; + readonly bounds: ReplBounds; +} + +/** Where everything landed. Immutable, and retained with its frame. */ +export interface ReplFrameMap { + /** In draw order, so a later entry is drawn over an earlier one. */ + readonly targets: readonly ReplTarget[]; + /** The live node at one position, topmost first, or none. */ + at(x: number, y: number): string | undefined; + /** The geometry one element id was given, or none if it was not drawn. */ + boundsOf(id: string): ReplBounds | undefined; + /** The live node one element id belongs to. */ + nodeOf(id: string): string | undefined; +} + +/** What one render produced. */ +export interface ReplRendered { + /** The Slice C frame id that was drawn. */ + readonly tree: number; + /** Bytes this result owns. Safe to hold. */ + readonly output: Uint8Array; + readonly map: ReplFrameMap; + readonly pointers: readonly PointerEvent[]; + readonly animating: boolean; + /** Whether the engine had to be rebuilt to fit this frame. */ + readonly recovered: boolean; +} + +/** The renderer, for as long as its scope lives. */ +export interface ReplRenderer { + /** Draw one snapshot. */ + render(snapshot: ReplRenderSnapshot): Operation>; + /** Tell the engine the terminal changed size. */ + resize(size: { readonly columns: number; readonly rows: number }): void; + /** The last snapshot drawn, for a caller that wants to redraw it verbatim. */ + last(): ReplRenderSnapshot | undefined; +} + +/** Freeze one render input so repeating it cannot depend on anything mutable. */ +export function snapshotRender(input: ReplRenderSnapshot): ReplRenderSnapshot { + return Object.freeze({ + frame: input.frame, + tree: input.tree, + mounted: Object.freeze([...input.mounted]), + deltaTime: input.deltaTime, + pointer: input.pointer === undefined ? undefined : Object.freeze({ ...input.pointer }), + }); +} + +/** + * Build a renderer owned by the calling scope. + * + * The engine is a WASM instance, so it is a resource: one per renderer, torn + * down with the scope that asked for it, and replaced in place when a frame + * overflows its arenas. + */ +export function useReplRenderer(size: { + readonly columns: number; + readonly rows: number; +}): Operation { + return resource(function* (provide) { + let term = yield* until(createTerm({ width: size.columns, height: size.rows })); + let retained: ReplRenderSnapshot | undefined; + + function* rebuild(frame: ReplSemanticFrame): Operation { + // Sized for the frame in hand, which is the terminal's current size: the + // engine's measurement arenas scale with width and height, so an engine + // built when the terminal was smaller is exactly the one that overflows. + term = yield* until(createTerm({ width: frame.size.columns, height: frame.size.rows })); + } + + yield* provide({ + *render(snapshot) { + let frozen = snapshotRender(snapshot); + retained = frozen; + let ops = opsFor(frozen); + + let first = attempt(term, ops, frozen); + if (first.kind === "drawn") { + return Ok({ ...first.rendered, recovered: false }); + } + if (!first.capacity) { + return Err(new ReplRenderError(first.reason)); + } + + yield* rebuild(frozen.frame); + let second = attempt(term, ops, frozen); + if (second.kind === "drawn") { + return Ok({ ...second.rendered, recovered: true }); + } + return Err( + new ReplRenderError( + `the frame does not fit the layout engine even after rebuilding it: ${second.reason}`, + ), + ); + }, + resize(next) { + term.update({ width: next.columns, height: next.rows }); + }, + last() { + return retained; + }, + }); + }); +} + +/** + * What one pass at the engine did. + * + * Not a `Result`: the caller has to tell a refusal a bigger engine would fix + * from one it would not, and that is a second question about the same failure + * rather than a different outcome. + */ +type Attempted = + | { readonly kind: "drawn"; readonly rendered: Omit } + | { readonly kind: "refused"; readonly capacity: boolean; readonly reason: string }; + +/** One pass at the engine, classifying whatever came back. */ +function attempt(term: Term, ops: Op[], snapshot: ReplRenderSnapshot): Attempted { + let result; + try { + result = term.render(ops, { + row: 1, + deltaTime: snapshot.deltaTime, + ...(snapshot.pointer === undefined ? {} : { pointer: { ...snapshot.pointer } }), + }); + } catch (error) { + // `pack` throws rather than reporting when the ops themselves outgrow the + // transfer buffer, which is the same problem a bigger engine solves. + let reason = error instanceof Error ? error.message : String(error); + return { kind: "refused", capacity: error instanceof RangeError, reason }; + } + + let capacity = result.errors.filter((error) => CAPACITY.has(error.type)); + if (capacity.length > 0) { + return { + kind: "refused", + capacity: true, + reason: capacity.map((error) => `${error.type}: ${error.message}`).join("; "), + }; + } + if (result.errors.length > 0) { + return { + kind: "refused", + capacity: false, + reason: result.errors.map((error) => `${error.type}: ${error.message}`).join("; "), + }; + } + + return { + kind: "drawn", + rendered: { + tree: snapshot.tree, + // Copied now: this view points into the engine's memory and the next + // render invalidates it. + output: new Uint8Array(result.output), + map: mapOf(snapshot, result.info), + pointers: Object.freeze([...result.events]), + animating: result.animating, + }, + }; +} + +/** The cells of one frame that name a node the tree still has mounted. */ +function drawn(snapshot: ReplRenderSnapshot): readonly ReplPlacedCell[] { + let mounted = new Set(snapshot.mounted); + return snapshot.frame.cells.filter((cell) => mounted.has(cell.node)); +} + +/** Turn one laid-out frame into engine ops. */ +function opsFor(snapshot: ReplRenderSnapshot): Op[] { + let { frame } = snapshot; + let ops: Op[] = [ + open("repl", { + layout: { + width: fixed(frame.size.columns), + height: fixed(frame.size.rows), + direction: "ttb", + }, + }), + ]; + + if (frame.refusal !== undefined) { + ops.push(open("repl:refusal", { layout: { width: fixed(frame.size.columns) } })); + ops.push(text(frame.refusal)); + ops.push(close()); + ops.push(close()); + return ops; + } + + for (let cell of drawn(snapshot)) { + ops.push( + open(cell.id, { + layout: { width: fixed(cell.bounds.width), height: fixed(cell.bounds.height) }, + floating: { x: cell.bounds.x, y: cell.bounds.y, attachTo: "root" }, + }), + ); + ops.push(text(cell.text)); + ops.push(close()); + } + + ops.push( + open("repl:history", { + layout: { width: fixed(frame.size.columns), height: fixed(frame.historyRows.length) }, + }), + ); + for (let row of frame.historyRows) { + ops.push(text(row)); + } + ops.push(close()); + + ops.push(close()); + return ops; +} + +/** The frame map: what the engine measured, paired with the node that asked. */ +function mapOf( + snapshot: ReplRenderSnapshot, + info: { + get( + id: string, + ): { bounds: { x: number; y: number; width: number; height: number } } | undefined; + }, +): ReplFrameMap { + let targets: ReplTarget[] = []; + for (let cell of drawn(snapshot)) { + let measured = info.get(cell.id); + if (measured === undefined) { + // Not measured means not on screen, and something not on screen must not + // be pointable: leaving it out is the whole point of the map. + continue; + } + if (!cell.targetable) { + continue; + } + targets.push( + Object.freeze({ + id: cell.id, + node: cell.node, + bounds: Object.freeze({ + x: measured.bounds.x, + y: measured.bounds.y, + width: measured.bounds.width, + height: measured.bounds.height, + }), + }), + ); + } + + let frozen = Object.freeze(targets); + let byId = new Map(frozen.map((target) => [target.id, target])); + let map: ReplFrameMap = { + targets: frozen, + at(x, y) { + // Back to front: the last thing drawn is the thing on top, which is how a + // drawer keeps a pointer off the controls it covers. + for (let index = frozen.length - 1; index >= 0; index -= 1) { + let { bounds, node } = frozen[index]; + if ( + x >= bounds.x && + x < bounds.x + bounds.width && + y >= bounds.y && + y < bounds.y + bounds.height + ) { + return node; + } + } + return undefined; + }, + boundsOf(id) { + return byId.get(id)?.bounds; + }, + nodeOf(id) { + return byId.get(id)?.node; + }, + }; + return Object.freeze(map); +} + +/** The pointer member of the normalized union, which is all this can produce. */ +export type ReplPointerInput = Extract; + +/** + * Which node a pointer landed on, according to the frame it was resolved + * against. + * + * The frame id travels with the answer, so a pointer resolved against a frame + * the tree has moved past arrives at `dispatch` carrying that older number and + * is dropped there. That is why this takes a whole render result rather than a + * map: a map on its own could be paired with any frame id, and a pointer that + * borrowed a current number would reach a node from a tree that no longer + * exists. + */ +export function resolvePointer( + rendered: ReplRendered, + at: { readonly column: number; readonly row: number }, +): ReplPointerInput | undefined { + const target = rendered.map.at(at.column, at.row); + if (target === undefined) { + return undefined; + } + return { kind: "pointer", target, frame: rendered.tree }; +} diff --git a/packages/cli/src/repl/screen.ts b/packages/cli/src/repl/screen.ts new file mode 100644 index 00000000..79ca0d93 --- /dev/null +++ b/packages/cli/src/repl/screen.ts @@ -0,0 +1,241 @@ +/** + * Everything the REPL has to give back, and the one place that gives it back. + * + * Opening a terminal for a full-screen interface changes things that belong to + * whoever ran the command: raw mode, the alternate buffer, the cursor, mouse + * reporting, the keyboard protocol. Every one of those has to be undone on + * every way out — the run finishing, a refusal, a failure, a cancellation, and + * the person closing the input. So there is exactly one owner of all of it, it + * registers the undo **before** it does the do, and the final reset is written + * once whatever happened. + * + * ## Cleanup before capability + * + * Each step here registers its teardown first and only then takes the thing it + * is undoing. A cancellation that lands between the two orders would otherwise + * leave a raw terminal with no cursor and nobody left who knows to fix it — + * which is not a crash, it is a shell the person has to blindly type `reset` + * into. + * + * ## What comes out + * + * Normalized events and nothing else. The screen decodes bytes, reads the + * terminal's size and reports both; it does not know what any of it means. A + * key is a key, a pointer is a column and a row, a resize is a size, and end of + * input is a lifecycle outcome rather than a keystroke. + */ + +import { + createSignal, + ensure, + type Operation, + resource, + spawn, + type Stream, + type Subscription, + type Task, +} from "effection"; +import { + alternateBuffer, + cursor, + mouseTracking, + progressiveInput, + type Setting, + settings, +} from "@bomb.sh/tty"; +import { ReplTerminal, type ReplTerminalSize } from "./terminal.ts"; +import { ReplClock } from "./frame.ts"; +import { type ReplPointerAt, useReplDecoder } from "./input.ts"; +import type { ReplInputEvent } from "./description.ts"; + +/** What the screen reports. */ +export type ReplScreenEvent = + | { readonly kind: "input"; readonly event: ReplInputEvent } + | { readonly kind: "pointer"; readonly at: ReplPointerAt } + | { readonly kind: "resize"; readonly size: ReplTerminalSize } + /** Input ended. A lifecycle outcome, not a key. */ + | { readonly kind: "eof" }; + +/** The open terminal, for as long as its scope lives. */ +export interface ReplScreen { + /** The size as it stands. */ + size(): Operation; + /** Put one rendered frame on the terminal. */ + present(bytes: Uint8Array): Operation; + /** + * Everything that happened, in order, from the moment the screen opened. + * + * One consumer. The subscription is taken when the screen opens rather than + * when the caller gets around to reading, so nothing that arrives in between + * is lost. + */ + events(): Stream; +} + +/** + * The modes a full-screen REPL takes, and therefore has to give back. + * + * One `Setting`, so there is one `apply` and one `revert` and no way to restore + * three of four things. + */ +export function replModes(): Setting { + return settings( + alternateBuffer({ clear: true }), + cursor(false), + mouseTracking(), + // Level 1: disambiguated escape codes, which is what makes a lone Escape + // decidable at all on terminals that support it. + progressiveInput(1), + ); +} + +/** Open the terminal for the calling scope. */ +export function useReplScreen(options?: { readonly escLatency?: number }): Operation { + return resource(function* (provide) { + const decoder = yield* useReplDecoder( + options?.escLatency === undefined ? {} : { escLatency: options.escLatency }, + ); + const modes = replModes(); + + let restored = false; + // Registered before the modes are applied, so a cancellation in between + // still restores. Written once: the flag is what makes "exactly once" true + // across a teardown that runs after an earlier failure already reset. + yield* ensure(function* (): Operation { + if (restored) { + return; + } + restored = true; + yield* ReplTerminal.operations.setRaw(false); + yield* ReplTerminal.operations.writeNow(modes.revert); + }); + yield* ReplTerminal.operations.setRaw(true); + yield* ReplTerminal.operations.write(modes.apply); + + const events = createSignal(); + // Subscribed here, not in `events()`: a subscription taken later would miss + // whatever arrived first, and the first thing that arrives is usually the + // size the renderer needs. + const reported: Subscription = yield* events; + + // Bytes, or nothing at all — which means "the pending Escape's latency + // expired, scan again with no new input". One queue and therefore one place + // the scanner ever waits: a scanner waiting on two things would have to race + // them, and the loser of that race is a read that was already in flight. + const chunks = createSignal(); + const incoming: Subscription = yield* chunks; + + let delivered: ReplTerminalSize | undefined; + function deliverResize(size: ReplTerminalSize): void { + if ( + delivered !== undefined && + delivered.columns === size.columns && + delivered.rows === size.rows + ) { + // The same size twice is the same frame twice. Terminals report a + // resize both in band and out of band, and either one is enough. + return; + } + delivered = size; + events.send({ kind: "resize", size }); + } + + /** Publish what one scan produced. */ + function* publish(bytes?: Uint8Array): Operation { + const scan = yield* decoder.scan(bytes); + for (const event of scan.events) { + events.send({ kind: "input", event }); + } + for (const at of scan.pointers) { + events.send({ kind: "pointer", at }); + } + if (scan.resized) { + deliverResize(yield* ReplTerminal.operations.size()); + } + return scan.pendingFor; + } + + yield* spawn(function* reader(): Operation { + const source = yield* ReplTerminal.operations.input(); + while (true) { + const next = yield* source.next(); + if (next.done === true) { + chunks.close(); + return; + } + chunks.send(next.value); + } + }); + + yield* spawn(function* scanner(): Operation { + // The one-shot task that turns a held Escape into an Escape. It exists for + // exactly as long as the Escape is undecided. + let latency: Task | undefined; + + while (true) { + const next = yield* incoming.next(); + if (next.done === true) { + // Flush first: a held Escape is something the person pressed, and + // losing it because the stream then ended would be a lost keystroke. + yield* publish(); + events.send({ kind: "eof" }); + events.close(); + return; + } + + const waiting = latency; + latency = undefined; + if (waiting !== undefined) { + // Either it fired and sent this, or bytes beat it. Either way it is + // done, and leaving it running would flush an Escape that the bytes + // just now decided was part of a sequence. + yield* waiting.halt(); + } + + // `undefined` is the flush: rescanning with no bytes emits the Escape. + // Bytes are rescanned with the held prefix still in the decoder, so one + // sequence is never split into a false Escape followed by text. + const pending = yield* publish(next.value); + if (pending !== undefined) { + latency = yield* spawn(function* (): Operation { + yield* ReplClock.operations.wait(pending / 1000); + chunks.send(undefined); + }); + } + } + }); + + // Subscribed before the task that reads it, because a task starts a turn + // after it is spawned and a terminal resized during that turn would be a + // resize nobody was listening for. + const sizes: Subscription = yield* ReplTerminal.operations.resizes(); + yield* spawn(function* watcher(): Operation { + while (true) { + const next = yield* sizes.next(); + if (next.done === true) { + return; + } + deliverResize(next.value); + } + }); + + deliverResize(yield* ReplTerminal.operations.size()); + + yield* provide({ + size(): Operation { + return ReplTerminal.operations.size(); + }, + present(bytes: Uint8Array): Operation { + return ReplTerminal.operations.write(bytes); + }, + events(): Stream { + return { + // deno-lint-ignore require-yield + *[Symbol.iterator]() { + return reported; + }, + }; + }, + }); + }); +} diff --git a/packages/cli/src/repl/terminal-host.ts b/packages/cli/src/repl/terminal-host.ts new file mode 100644 index 00000000..6013e4c2 --- /dev/null +++ b/packages/cli/src/repl/terminal-host.ts @@ -0,0 +1,241 @@ +/** + * The portable half of a REPL terminal, over capabilities a runtime supplies. + * + * `terminal.ts` says what the REPL needs; this says how to build it + * once a host has handed over five things it alone can do. Everything that is + * the same on every runtime — turning an async byte source into a stream with + * real backpressure, turning a resize notification into a stream of sizes, + * ending both when the scope ends — lives here, so the runtime-named factories + * contain nothing but their own runtime's spelling of those five capabilities. + * + * Nothing registers this. A runtime entrypoint installs it when the REPL + * command exists; until then it is a factory nobody calls, which is what keeps + * `@bomb.sh/tty` and raw mode off the path of every other command. + */ + +import { + createSignal, + ensure, + type Operation, + resource, + type Stream, + type Subscription, + until, +} from "effection"; +import { ReplTerminal, type ReplTerminalSize } from "./terminal.ts"; + +/** + * What a runtime has to provide, stated structurally. + * + * No host global appears in this module's types: a factory passes its own + * runtime's functions in by value, the same way the standard-input adapter + * takes a stream (Code Rule 12). + */ +export interface ReplTerminalCapabilities { + /** The size right now. */ + size(): ReplTerminalSize; + /** + * Hand bytes to the terminal, resolving once it has them. + * + * A promise rather than a fire-and-forget call, so a slow terminal slows the + * renderer down instead of queueing frames the user will never see. + */ + write(bytes: Uint8Array): Promise; + /** Hand bytes over with no suspension point inside the call. */ + writeNow(bytes: Uint8Array): void; + /** Turn raw mode on or off. */ + setRaw(raw: boolean): void; + /** + * Open the byte source. + * + * Called once per subscriber and closed when that subscriber's scope ends, + * which is what releases the terminal's input for whatever runs next. + */ + bytes(): AsyncIterable; + /** Watch for size changes; the returned function stops watching. */ + onResize(listener: () => void): () => void; +} + +/** Install one runtime's terminal for the calling scope. */ +export function installReplTerminal(host: ReplTerminalCapabilities): Operation { + return ReplTerminal.around( + { + // deno-lint-ignore require-yield + *size(): Operation { + return host.size(); + }, + *write([bytes]: [Uint8Array]): Operation { + yield* until(host.write(bytes)); + }, + writeNow([bytes]: [Uint8Array]): void { + host.writeNow(bytes); + }, + // deno-lint-ignore require-yield + *setRaw([raw]: [boolean]): Operation { + host.setRaw(raw); + }, + input(): Stream { + return byteStream(host); + }, + resizes(): Stream { + return sizeStream(host); + }, + }, + { at: "min" }, + ); +} + +/** + * The host's bytes, pulled one chunk at a time. + * + * Pulled, not pushed: the subscriber's `next()` is what asks the host for more, + * so an input burst cannot outrun whatever is decoding it and nothing has to + * buffer on its behalf. + */ +function byteStream(host: ReplTerminalCapabilities): Stream { + return resource>(function* (provide) { + // Declared first, released second, opened third. Opening the source before + // its release was registered leaves a window where a cancellation abandons a + // reader holding the terminal's input, and nothing is left that knows to + // close it. + let iterator: AsyncIterator | undefined; + yield* ensure(function* () { + const open = iterator; + if (open === undefined) { + return; + } + const close = open.return; + if (close !== undefined) { + // Releasing the source is what lets the next thing to run read the + // terminal; a reader left open holds input away from it. + yield* until(close.call(open)); + } + }); + iterator = host.bytes()[Symbol.asyncIterator](); + + const reading = iterator; + yield* provide({ + *next(): Operation> { + const result = yield* until(reading.next()); + if (result.done === true) { + return { done: true, value: undefined }; + } + return { done: false, value: result.value }; + }, + }); + }); +} + +/** + * Each size the terminal becomes. + * + * The size is read when the notification arrives rather than carried by it, + * because a resize that happens twice in quick succession should deliver where + * the terminal actually ended up. + */ +function sizeStream(host: ReplTerminalCapabilities): Stream { + return resource>(function* (provide) { + const sizes = createSignal(); + // Removal registered before the listener exists, for the same reason: a + // listener attached in a window with no teardown behind it outlives whoever + // wanted it and sends into a signal nobody is reading. + let stop: (() => void) | undefined; + yield* ensure(() => { + stop?.(); + }); + stop = host.onResize(() => { + sizes.send(host.size()); + }); + yield* provide(yield* sizes); + }); +} + +/** + * Write every byte through a sink that may take fewer than it was given. + * + * A partial write that nobody continued leaves a frame half drawn, which on a + * terminal means escape sequences cut in the middle. Portable because it is the + * same loop on every runtime whose write returns a count. + */ +export function writeAllTo( + sink: (chunk: Uint8Array) => Promise, + bytes: Uint8Array, +): Promise { + let written = 0; + const step = (): Promise => { + if (written >= bytes.length) { + return Promise.resolve(); + } + return sink(bytes.subarray(written)).then((count) => { + if (count <= 0) { + return Promise.reject(new Error("the terminal accepted none of the bytes it was given")); + } + written += count; + return step(); + }); + }; + return step(); +} + +/** + * The same sink for a host whose write takes a callback instead of returning. + * + * Node-shaped streams report completion rather than a count and handle partial + * writes themselves, so there is nothing to loop over — only a callback to turn + * into the promise the capability promises. + */ +export function writeThrough( + sink: (chunk: Uint8Array, done: (error?: Error | null) => void) => unknown, + bytes: Uint8Array, +): Promise { + return new Promise((resolve, reject) => { + sink(bytes, (error) => { + if (error === undefined || error === null) { + resolve(); + } else { + reject(error); + } + }); + }); +} + +/** + * A byte source built from one that may hand back text. + * + * Node-shaped standard input yields strings when an encoding was set on it, and + * the decoder downstream reads bytes; converting here keeps that difference out + * of the shared input path. Written with promise combinators rather than `async + * function*`, which this repository does not use. + */ +export function decodedChunks( + source: AsyncIterable, +): AsyncIterable { + const encoder = new TextEncoder(); + return { + [Symbol.asyncIterator](): AsyncIterator { + const iterator = source[Symbol.asyncIterator](); + const close = iterator.return; + return { + next(): Promise> { + return iterator.next().then((result) => { + if (result.done === true) { + return { done: true, value: undefined }; + } + const chunk = result.value; + return { + done: false, + value: typeof chunk === "string" ? encoder.encode(chunk) : chunk, + }; + }); + }, + ...(close === undefined + ? {} + : { + return(): Promise> { + return close.call(iterator).then(() => ({ done: true, value: undefined })); + }, + }), + }; + }, + }; +} diff --git a/packages/cli/src/repl/terminal.ts b/packages/cli/src/repl/terminal.ts new file mode 100644 index 00000000..276c9e23 --- /dev/null +++ b/packages/cli/src/repl/terminal.ts @@ -0,0 +1,83 @@ +/** + * What the REPL needs from a terminal, and who restores it. + * + * Shared code never asks which runtime it is on. It asks this Api, and a + * runtime-named adapter installs the answers: writing bytes, reading them, + * reporting the size, being told when the size changes, and turning raw mode on + * and off. Everything else — modes, the reader, the reset sequence — is + * ordinary portable code built on top of those five things. + * + * ## One unsuspending write, and why + * + * Restoring a terminal has to happen in order, and the last step of it happens + * while the process is already unwinding. `writeNow` is that one step: the + * adapter hands the bytes to the terminal with no suspension point inside the + * call, so nothing else being torn down can interleave between deciding to + * reset and the reset landing. Every other write suspends and yields the + * interpreter like any other operation. + */ + +import { type Api, createApi } from "@effectionx/context-api"; +import type { Operation, Stream } from "effection"; + +/** How much terminal there is, in cells. */ +export interface ReplTerminalSize { + readonly columns: number; + readonly rows: number; +} + +/** No adapter installed this operation, so the REPL cannot perform it. */ +export class ReplTerminalError extends Error { + constructor(operation: string) { + super( + `the REPL has no terminal for ${operation}. A runtime adapter installs one with ` + + 'ReplTerminal.around({ ... }, { at: "min" }) before the terminal is opened.', + ); + this.name = "ReplTerminalError"; + } +} + +export interface ReplTerminalApi { + /** The size as it stands. */ + size(): Operation; + /** Write bytes, returning once the terminal has them. */ + write(bytes: Uint8Array): Operation; + /** + * Write bytes without suspending. + * + * The one ordered-teardown case: the final reset must not be interleaved with + * anything else unwinding, so the adapter completes it inside the call rather + * than at a suspension point. Nothing else uses it. + */ + writeNow(bytes: Uint8Array): void; + /** Turn raw mode on or off. */ + setRaw(raw: boolean): Operation; + /** Each chunk of input bytes, closing at end of input. */ + input(): Stream; + /** Each size the terminal becomes. A host event, never a decoded key. */ + resizes(): Stream; +} + +export const ReplTerminal: Api = createApi("ReplTerminal", { + // deno-lint-ignore require-yield + *size(): Operation { + throw new ReplTerminalError("reading its size"); + }, + // deno-lint-ignore require-yield + *write(_bytes: Uint8Array): Operation { + throw new ReplTerminalError("writing to it"); + }, + writeNow(_bytes: Uint8Array): void { + throw new ReplTerminalError("writing to it"); + }, + // deno-lint-ignore require-yield + *setRaw(_raw: boolean): Operation { + throw new ReplTerminalError("changing its modes"); + }, + input(): Stream { + throw new ReplTerminalError("reading from it"); + }, + resizes(): Stream { + throw new ReplTerminalError("watching its size"); + }, +}); diff --git a/packages/cli/tests/fixtures/repl/surface.ts b/packages/cli/tests/fixtures/repl/surface.ts new file mode 100644 index 00000000..8094cf84 --- /dev/null +++ b/packages/cli/tests/fixtures/repl/surface.ts @@ -0,0 +1,362 @@ +/** + * A semantic REPL screen, described through the composition factories. + * + * Enough of a screen to lay out and render: a Sessions list that can be empty, + * Entries, the selected entry's scopes, a transcript, a bindings/history + * inspection column, an optional drawer and a footer holding History and an + * input. Every one of them is an ordinary described node, so what is under test + * is placement and rendering rather than a screen this fixture invented. + * + * The region a node belongs to is decided **here**, from its key, and the text + * it shows comes from the committed frame. Presentation therefore reads the + * tree; it never tells the tree anything, which is what keeps a resize from + * touching identity. + */ + +import { component, describe as describeNode, fields } from "../../../src/repl/description.ts"; +import type { + ReplComponent, + ReplDescription, + ReplInputEvent, + ReplNode, + ReplViewData, +} from "../../../src/repl/description.ts"; +import type { + ReplRegion, + ReplSurface, + ReplSurfaceCell, + ReplSurfaceMarker, +} from "../../../src/repl/layout.ts"; +import type { ReplTree } from "../../../src/repl/reconcile.ts"; + +/** Everything this screen's controls can ask the root to do. */ +export type Surfaced = + | { readonly kind: "select-session"; readonly key: string } + | { readonly kind: "select-entry"; readonly key: string } + | { readonly kind: "select-scope"; readonly key: string } + | { readonly kind: "select-marker"; readonly key: string } + | { readonly kind: "close-drawer" } + | { readonly kind: "submit" } + /** Text the person typed or pasted, for the draft to append. */ + | { readonly kind: "insert"; readonly text: string } + | { readonly kind: "erase" }; + +/** The one string field every fixture component is given. */ +function text(input: ReplViewData): string { + const value = fields(input)?.["text"]; + if (typeof value !== "string") { + throw new Error("a fixture node is given a { text } input"); + } + return value; +} + +/** + * A selectable row. + * + * Its action comes from its key, which does not change when its text does — so + * redescribing a row for a narrower screen cannot change what activating it + * means. + */ +/** The actions a selectable row can produce. */ +type Selecting = "select-session" | "select-entry" | "select-scope" | "select-marker"; + +/** One selection action, spelled out per kind so nothing has to be asserted. */ +function selected(kind: Selecting, key: string): Surfaced { + switch (kind) { + case "select-session": + return { kind: "select-session", key }; + case "select-entry": + return { kind: "select-entry", key }; + case "select-scope": + return { kind: "select-scope", key }; + case "select-marker": + return { kind: "select-marker", key }; + } +} + +function row(kind: Selecting | "close-drawer"): ReplComponent { + return component({ + name: `row:${kind}`, + attach(node: ReplNode): void { + node.focusable(); + node.render(text(node.input)); + node.onInput((input: ReplViewData) => node.render(text(input))); + node.claim((event: ReplInputEvent): Surfaced | undefined => { + // A row is not an editor: text passes it by on the way to whatever is. + if (event.kind === "text") { + return undefined; + } + if (event.kind !== "pointer" && event.key !== "Enter") { + return undefined; + } + if (kind === "close-drawer") { + return { kind: "close-drawer" }; + } + return selected(kind, node.key); + }); + }, + }); +} + +/** A line of text nobody can select. */ +const LINE: ReplComponent = component({ + name: "line", + attach(node: ReplNode): void { + node.render(text(node.input)); + node.onInput((input: ReplViewData) => node.render(text(input))); + }, +}); + +/** A titled container. */ +const SECTION: ReplComponent = component({ + name: "section", + attach(node: ReplNode): void { + node.render(text(node.input)); + node.onInput((input: ReplViewData) => node.render(text(input))); + }, +}); + +/** A drawer: a modal branch that closes on Escape. */ +const DRAWER: ReplComponent = component({ + name: "drawer", + attach(node: ReplNode): void { + node.render(text(node.input)); + node.onInput((input: ReplViewData) => node.render(text(input))); + node.claim((event: ReplInputEvent): Surfaced | undefined => + event.kind === "key" && event.key === "Escape" ? { kind: "close-drawer" } : undefined, + ); + }, +}); + +/** + * The draft line, which is where typing goes. + * + * It claims text and Backspace as well as Enter, and answers each with its own + * semantic action. The host said what arrived; what it means to a draft — append, + * erase, submit — is this component's to decide, which is why the same Enter that + * activates a row submits here. + */ +const DRAFT: ReplComponent = component({ + name: "draft", + attach(node: ReplNode): void { + node.focusable(); + node.render(text(node.input)); + node.onInput((input: ReplViewData) => node.render(text(input))); + node.claim((event: ReplInputEvent): Surfaced | undefined => { + if (event.kind === "text") { + return { kind: "insert", text: event.text }; + } + if (event.kind === "pointer") { + return { kind: "submit" }; + } + if (event.key === "Enter") { + return { kind: "submit" }; + } + if (event.key === "Backspace") { + return { kind: "erase" }; + } + return undefined; + }); + }, +}); + +const SESSION_ROW = row("select-session"); +const ENTRY_ROW = row("select-entry"); +const SCOPE_ROW = row("select-scope"); +const MARKER_ROW = row("select-marker"); +const CLOSE_ROW = row("close-drawer"); + +/** + * Which surface a narrow screen is showing. + * + * A route, not a preference: the application decides it, and placement is told + * rather than asked. + */ +export type ReplFixtureRoute = "sessions" | "entries" | "transcript" | "inspection"; + +/** What the screen is showing. */ +export interface ReplFixtureState { + /** The surface a narrow frame routes to. */ + readonly route: ReplFixtureRoute; + readonly sessions: readonly string[]; + readonly entries: readonly string[]; + readonly scopes: readonly string[]; + readonly transcript: readonly string[]; + readonly bindings: readonly { readonly name: string; readonly value: string }[]; + readonly history: readonly ReplSurfaceMarker[]; + /** The open drawer's title, or none. */ + readonly drawer: string | undefined; + readonly draft: string; +} + +/** A state with nothing in it yet, which is a screen the REPL really shows. */ +export const EMPTY: ReplFixtureState = { + route: "sessions", + sessions: [], + entries: [], + scopes: [], + transcript: [], + bindings: [], + history: [], + drawer: undefined, + draft: "", +}; + +function leaf( + kind: ReplComponent, + key: string, + body: string, + options: { + readonly modal?: true; + readonly focus?: true; + readonly children?: readonly ReplDescription[]; + } = {}, +): ReplDescription { + return describeNode({ + key, + component: kind, + input: { text: body }, + ...(options.children === undefined ? {} : { children: options.children }), + ...(options.modal === undefined ? {} : { modal: options.modal }), + ...(options.focus === undefined ? {} : { focus: options.focus }), + }); +} + +/** Describe the whole screen. One flat set: placement is not nesting. */ +export function fixtureDescriptions(state: ReplFixtureState): readonly ReplDescription[] { + const described: ReplDescription[] = []; + + described.push( + state.sessions.length === 0 + ? // An empty list is a thing the screen says, not a thing it omits. + leaf(SECTION, "sessions:empty", "Sessions: none yet") + : leaf(SECTION, "sessions:heading", "Sessions"), + ); + for (const session of state.sessions) { + described.push(leaf(SESSION_ROW, `session:${session}`, session)); + } + described.push(leaf(SECTION, "entries:heading", "Entries")); + for (const entry of state.entries) { + described.push(leaf(ENTRY_ROW, `entry:${entry}`, entry)); + } + for (const scope of state.scopes) { + described.push(leaf(SCOPE_ROW, `scope:${scope}`, scope)); + } + for (const [index, line] of state.transcript.entries()) { + described.push(leaf(LINE, `line:${index}`, line)); + } + for (const binding of state.bindings) { + described.push(leaf(LINE, `binding:${binding.name}`, `${binding.name} = ${binding.value}`)); + } + for (const marker of state.history) { + described.push(leaf(MARKER_ROW, `marker:${marker.marker}`, marker.label)); + } + // Focus follows the innermost modal. An open drawer holds it, because the + // composition kernel refuses a focus claim its open modal does not contain — + // which is the same reason the drawer's own control is its child rather than + // its sibling. + described.push( + state.drawer === undefined + ? leaf(DRAFT, "footer:input", `> ${state.draft}`, { focus: true }) + : leaf(DRAFT, "footer:input", `> ${state.draft}`), + ); + if (state.drawer !== undefined) { + described.push( + leaf(DRAWER, "drawer:open", state.drawer, { + modal: true, + children: [leaf(CLOSE_ROW, "drawer:close", "Close", { focus: true })], + }), + ); + } + return described; +} + +/** Which region a key belongs to, and whether a pointer may activate it. */ +function regionOf( + key: string, +): { readonly region: ReplRegion; readonly targetable: boolean } | undefined { + if (key.startsWith("sessions:") || key.startsWith("entries:")) { + return { region: "sidebar", targetable: false }; + } + if (key.startsWith("session:") || key.startsWith("entry:") || key.startsWith("scope:")) { + return { region: "sidebar", targetable: true }; + } + if (key.startsWith("line:")) { + return { region: "transcript", targetable: false }; + } + if (key.startsWith("binding:")) { + return { region: "inspection", targetable: false }; + } + if (key.startsWith("marker:")) { + return { region: "footer", targetable: true }; + } + if (key.startsWith("footer:")) { + return { region: "footer", targetable: true }; + } + if (key.startsWith("drawer:")) { + return { region: "drawer", targetable: true }; + } + return undefined; +} + +/** + * The surface a committed tree presents. + * + * Read from the frame, so a node the tree has removed contributes nothing and a + * cell nobody rendered is not in it. The live node id travels with every cell, + * because that id is what a pointer resolved against the drawn frame has to name. + */ +export function fixtureSurface(tree: ReplTree, state: ReplFixtureState): ReplSurface { + const collected = new Map([ + ["sidebar", []], + ["transcript", []], + ["inspection", []], + ["drawer", []], + ["footer", []], + ]); + + for (const cell of tree.frame().cells) { + const key = tree.keyOf(cell.node); + if (key === undefined) { + continue; + } + const placed = regionOf(key); + if (placed === undefined) { + continue; + } + collected.get(placed.region)?.push({ + node: cell.node, + text: cell.cell, + ...(placed.targetable ? { targetable: true } : {}), + }); + } + + const sidebar = collected.get("sidebar") ?? []; + const sessions = sidebar.filter((cell) => { + const key = tree.keyOf(cell.node); + return key !== undefined && (key.startsWith("sessions:") || key.startsWith("session:")); + }); + const entries = sidebar.filter((cell) => !sessions.includes(cell)); + const transcript = collected.get("transcript") ?? []; + const inspection = collected.get("inspection") ?? []; + + const routed: { readonly [route in ReplFixtureRoute]: readonly ReplSurfaceCell[] } = { + sessions, + entries, + transcript, + inspection, + }; + + return { + // The route picks one. Everything else stays mounted and stays off the + // narrow frame, which is what makes it unreachable there rather than hidden. + content: routed[state.route], + sessions, + entries, + transcript, + inspection, + drawer: collected.get("drawer") ?? [], + footer: collected.get("footer") ?? [], + history: state.history, + }; +} diff --git a/packages/cli/tests/repl-composition.test.ts b/packages/cli/tests/repl-composition.test.ts index 56e7d693..bba6ccec 100644 --- a/packages/cli/tests/repl-composition.test.ts +++ b/packages/cli/tests/repl-composition.test.ts @@ -93,7 +93,7 @@ function panel(journal: Journal): ReplComponent { node.focusable(); node.render(`[${label(node.input)}]`); node.claim((event: ReplInputEvent): Act | undefined => - event.kind === "pointer" || event.key === "Enter" + event.kind === "pointer" || (event.kind === "key" && event.key === "Enter") ? { kind: "activate", from: node.key } : undefined, ); @@ -144,7 +144,7 @@ function group(journal: Journal): ReplComponent { journal.push(`mount:${node.key}`); node.render(`{${label(node.input)}}`); node.claim((event: ReplInputEvent): Act | undefined => - event.kind === "pointer" || event.key === "Enter" + event.kind === "pointer" || (event.kind === "key" && event.key === "Enter") ? { kind: "submit", from: node.key } : undefined, ); @@ -161,7 +161,7 @@ function drawer(journal: Journal): ReplComponent { journal.push(`mount:${node.key}`); node.render(`|${label(node.input)}|`); node.claim((event: ReplInputEvent): Act | undefined => - event.kind === "pointer" || event.key === "Escape" + event.kind === "pointer" || (event.kind === "key" && event.key === "Escape") ? { kind: "close-drawer", from: node.key } : undefined, ); diff --git a/packages/cli/tests/repl-terminal.test.ts b/packages/cli/tests/repl-terminal.test.ts new file mode 100644 index 00000000..458cf4c5 --- /dev/null +++ b/packages/cli/tests/repl-terminal.test.ts @@ -0,0 +1,1550 @@ +/** + * The terminal platform (#848 F1, H1, H2). + * + * One real Freedom tree, one real layout engine, and a terminal the test owns + * completely. What is under test is the platform Slice E sits on: that a + * described screen lays out the same way every time at four sizes, that what + * was rendered is what a pointer resolves against, that presentation time has + * one owner, and that every way out of the terminal gives it back. + * + * The terminal is injected through the contextual Api, so nothing here reads + * `Deno`, `process` or a platform name, and the same corpus runs under Deno, + * Node and Bun. The runtime adapters are constructed but never installed — that + * is Slice E's turn. + */ + +import { describe, it } from "@executablemd/test-support/bdd"; +import { expect } from "@executablemd/test-support/expect"; +import { type Operation, scoped, sleep, spawn, suspend, withResolvers } from "effection"; + +import { HISTORY_ROWS, layout, NARROW, profileFor } from "../src/repl/layout.ts"; +import type { ReplBounds, ReplRegion, ReplSemanticFrame, ReplSurface } from "../src/repl/layout.ts"; +import { resolvePointer, snapshotRender, useReplRenderer } from "../src/repl/renderer.ts"; +import type { ReplRendered, ReplRenderSnapshot } from "../src/repl/renderer.ts"; +import { nearestCommonAncestor, ReplClock, useReplFrames } from "../src/repl/frame.ts"; +import type { ReplFrameSubscription } from "../src/repl/frame.ts"; +import { replModes, useReplScreen } from "../src/repl/screen.ts"; +import type { ReplScreenEvent } from "../src/repl/screen.ts"; +import { installReplTerminal } from "../src/repl/terminal-host.ts"; +import type { ReplTerminalCapabilities } from "../src/repl/terminal-host.ts"; +import type { ReplTerminalSize } from "../src/repl/terminal.ts"; +import { useReplTree } from "../src/repl/reconcile.ts"; +import type { ReplDispatched, ReplTree } from "../src/repl/reconcile.ts"; +import type { ReplInputEvent } from "../src/repl/description.ts"; +import { + EMPTY, + fixtureDescriptions, + fixtureSurface, + type ReplFixtureState, + type Surfaced, +} from "./fixtures/repl/surface.ts"; + +/** A screen with something on it, so placement has something to place. */ +const FILLED: ReplFixtureState = { + route: "entries", + sessions: ["kf39sla2"], + entries: ["entry-1"], + scopes: ["entry-1/component", "entry-1/generated"], + transcript: ["About to evaluate: the plan", "Decision: approve"], + bindings: [ + { name: "plan", value: "ship it" }, + { name: "response", value: "approve" }, + ], + history: [ + { marker: "yield:__root__:1", label: "root 1" }, + { marker: "close:__root__", label: "root close" }, + ], + drawer: undefined, + draft: "", +}; + +/** Let every task that is ready take its turn. */ +function* settled(): Operation { + yield* sleep(0); + yield* sleep(0); + yield* sleep(0); +} + +function bounds(frame: ReplSemanticFrame, region: ReplRegion): ReplBounds | undefined { + return frame.regions.find((placed) => placed.region === region)?.bounds; +} + +function regions(frame: ReplSemanticFrame): ReplRegion[] { + return frame.regions.map((placed) => placed.region); +} + +function textsIn(frame: ReplSemanticFrame, region: ReplRegion): string[] { + return frame.cells.filter((cell) => cell.region === region).map((cell) => cell.text); +} + +function committed(outcome: { ok: boolean; error?: Error }): void { + if (!outcome.ok) { + throw outcome.error; + } +} + +/** What one dispatch produced, or a failure naming what came back instead. */ +function acted(outcome: { ok: boolean; value?: unknown; error?: Error }): ReplDispatched { + if (!outcome.ok) { + throw outcome.error; + } + const value = outcome.value; + if (typeof value !== "object" || value === null || !("outcome" in value)) { + throw new Error("a dispatch answers with a closed outcome"); + } + const outcomeValue = value.outcome; + if (outcomeValue === "action" && "action" in value && "ancestry" in value) { + const action = value.action; + const ancestry = value.ancestry; + if (isSurfaced(action) && Array.isArray(ancestry)) { + return { outcome: "action", action, ancestry: ancestry.map(String) }; + } + } + if (outcomeValue === "focus" && "focused" in value) { + const focused = value.focused; + return { outcome: "focus", focused: typeof focused === "string" ? focused : undefined }; + } + if (outcomeValue === "dropped" && "reason" in value && typeof value.reason === "string") { + return { outcome: "dropped", reason: value.reason }; + } + throw new Error( + `a dispatch answered with something this test does not recognize: ${outcomeValue}`, + ); +} + +/** Whether a value is one of the fixture's actions, decided by parsing it. */ +function isSurfaced(value: unknown): value is Surfaced { + if (typeof value !== "object" || value === null || !("kind" in value)) { + return false; + } + const kind = value.kind; + return ( + kind === "select-session" || + kind === "select-entry" || + kind === "select-scope" || + kind === "select-marker" || + kind === "close-drawer" || + kind === "submit" || + kind === "insert" || + kind === "erase" + ); +} + +/** Mount one fixture state and hand back the tree and its surface. */ +function* mounted( + state: ReplFixtureState, +): Operation<{ tree: ReplTree; surface: ReplSurface }> { + const tree = yield* useReplTree(); + committed(yield* tree.apply(fixtureDescriptions(state))); + return { tree, surface: fixtureSurface(tree, state) }; +} + +/** A render input for one laid-out frame and one mounted tree. */ +function input(frame: ReplSemanticFrame, tree: ReplTree): ReplRenderSnapshot { + return snapshotRender({ + frame, + tree: tree.frame().id, + mounted: tree.mounted(), + deltaTime: 0, + pointer: undefined, + }); +} + +/** Render one frame, failing the test if the renderer refused it. */ +function* drawn( + renderer: { + render( + snapshot: ReplRenderSnapshot, + ): Operation<{ ok: boolean; value?: ReplRendered; error?: Error }>; + }, + snapshot: ReplRenderSnapshot, +): Operation { + const outcome = yield* renderer.render(snapshot); + if (!outcome.ok || outcome.value === undefined) { + throw outcome.error ?? new Error("the renderer refused a frame this test expects it to draw"); + } + return outcome.value; +} + +describe("REPL terminal: responsive semantic frames", () => { + it("F1: a wide frame carries the sidebar, transcript, inspection and fixed footer", function* () { + const { tree, surface } = yield* mounted(FILLED); + const frame = layout({ columns: 160, rows: 36 }, surface); + + expect(frame.profile).toBe("wide"); + expect(regions(frame)).toEqual(["sidebar", "transcript", "inspection", "footer"]); + expect(bounds(frame, "sidebar")).toEqual({ x: 0, y: 0, width: 32, height: 29 }); + expect(bounds(frame, "transcript")).toEqual({ x: 32, y: 0, width: 92, height: 29 }); + expect(bounds(frame, "inspection")).toEqual({ x: 124, y: 0, width: 36, height: 29 }); + // Full width and pinned to the bottom, at every size. + expect(bounds(frame, "footer")).toEqual({ x: 0, y: 29, width: 160, height: 7 }); + expect(frame.historyRows).toHaveLength(HISTORY_ROWS); + expect(frame.refusal).toBeUndefined(); + + expect(textsIn(frame, "sidebar")).toEqual([ + "Sessions", + "kf39sla2", + "Entries", + "entry-1", + "entry-1/component", + "entry-1/generated", + ]); + expect(textsIn(frame, "transcript")).toEqual([ + "About to evaluate: the plan", + "Decision: approve", + ]); + expect(textsIn(frame, "inspection")).toEqual(["plan = ship it", "response = approve"]); + expect(textsIn(frame, "footer")).toEqual(["root 1", "root close", "> "]); + expect(tree.mounted()).toHaveLength(frame.cells.length); + }); + + it("F1: a medium frame keeps every region, narrower", function* () { + const { surface } = yield* mounted(FILLED); + const frame = layout({ columns: 120, rows: 30 }, surface); + + expect(frame.profile).toBe("medium"); + expect(regions(frame)).toEqual(["sidebar", "transcript", "inspection", "footer"]); + expect(bounds(frame, "sidebar")).toEqual({ x: 0, y: 0, width: 28, height: 23 }); + expect(bounds(frame, "transcript")).toEqual({ x: 28, y: 0, width: 64, height: 23 }); + expect(bounds(frame, "inspection")).toEqual({ x: 92, y: 0, width: 28, height: 23 }); + expect(bounds(frame, "footer")).toEqual({ x: 0, y: 23, width: 120, height: 7 }); + expect(frame.historyRows).toHaveLength(HISTORY_ROWS); + }); + + it("F1: a narrow frame routes one content surface and keeps the drawer and footer", function* () { + const { surface } = yield* mounted({ ...FILLED, drawer: "Binding: plan" }); + const frame = layout(NARROW, surface); + + expect(frame.profile).toBe("narrow"); + expect(regions(frame)).toEqual(["content", "drawer", "footer"]); + expect(bounds(frame, "content")).toEqual({ x: 0, y: 0, width: 72, height: 13 }); + // The same footer contract as the wide frame: full width, five History rows. + expect(bounds(frame, "footer")).toEqual({ x: 0, y: 13, width: 72, height: 7 }); + expect(frame.historyRows).toHaveLength(HISTORY_ROWS); + expect(bounds(frame, "drawer")).toEqual({ x: 9, y: 1, width: 54, height: 11 }); + expect(textsIn(frame, "drawer")).toEqual(["Binding: plan", "Close"]); + // One surface, the routed one, and nothing from the others. + expect(textsIn(frame, "content")).toEqual([ + "Entries", + "entry-1", + "entry-1/component", + "entry-1/generated", + ]); + expect(textsIn(frame, "content")).not.toContain("kf39sla2"); + expect(textsIn(frame, "content")).not.toContain("Decision: approve"); + }); + + it("F1: narrow shows the routed surface, and no other surface is reachable", function* () { + const routes = [ + { route: "sessions", shown: ["Sessions", "kf39sla2"], hidden: "entry-1" }, + { + route: "transcript", + shown: ["About to evaluate: the plan", "Decision: approve"], + hidden: "kf39sla2", + }, + ] as const; + + for (const { route, shown, hidden } of routes) { + const state: ReplFixtureState = { ...FILLED, route }; + const { tree, surface } = yield* mounted(state); + const frame = layout(NARROW, surface); + const renderer = yield* useReplRenderer(NARROW); + const rendered = yield* drawn(renderer, input(frame, tree)); + + expect(textsIn(frame, "content")).toEqual(shown); + + // The other surfaces are still mounted — routing is presentation — and + // that is exactly why the proof has to be that they are not in the frame + // and not in its map. + const inactive = tree.mounted().filter((id) => { + const key = tree.keyOf(id); + return key !== undefined && key.endsWith(hidden); + }); + expect(inactive.length).toBeGreaterThan(0); + for (const node of inactive) { + expect(frame.cells.map((cell) => cell.node)).not.toContain(node); + expect(rendered.map.targets.map((target) => target.node)).not.toContain(node); + expect(rendered.map.nodeOf(`content:${node}`)).toBeUndefined(); + } + + // And nowhere a pointer can land reaches one of them. + for (let row = 0; row < NARROW.rows; row += 1) { + for (let column = 0; column < NARROW.columns; column += 8) { + const pointer = resolvePointer(rendered, { column, row }); + if (pointer !== undefined) { + expect(inactive).not.toContain(pointer.target); + } + } + } + } + }); + + it("F1: below the minimum the frame is an explicit refusal with nothing targetable", function* () { + const { surface } = yield* mounted(FILLED); + + for (const size of [ + { columns: 71, rows: 20 }, + { columns: 72, rows: 19 }, + { columns: 40, rows: 10 }, + ]) { + const frame = layout(size, surface); + expect(profileFor(size)).toBe("too-small"); + expect(frame.profile).toBe("too-small"); + expect(frame.refusal).toContain("at least 72x20"); + expect(frame.refusal).toContain(`${size.columns}x${size.rows}`); + expect(regions(frame)).toEqual(["refusal"]); + // A control that is not in the frame cannot be in its target map either. + expect(frame.cells).toEqual([]); + expect(frame.historyRows).toHaveLength(HISTORY_ROWS); + } + }); + + it("F1: Sessions says so when there is nothing in it", function* () { + const { surface } = yield* mounted(EMPTY); + const frame = layout({ columns: 160, rows: 36 }, surface); + + expect(textsIn(frame, "sidebar")).toEqual(["Sessions: none yet", "Entries"]); + expect(textsIn(frame, "transcript")).toEqual([]); + expect(frame.refusal).toBeUndefined(); + }); + + it("F1: compact geometry groups marker labels and keeps every marker's identity", function* () { + const history = Array.from({ length: 40 }, (_unused, index) => ({ + marker: `yield:entry-1:${index}`, + label: `entry-1 yield ${index}`, + })); + const { surface } = yield* mounted({ ...FILLED, history }); + + const wide = layout({ columns: 160, rows: 36 }, surface); + const narrow = layout(NARROW, surface); + + // Every marker survives at both sizes, once each, under its own name. + for (const frame of [wide, narrow]) { + expect(frame.markers.map((marker) => marker.marker)).toEqual( + history.map((one) => one.marker), + ); + expect(frame.historyRows).toHaveLength(HISTORY_ROWS); + } + // Wide has room for its own labels; narrow shares them. + expect(wide.markers.every((marker) => marker.grouped.length === 0)).toBe(true); + expect(narrow.markers.some((marker) => marker.grouped.length > 0)).toBe(true); + const shared = narrow.markers.filter((marker) => marker.label === narrow.markers[0].label); + expect(shared.length).toBeGreaterThan(1); + // A grouped marker names the others it shares a label with, so the identity + // of a compact position is still recoverable. + expect(narrow.markers[0].grouped).toEqual(shared.slice(1).map((marker) => marker.marker)); + expect(narrow.markers[0].label).toContain("+"); + }); + + it("F1: resizing through every profile keeps selection, nodes and action identity", function* () { + const { tree, surface } = yield* mounted(FILLED); + const before = tree.mounted(); + const focused = tree.focused(); + const frameId = tree.frame().id; + + const sizes = [ + { columns: 160, rows: 36 }, + { columns: 120, rows: 30 }, + NARROW, + { columns: 40, rows: 10 }, + ]; + const laid = sizes.map((size) => layout(size, surface)); + + expect(laid.map((frame) => frame.profile)).toEqual(["wide", "medium", "narrow", "too-small"]); + // Laying out is presentation: it reads the tree and tells it nothing. + expect(tree.mounted()).toEqual(before); + expect(tree.focused()).toBe(focused); + expect(tree.frame().id).toBe(frameId); + + // And the same control still means the same thing. + for (const _frame of laid) { + const outcome = acted(yield* tree.dispatch({ kind: "key", key: "Enter" })); + expect(outcome.outcome).toBe("action"); + if (outcome.outcome === "action") { + expect(outcome.action).toEqual({ kind: "submit" }); + } + } + }); + + it("F1: a pointer against a control the refusal hides reaches nothing", function* () { + const { tree, surface } = yield* mounted(FILLED); + const renderer = yield* useReplRenderer(NARROW); + + const visible = yield* drawn(renderer, input(layout(NARROW, surface), tree)); + const footer = visible.map.targets.find((target) => tree.keyOf(target.node) === "footer:input"); + expect(footer).toBeDefined(); + if (footer === undefined) { + throw new Error("the narrow frame draws the footer input"); + } + // The same place, now under a refusal. + const refused = yield* drawn(renderer, input(layout({ columns: 40, rows: 10 }, surface), tree)); + expect(refused.map.targets).toEqual([]); + expect( + resolvePointer(refused, { column: footer.bounds.x, row: footer.bounds.y }), + ).toBeUndefined(); + }); +}); + +/** A clock the test moves, so "no timer" is something it can observe. */ +interface TestClock { + /** How many waits were ever scheduled. */ + waits: number; + now: number; + install(): Operation; + /** Advance and release every wait outstanding right now. */ + tick(seconds: number): Operation; +} + +function controllableClock(): TestClock { + let pending: (() => void)[] = []; + const clock: TestClock = { + waits: 0, + now: 0, + install(): Operation { + return ReplClock.around( + { + // deno-lint-ignore require-yield + *now(): Operation { + return clock.now; + }, + *wait(): Operation { + clock.waits += 1; + const waiter = withResolvers(); + pending.push(() => waiter.resolve()); + yield* waiter.operation; + }, + }, + { at: "min" }, + ); + }, + *tick(seconds: number): Operation { + clock.now += seconds; + const releasing = pending; + pending = []; + for (const release of releasing) { + release(); + } + yield* settled(); + }, + }; + return clock; +} + +describe("REPL terminal: frame and renderer ownership", () => { + it("H1: a frame advances only once every subscriber has applied the last one", function* () { + const clock = controllableClock(); + yield* clock.install(); + const frames = yield* useReplFrames(); + + // One ancestor, two participants beneath it: one animation, one clock. + const first = yield* frames.subscribe({ + owner: "page", + participants: [ + ["left", "page", "root"], + ["right", "page", "root"], + ], + }); + const second = yield* frames.subscribe({ owner: "footer", participants: [["footer"]] }); + if (!first.ok) { + throw first.error; + } + if (!second.ok) { + throw second.error; + } + yield* settled(); + + expect(frames.demanded()).toBe(true); + expect(frames.published()).toBe(0); + expect(clock.waits).toBe(1); + + yield* clock.tick(1 / 60); + expect(frames.published()).toBe(1); + expect(frames.outstanding()).toBe(2); + // Nothing further is scheduled while anybody still owes this frame. + expect(clock.waits).toBe(1); + + const received = yield* first.value.next(); + expect(received.id).toBe(1); + expect(received.delta).toBe(0); + yield* settled(); + // Received, not applied: drawing with a timestamp suspends, and the stream + // has to wait for the drawing rather than for the handover. + expect(first.value.received()).toBe(1); + expect(first.value.applied()).toBe(0); + expect(frames.outstanding()).toBe(2); + expect(clock.waits).toBe(1); + + first.value.acknowledge(); + yield* settled(); + expect(first.value.applied()).toBe(1); + expect(frames.outstanding()).toBe(1); + expect(clock.waits).toBe(1); + + yield* second.value.next(); + second.value.acknowledge(); + yield* settled(); + expect(frames.outstanding()).toBe(0); + // Now, and only now. + expect(clock.waits).toBe(2); + + yield* clock.tick(1 / 30); + expect(frames.published()).toBe(2); + const later = yield* first.value.next(); + expect(later.id).toBe(2); + // Seconds, because that is what the layout engine's transitions take. + expect(later.delta).toBeCloseTo(1 / 30, 6); + }); + + it("H1: a subscriber holding a frame holds the whole stream until it applies it", function* () { + const clock = controllableClock(); + yield* clock.install(); + const frames = yield* useReplFrames(); + + const holding = yield* frames.subscribe({ owner: "page", participants: [["page"]] }); + const prompt = yield* frames.subscribe({ owner: "footer", participants: [["footer"]] }); + if (!holding.ok) { + throw holding.error; + } + if (!prompt.ok) { + throw prompt.error; + } + yield* settled(); + yield* clock.tick(1 / 60); + + // One receives its frame and holds it, as a subscriber does while it is + // laying out and drawing. + const held = yield* holding.value.next(); + expect(held.id).toBe(1); + // The other applies and says so. + yield* prompt.value.next(); + prompt.value.acknowledge(); + yield* settled(); + + // Still one frame, still one timer: the holder's work has not finished. + expect(frames.published()).toBe(1); + expect(frames.outstanding()).toBe(1); + expect(clock.waits).toBe(1); + // Releasing the clock changes nothing while the frame is unapplied. + yield* clock.tick(1 / 60); + expect(frames.published()).toBe(1); + expect(clock.waits).toBe(1); + + holding.value.acknowledge(); + yield* settled(); + expect(frames.outstanding()).toBe(0); + expect(clock.waits).toBe(2); + }); + + it("H1: cancelling a holder drops its demand without claiming it applied anything", function* () { + const clock = controllableClock(); + yield* clock.install(); + const frames = yield* useReplFrames(); + + const prompt = yield* frames.subscribe({ owner: "footer", participants: [["footer"]] }); + if (!prompt.ok) { + throw prompt.error; + } + + // The handle is read *after* the cancellation has fully unwound, not from + // inside a destructor: destructors run in reverse, so a reading registered + // by the holder would run before the subscription's own teardown and would + // miss anything that teardown did. + let handle: ReplFrameSubscription | undefined; + const holder = yield* spawn(function* (): Operation { + const held = yield* frames.subscribe({ owner: "page", participants: [["page"]] }); + if (!held.ok) { + throw held.error; + } + handle = held.value; + // Received, and then cancelled part-way through applying it. + yield* held.value.next(); + yield* suspend(); + }); + yield* settled(); + yield* clock.tick(1 / 60); + yield* settled(); + + yield* prompt.value.next(); + prompt.value.acknowledge(); + yield* settled(); + expect(frames.outstanding()).toBe(1); + expect(clock.waits).toBe(1); + + yield* holder.halt(); + yield* settled(); + + // Its demand is gone, so the stream moves; its acknowledgement never + // arrived, because it never finished the frame. + expect(handle?.received()).toBe(1); + expect(handle?.applied()).toBe(0); + expect(frames.outstanding()).toBe(0); + expect(clock.waits).toBe(2); + // The frame it was holding is not counted as drawn anywhere. + expect(frames.published()).toBe(1); + }); + + it("H1: no demand outlives a subscription, at any turn of its life", function* () { + // The same walk, for the other resource this slice publishes: whichever turn + // puts the subscriber in the demand set, the removal for it already exists. + for (let turns = 0; turns <= 6; turns += 1) { + const clock = controllableClock(); + yield* scoped(function* (): Operation { + yield* clock.install(); + const frames = yield* useReplFrames(); + + const task = yield* spawn(function* (): Operation { + const held = yield* frames.subscribe({ owner: "page", participants: [["page"]] }); + if (!held.ok) { + throw held.error; + } + yield* suspend(); + }); + for (let turn = 0; turn < turns; turn += 1) { + yield* sleep(0); + } + yield* task.halt(); + yield* settled(); + + expect(frames.demanded()).toBe(false); + expect(frames.outstanding()).toBe(0); + }); + } + }); + + it("H1: a settled tree schedules no timer at all", function* () { + const clock = controllableClock(); + yield* clock.install(); + const frames = yield* useReplFrames(); + + yield* settled(); + expect(frames.demanded()).toBe(false); + expect(clock.waits).toBe(0); + + const holder = yield* spawn(function* (): Operation { + const held = yield* frames.subscribe({ owner: "page", participants: [["page"]] }); + if (!held.ok) { + throw held.error; + } + yield* suspend(); + }); + yield* settled(); + expect(clock.waits).toBe(1); + + yield* holder.halt(); + yield* settled(); + expect(frames.demanded()).toBe(false); + yield* clock.tick(1); + yield* settled(); + // The released wait published nothing and scheduled nothing after it. + expect(frames.published()).toBe(0); + expect(clock.waits).toBe(1); + }); + + it("H1: a cross-component animation belongs to exactly its nearest common ancestor", function* () { + const clock = controllableClock(); + yield* clock.install(); + const frames = yield* useReplFrames(); + const participants = [ + ["left", "page", "root"], + ["right", "page", "root"], + ]; + + expect(nearestCommonAncestor(participants)).toBe("page"); + + const owned = yield* frames.subscribe({ owner: "page", participants }); + expect(owned.ok).toBe(true); + + // A participant cannot own it: its own lifetime is shorter than the + // animation's. + const byParticipant = yield* frames.subscribe({ owner: "left", participants }); + expect(byParticipant.ok).toBe(false); + // Nor can a grandparent: it would hold the clock for a subtree that stopped. + const byAncestor = yield* frames.subscribe({ owner: "root", participants }); + expect(byAncestor.ok).toBe(false); + if (byAncestor.ok) { + throw new Error("a distant ancestor must not own this animation"); + } + expect(byAncestor.error.name).toBe("ReplFrameOwnerError"); + expect(byAncestor.error.message).toContain("page"); + + yield* settled(); + // The two refusals registered no demand, so there is exactly one clock. + expect(clock.waits).toBe(1); + yield* clock.tick(1 / 60); + expect(frames.outstanding()).toBe(1); + }); + + it("H1: rendered bytes are copied out before the engine reuses its view", function* () { + const { tree, surface } = yield* mounted(FILLED); + const renderer = yield* useReplRenderer(NARROW); + + const narrow = yield* drawn(renderer, input(layout(NARROW, surface), tree)); + const held = narrow.output; + const witnessed = Array.from(held); + expect(witnessed.length).toBeGreaterThan(0); + + // A second render writes over the engine's output region, and a resize grows + // its memory, which detaches every view into the old buffer. + yield* drawn(renderer, input(layout({ columns: 120, rows: 30 }, surface), tree)); + renderer.resize({ columns: 160, rows: 36 }); + yield* drawn(renderer, input(layout({ columns: 160, rows: 36 }, surface), tree)); + + expect(Array.from(held)).toEqual(witnessed); + }); + + it("H1: what a caller does to bytes it holds cannot change a later render", function* () { + const { tree, surface } = yield* mounted(FILLED); + // The same three frames through two renderers. One caller scribbles over + // every result it is handed; the other leaves them alone. A renderer whose + // result aliased the engine's memory would have the scribbling corrupt the + // state the next frame is diffed against, and the two sequences would part. + const sequence = [ + layout(NARROW, surface), + layout({ columns: 120, rows: 30 }, surface), + layout(NARROW, surface), + ]; + + const tampered: number[][] = []; + const scribbling = yield* useReplRenderer(NARROW); + for (const frame of sequence) { + const rendered = yield* drawn(scribbling, input(frame, tree)); + tampered.push(Array.from(rendered.output)); + rendered.output.fill(0); + } + + const clean: number[][] = []; + const untouched = yield* useReplRenderer(NARROW); + for (const frame of sequence) { + const rendered = yield* drawn(untouched, input(frame, tree)); + clean.push(Array.from(rendered.output)); + } + + expect(tampered[0].length).toBeGreaterThan(0); + expect(tampered).toEqual(clean); + }); + + it("H1: a frame laid out before a removal draws only what is still mounted", function* () { + const { tree, surface } = yield* mounted(FILLED); + const renderer = yield* useReplRenderer({ columns: 160, rows: 36 }); + // Laid out while the row was there. + const frame = layout({ columns: 160, rows: 36 }, surface); + const going = tree.mounted().find((id) => tree.keyOf(id) === "scope:entry-1/generated"); + if (going === undefined) { + throw new Error("the generated scope row is mounted before the removal"); + } + expect(frame.cells.map((cell) => cell.node)).toContain(going); + + // And the tree moved on before anything was drawn. + committed(yield* tree.apply(fixtureDescriptions({ ...FILLED, scopes: ["entry-1/component"] }))); + expect(tree.mounted()).not.toContain(going); + + const rendered = yield* drawn(renderer, input(frame, tree)); + + // The mounted tree is the only source of renderable nodes: a cell naming a + // node that has gone is not drawn, and is therefore not a target either. + expect(rendered.map.targets.map((target) => target.node)).not.toContain(going); + expect(rendered.map.nodeOf(`sidebar:${going}`)).toBeUndefined(); + expect(rendered.map.targets.length).toBeGreaterThan(0); + }); + + it("H1: capacity replacement redraws the identical logical frame", function* () { + // Deliberately more than any of the four profiles produces: the engine's + // measurement arenas are sized from the dimensions it was built for, and + // what this proves is that a renderer built when the terminal was tiny + // recovers rather than refusing the frame the terminal now needs. + const bindings = Array.from({ length: 1000 }, (_unused, index) => ({ + name: `binding-${index}`, + value: `a fairly long recorded value for binding number ${index}`, + })); + const { tree, surface } = yield* mounted({ ...FILLED, bindings }); + const frame = layout({ columns: 160, rows: 1010 }, surface); + expect(frame.cells.length).toBeGreaterThan(900); + + const renderer = yield* useReplRenderer({ columns: 20, rows: 5 }); + const snapshot = input(frame, tree); + const rendered = yield* drawn(renderer, snapshot); + + expect(rendered.recovered).toBe(true); + // The same logical frame: the same tree, the same targets, the same + // geometry. Recovery is invisible above this line. + expect(rendered.tree).toBe(tree.frame().id); + expect(rendered.map.targets.length).toBeGreaterThan(0); + expect(renderer.last()).toEqual(snapshot); + + const repeated = yield* drawn(renderer, snapshot); + expect(repeated.recovered).toBe(false); + expect(repeated.map.targets.map((target) => target.id)).toEqual( + rendered.map.targets.map((target) => target.id), + ); + expect(tree.focused()).toBe(tree.focused()); + expect(tree.mounted()).toHaveLength(frame.cells.length); + }); +}); + +/** Everything the fake terminal was asked to do, and what it still holds. */ +interface TerminalLog { + /** Every raw-mode change, in order. */ + readonly raw: boolean[]; + /** Suspending writes that completed. */ + writes: number; + /** Unsuspending writes, which is how the final reset is written. */ + resets: number; + /** Resize listeners still attached. */ + listeners: number; + /** Byte sources still open. */ + readers: number; + /** Byte sources ever opened. */ + opened: number; + /** Whether a read is outstanding right now. */ + reading: boolean; + size: ReplTerminalSize; + /** When set, a suspending write never completes. */ + hold: boolean; + feed(bytes: Uint8Array): void; + end(): void; + resized(size: ReplTerminalSize): void; +} + +/** A terminal the test owns completely, and a host that installs it. */ +function recordingTerminal(size: ReplTerminalSize = { columns: 160, rows: 36 }): { + log: TerminalLog; + install(): Operation; +} { + const queue: Uint8Array[] = []; + const watchers = new Set<() => void>(); + let waiting: ((result: IteratorResult) => void) | undefined; + let ended = false; + + const log: TerminalLog = { + raw: [], + writes: 0, + resets: 0, + listeners: 0, + readers: 0, + opened: 0, + reading: false, + size, + hold: false, + feed(bytes: Uint8Array): void { + const resolve = waiting; + if (resolve === undefined) { + queue.push(bytes); + return; + } + waiting = undefined; + log.reading = false; + resolve({ done: false, value: bytes }); + }, + end(): void { + ended = true; + const resolve = waiting; + if (resolve !== undefined) { + waiting = undefined; + log.reading = false; + resolve({ done: true, value: undefined }); + } + }, + resized(next: ReplTerminalSize): void { + log.size = next; + for (const watcher of watchers) { + watcher(); + } + }, + }; + + const host: ReplTerminalCapabilities = { + size(): ReplTerminalSize { + return log.size; + }, + write(_bytes: Uint8Array): Promise { + if (log.hold) { + // Never completes: what a terminal that has stopped accepting bytes + // looks like from here, and where a cancellation can land. + return new Promise(() => {}); + } + log.writes += 1; + return Promise.resolve(); + }, + writeNow(_bytes: Uint8Array): void { + log.resets += 1; + }, + setRaw(raw: boolean): void { + log.raw.push(raw); + }, + bytes(): AsyncIterable { + return { + [Symbol.asyncIterator](): AsyncIterator { + log.readers += 1; + log.opened += 1; + return { + next(): Promise> { + const head = queue.shift(); + if (head !== undefined) { + return Promise.resolve({ done: false, value: head }); + } + if (ended) { + return Promise.resolve({ done: true, value: undefined }); + } + log.reading = true; + return new Promise>((resolve) => { + waiting = resolve; + }); + }, + return(): Promise> { + log.readers -= 1; + log.reading = false; + // Closing a source settles the read that was outstanding on it, + // the way a real reader does. A fake that abandoned the promise + // would leave the runtime holding one forever. + const resolve = waiting; + waiting = undefined; + resolve?.({ done: true, value: undefined }); + return Promise.resolve({ done: true, value: undefined }); + }, + }; + }, + }; + }, + onResize(listener: () => void): () => void { + watchers.add(listener); + log.listeners += 1; + return () => { + watchers.delete(listener); + log.listeners -= 1; + }; + }, + }; + + return { log, install: () => installReplTerminal(host) }; +} + +/** What every exit owes the terminal. */ +function givenBack(log: TerminalLog): void { + // Exactly once, however the owner ended. + expect(log.resets).toBe(1); + expect(log.raw[log.raw.length - 1]).toBe(false); + expect(log.listeners).toBe(0); + expect(log.readers).toBe(0); + expect(log.reading).toBe(false); +} + +const BYTES = new TextEncoder(); + +describe("REPL terminal: giving the terminal back", () => { + it("H2: a run that finishes stops the reader, drops every listener and resets once", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + const finished = yield* spawn(function* (): Operation { + const screen = yield* useReplScreen(); + const events = yield* screen.events(); + const first = yield* events.next(); + expect(first.done).toBe(false); + if (first.done !== true) { + expect(first.value.kind).toBe("resize"); + } + // The reader is a task, and a task starts a turn after it is spawned. + yield* settled(); + expect(terminal.log.raw).toEqual([true]); + expect(terminal.log.opened).toBe(1); + expect(terminal.log.resets).toBe(0); + }); + + yield* finished; + yield* settled(); + givenBack(terminal.log); + expect(terminal.log.opened).toBe(1); + }); + + it("H2: a refusal restores everything a success would have", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + const refusing = yield* spawn(function* (): Operation { + yield* useReplScreen(); + yield* settled(); + // The owner decided it cannot go on. That is an outcome, not an exception, + // and it owes the terminal exactly what any other outcome does. + return new Error("this REPL needs a terminal at least 72x20"); + }); + + const refusal = yield* refusing; + expect(refusal.message).toContain("72x20"); + yield* settled(); + givenBack(terminal.log); + }); + + it("H2: a failure restores everything before it propagates", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + let raised: Error | undefined; + try { + // Its own scope, not a spawned task: a task's failure raises into its + // owner, and what this test needs is the failure delivered to the caller + // with the screen's own scope already unwound. + yield* scoped(function* (): Operation { + yield* useReplScreen(); + yield* settled(); + throw new Error("the renderer could not draw this frame"); + }); + } catch (error) { + raised = error instanceof Error ? error : new Error(String(error)); + } + expect(raised?.message).toContain("could not draw"); + yield* settled(); + givenBack(terminal.log); + }); + + it("H2: a cancellation restores everything, including a reader mid-read", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + const task = yield* spawn(function* (): Operation { + yield* useReplScreen(); + yield* suspend(); + }); + yield* settled(); + // The reader is blocked on bytes that will never come, which is the state a + // person pressing nothing leaves it in. + expect(terminal.log.reading).toBe(true); + expect(terminal.log.readers).toBe(1); + + yield* task.halt(); + yield* settled(); + givenBack(terminal.log); + }); + + it("H2: end of input is a lifecycle outcome, and shuts down like the others", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + const seen: ReplScreenEvent["kind"][] = []; + const reading = yield* spawn(function* (): Operation { + const screen = yield* useReplScreen(); + const events = yield* screen.events(); + terminal.log.feed(BYTES.encode("\r")); + terminal.log.end(); + while (true) { + const next = yield* events.next(); + if (next.done === true) { + return; + } + seen.push(next.value.kind); + if (next.value.kind === "eof") { + // The owner shuts down on end of input; it is not a key anybody claims. + return; + } + } + }); + + yield* reading; + expect(seen).toEqual(["resize", "input", "eof"]); + yield* settled(); + givenBack(terminal.log); + }); + + it("H2: a cancellation during registration leaves no half-taken terminal", function* () { + const terminal = recordingTerminal(); + terminal.log.hold = true; + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + const task = yield* spawn(function* (): Operation { + yield* useReplScreen(); + yield* suspend(); + }); + yield* settled(); + // Raw mode was taken; the mode sequence never landed. + expect(terminal.log.raw).toEqual([true]); + expect(terminal.log.writes).toBe(0); + expect(terminal.log.opened).toBe(0); + + yield* task.halt(); + yield* settled(); + // Restored anyway, because the undo was registered before the do. + givenBack(terminal.log); + }); + + it("H2: no reader and no resize listener outlives a cancellation, at any turn", function* () { + // The mode write is the one window the other cancellation case can hold, and + // it stops short of the reader and the listener. So this walks the owner's + // whole life a turn at a time and cancels at each of them: whichever turn + // opens the byte source or attaches the resize listener, the release for it + // was already registered, so nothing is left behind. + for (let turns = 0; turns <= 8; turns += 1) { + const terminal = recordingTerminal(); + yield* scoped(function* (): Operation { + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + const task = yield* spawn(function* (): Operation { + yield* useReplScreen(); + yield* suspend(); + }); + for (let turn = 0; turn < turns; turn += 1) { + yield* sleep(0); + } + yield* task.halt(); + yield* settled(); + + expect(terminal.log.readers).toBe(0); + expect(terminal.log.listeners).toBe(0); + // At most one reset, and exactly one once anything at all was taken. + expect(terminal.log.resets).toBeLessThanOrEqual(1); + if (terminal.log.raw.includes(true)) { + expect(terminal.log.resets).toBe(1); + expect(terminal.log.raw[terminal.log.raw.length - 1]).toBe(false); + } + }); + } + }); + + it("H2: nothing survives its owner, even the modes it never finished applying", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + const modes = replModes(); + expect(modes.apply.length).toBeGreaterThan(0); + expect(modes.revert.length).toBeGreaterThan(0); + + const task = yield* spawn(function* (): Operation { + yield* useReplScreen(); + yield* suspend(); + }); + yield* settled(); + yield* task.halt(); + yield* settled(); + + // Halting twice must not reset twice: the reset is the terminal's, and it + // happens once. + yield* task.halt(); + yield* settled(); + givenBack(terminal.log); + }); +}); + +/** Read the next event the screen reports, failing if the stream ended. */ +function* reported(events: { + next(): Operation>; +}): Operation { + const next = yield* events.next(); + if (next.done === true) { + throw new Error("the screen ended while this test was waiting for an event"); + } + return next.value; +} + +/** + * Everything the screen reports within a bounded number of turns. + * + * Bounded on purpose: a test that blocked on the next event could only fail by + * hanging, and a hang names nothing. Draining lets the assertion be the exact + * sequence, so a missing event is a diff rather than a timeout. + */ +function* drained(events: { + next(): Operation>; +}): Operation { + const seen: ReplScreenEvent[] = []; + const collector = yield* spawn(function* (): Operation { + while (true) { + const next = yield* events.next(); + if (next.done === true) { + return; + } + seen.push(next.value); + } + }); + for (let turn = 0; turn < 12; turn += 1) { + yield* sleep(0); + } + yield* collector.halt(); + return seen; +} + +/** The normalized events out of a drained sequence, resizes and all else aside. */ +function normalizedFrom(reported: readonly ReplScreenEvent[]): ReplInputEvent[] { + const events: ReplInputEvent[] = []; + for (const one of reported) { + if (one.kind === "input") { + events.push(one.event); + } + } + return events; +} + +describe("REPL terminal: what the host normalizes, and what it refuses to guess", () => { + it("I1: a key becomes a normalized event and a mouse press becomes a position", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + yield* scoped(function* (): Operation { + const screen = yield* useReplScreen(); + const events = yield* screen.events(); + expect((yield* reported(events)).kind).toBe("resize"); + + terminal.log.feed(BYTES.encode("\r")); + expect(yield* reported(events)).toEqual({ + kind: "input", + event: { kind: "key", key: "Enter" }, + }); + + terminal.log.feed(BYTES.encode("\t")); + expect(yield* reported(events)).toEqual({ + kind: "input", + event: { kind: "key", key: "Tab" }, + }); + + // Shift-Tab, which travels the same path rather than through a shortcut of + // the host's own. + terminal.log.feed(BYTES.encode("\x1b[Z")); + expect(yield* reported(events)).toEqual({ + kind: "input", + event: { kind: "key", key: "Backtab" }, + }); + + // A press is a column and a row. Which node is there is the frame's answer. + // The protocol counts from one and the frame counts from zero. + terminal.log.feed(BYTES.encode("\x1b[<0;5;3M")); + const pointer = yield* reported(events); + expect(pointer.kind).toBe("pointer"); + if (pointer.kind === "pointer") { + expect(pointer.at).toEqual({ column: 4, row: 2 }); + } + + // A key nothing has a meaning for is dropped rather than invented into one. + terminal.log.feed(BYTES.encode("\x1b[A")); + terminal.log.feed(BYTES.encode("\r")); + expect(yield* reported(events)).toEqual({ + kind: "input", + event: { kind: "key", key: "Enter" }, + }); + }); + }); + + it("I1: typed and pasted text arrives as text, in the order the terminal sent it", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + yield* scoped(function* (): Operation { + const screen = yield* useReplScreen(); + const events = yield* screen.events(); + + // Real bytes, through the real decoder: ASCII, a shifted capital, a space, + // an accented character and two multi-byte ones; then a pasted newline, + // which the terminal sends as LF and the decoder reports as Control-J with + // no text of its own; then editing and submission. + terminal.log.feed(BYTES.encode("aA é漢字")); + terminal.log.feed(new Uint8Array([0x0a])); + terminal.log.feed(BYTES.encode("b")); + terminal.log.feed(new Uint8Array([0x7f])); + terminal.log.feed(BYTES.encode("\r")); + + expect(normalizedFrom(yield* drained(events))).toEqual([ + { kind: "text", text: "a" }, + { kind: "text", text: "A" }, + { kind: "text", text: " " }, + { kind: "text", text: "é" }, + { kind: "text", text: "漢" }, + { kind: "text", text: "字" }, + { kind: "text", text: "\n" }, + { kind: "text", text: "b" }, + { kind: "key", key: "Backspace" }, + { kind: "key", key: "Enter" }, + ]); + }); + }); + + it("I1: a chord types nothing, whatever letter it was pressed with", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + yield* scoped(function* (): Operation { + const screen = yield* useReplScreen(); + const events = yield* screen.events(); + + // Every one of these decodes to a letter the person did not ask to type: + // Control-C is `c`, Alt-a carries the text `a`, Control-H is `h`, and F5 is + // a key with no meaning here at all. + terminal.log.feed(new Uint8Array([0x03])); + terminal.log.feed(BYTES.encode("\x1ba")); + terminal.log.feed(new Uint8Array([0x08])); + terminal.log.feed(BYTES.encode("\x1b[15~")); + // A sentinel behind them, so the assertion is the whole sequence and not + // just the absence of something. + terminal.log.feed(BYTES.encode("z")); + + expect(normalizedFrom(yield* drained(events))).toEqual([{ kind: "text", text: "z" }]); + }); + }); + + it("I1: text and Backspace reach the draft through the same ancestry as a key", function* () { + const { tree } = yield* mounted(FILLED); + + // Focus is on the draft line, which is where typing goes. + const draft = tree.mounted().find((id) => tree.keyOf(id) === "footer:input"); + expect(tree.focused()).toBe(draft); + + const typed = acted(yield* tree.dispatch({ kind: "text", text: "é" })); + expect(typed.outcome).toBe("action"); + if (typed.outcome === "action") { + expect(typed.action).toEqual({ kind: "insert", text: "é" }); + expect(typed.ancestry[0]).toBe(draft); + } + + const pasted = acted(yield* tree.dispatch({ kind: "text", text: "\n" })); + if (pasted.outcome === "action") { + expect(pasted.action).toEqual({ kind: "insert", text: "\n" }); + } + + const erased = acted(yield* tree.dispatch({ kind: "key", key: "Backspace" })); + if (erased.outcome === "action") { + expect(erased.action).toEqual({ kind: "erase" }); + } + + // And the named keys still mean what they meant. + const submitted = acted(yield* tree.dispatch({ kind: "key", key: "Enter" })); + if (submitted.outcome === "action") { + expect(submitted.action).toEqual({ kind: "submit" }); + } + const moved = acted(yield* tree.dispatch({ kind: "key", key: "Tab" })); + expect(moved.outcome).toBe("focus"); + }); + + it("I1: text a row does not claim passes it on the way up", function* () { + const { tree } = yield* mounted({ ...FILLED, drawer: "Binding: plan" }); + + // Focus is inside the drawer, on a row that has no use for text. + const closer = tree.mounted().find((id) => tree.keyOf(id) === "drawer:close"); + expect(tree.focused()).toBe(closer); + + // Nothing above it claims text either, so this is an explicit failure rather + // than a keystroke silently swallowed. + const outcome = yield* tree.dispatch({ kind: "text", text: "a" }); + expect(outcome.ok).toBe(false); + if (!outcome.ok) { + expect(outcome.error.name).toBe("ReplUnownedEventError"); + } + + // Escape still closes it. + const escaped = acted(yield* tree.dispatch({ kind: "key", key: "Escape" })); + if (escaped.outcome === "action") { + expect(escaped.action).toEqual({ kind: "close-drawer" }); + } + }); + + it("I1: a lone Escape waits for the decoder's latency and then arrives", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + yield* scoped(function* (): Operation { + const screen = yield* useReplScreen(); + const events = yield* screen.events(); + expect((yield* reported(events)).kind).toBe("resize"); + + terminal.log.feed(BYTES.encode("\x1b")); + yield* settled(); + // Undecided: it could still be the start of a sequence. + expect(clock.waits).toBe(1); + + yield* clock.tick(0.025); + expect(yield* reported(events)).toEqual({ + kind: "input", + event: { kind: "key", key: "Escape" }, + }); + }); + }); + + it("I1: bytes that follow a pending Escape are rescanned with it, not split", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + yield* scoped(function* (): Operation { + const screen = yield* useReplScreen(); + const events = yield* screen.events(); + expect((yield* reported(events)).kind).toBe("resize"); + + // An escape sequence arriving in two pieces, which is what a slow link does. + terminal.log.feed(BYTES.encode("\x1b")); + yield* settled(); + expect(clock.waits).toBe(1); + terminal.log.feed(BYTES.encode("[Z")); + + // Backtab, and no Escape before it: the held prefix was rescanned with what + // followed rather than flushed as a keypress nobody made. + expect(yield* reported(events)).toEqual({ + kind: "input", + event: { kind: "key", key: "Backtab" }, + }); + }); + }); + + it("I1: a resize is a size the host reports, not a key anybody claims", function* () { + const terminal = recordingTerminal(); + yield* terminal.install(); + const clock = controllableClock(); + yield* clock.install(); + + yield* scoped(function* (): Operation { + const screen = yield* useReplScreen(); + const events = yield* screen.events(); + expect(yield* reported(events)).toEqual({ kind: "resize", size: { columns: 160, rows: 36 } }); + + terminal.log.resized({ columns: 72, rows: 20 }); + expect(yield* reported(events)).toEqual({ kind: "resize", size: { columns: 72, rows: 20 } }); + expect(yield* screen.size()).toEqual({ columns: 72, rows: 20 }); + + // The same size again is the same frame again, and says nothing new. + terminal.log.resized({ columns: 72, rows: 20 }); + terminal.log.feed(BYTES.encode("\r")); + expect(yield* reported(events)).toEqual({ + kind: "input", + event: { kind: "key", key: "Enter" }, + }); + }); + }); + + it("I1: a key and a click on one control produce one action", function* () { + const { tree, surface } = yield* mounted(FILLED); + const renderer = yield* useReplRenderer({ columns: 160, rows: 36 }); + const rendered = yield* drawn( + renderer, + input(layout({ columns: 160, rows: 36 }, surface), tree), + ); + + const byKey = acted(yield* tree.dispatch({ kind: "key", key: "Enter" })); + expect(byKey.outcome).toBe("action"); + + const control = rendered.map.targets.find( + (target) => tree.keyOf(target.node) === "footer:input", + ); + if (control === undefined) { + throw new Error("the footer input is drawn and targetable"); + } + const pointer = resolvePointer(rendered, { column: control.bounds.x, row: control.bounds.y }); + expect(pointer).toEqual({ kind: "pointer", target: control.node, frame: rendered.tree }); + if (pointer === undefined) { + throw new Error("a pointer inside a drawn control resolves to it"); + } + const byPointer = acted(yield* tree.dispatch(pointer)); + + expect(byPointer).toEqual(byKey); + }); + + it("I1: a pointer outside the open drawer reaches nothing", function* () { + const state = { ...FILLED, drawer: "Binding: plan" }; + const { tree, surface } = yield* mounted(state); + const renderer = yield* useReplRenderer({ columns: 160, rows: 36 }); + const rendered = yield* drawn( + renderer, + input(layout({ columns: 160, rows: 36 }, surface), tree), + ); + + const behind = rendered.map.targets.find( + (target) => tree.keyOf(target.node) === "session:kf39sla2", + ); + if (behind === undefined) { + throw new Error("the sidebar's session row is drawn and targetable"); + } + const pointer = resolvePointer(rendered, { column: behind.bounds.x, row: behind.bounds.y }); + if (pointer === undefined) { + throw new Error("a pointer inside a drawn control resolves to it"); + } + + const outcome = acted(yield* tree.dispatch(pointer)); + expect(outcome.outcome).toBe("dropped"); + if (outcome.outcome === "dropped") { + expect(outcome.reason).toContain("behind the open drawer"); + } + }); + + it("I1: a pointer resolved against a frame the tree has moved past reaches nothing", function* () { + const { tree, surface } = yield* mounted(FILLED); + const renderer = yield* useReplRenderer({ columns: 160, rows: 36 }); + const rendered = yield* drawn( + renderer, + input(layout({ columns: 160, rows: 36 }, surface), tree), + ); + + const control = rendered.map.targets.find( + (target) => tree.keyOf(target.node) === "entry:entry-1", + ); + if (control === undefined) { + throw new Error("the entry row is drawn and targetable"); + } + const pointer = resolvePointer(rendered, { column: control.bounds.x, row: control.bounds.y }); + if (pointer === undefined) { + throw new Error("a pointer inside a drawn control resolves to it"); + } + + // The screen moved on. + committed(yield* tree.apply(fixtureDescriptions({ ...FILLED, draft: "yes" }))); + expect(tree.frame().id).not.toBe(pointer.frame); + + const outcome = acted(yield* tree.dispatch(pointer)); + expect(outcome.outcome).toBe("dropped"); + if (outcome.outcome === "dropped") { + expect(outcome.reason).toContain("no longer drawn"); + } + }); + + it("I1: a pointer naming a node the tree removed reaches nothing", function* () { + const { tree, surface } = yield* mounted(FILLED); + const renderer = yield* useReplRenderer({ columns: 160, rows: 36 }); + const rendered = yield* drawn( + renderer, + input(layout({ columns: 160, rows: 36 }, surface), tree), + ); + + const removed = rendered.map.targets.find( + (target) => tree.keyOf(target.node) === "scope:entry-1/generated", + ); + if (removed === undefined) { + throw new Error("the generated scope row is drawn and targetable"); + } + + committed(yield* tree.apply(fixtureDescriptions({ ...FILLED, scopes: ["entry-1/component"] }))); + expect(tree.mounted()).not.toContain(removed.node); + + // Carrying the current frame's number, so what is under test is the node and + // not the frame: a pointer cannot reach something that is not there. + const outcome = acted( + yield* tree.dispatch({ kind: "pointer", target: removed.node, frame: tree.frame().id }), + ); + expect(outcome.outcome).toBe("dropped"); + if (outcome.outcome === "dropped") { + expect(outcome.reason).toContain("no longer holds"); + } + + // And redrawing does not offer it either. + const after = yield* drawn( + renderer, + input(layout({ columns: 160, rows: 36 }, fixtureSurface(tree, FILLED)), tree), + ); + expect(after.map.targets.map((target) => target.node)).not.toContain(removed.node); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8cc5691b..3f3687d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ importers: packages/cli: dependencies: + '@bomb.sh/tty': + specifier: 0.9.0 + version: 0.9.0 '@effectionx/stream-helpers': specifier: 0.8.3 version: 0.8.3(effection@4.1.0) @@ -552,6 +555,10 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@bomb.sh/tty@0.9.0': + resolution: {integrity: sha512-1fX9lgwdc+kGRQeVEYwv7cJb5i855ysvB/TMCC3/wnnjMiKLZjWew6LLnsAMq4pLzkweivYw+zwYNZRUVhjoBA==} + engines: {node: '>= 22'} + '@clack/core@1.4.3': resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} engines: {node: '>= 20.12.0'} @@ -2723,6 +2730,8 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} + '@bomb.sh/tty@0.9.0': {} + '@clack/core@1.4.3': dependencies: fast-wrap-ansi: 0.2.2 diff --git a/test-weights.json b/test-weights.json index e77cffc5..bb32b4d5 100644 --- a/test-weights.json +++ b/test-weights.json @@ -1,8 +1,8 @@ { "version": 1, "source": { - "commit": "1019ea528d1de23c51c55467317ce5def25e5344", - "runUrl": "https://github.com/taras/executable.md/actions/runs/36293682586", + "commit": "05c912f26682c7ddf1818403aa332f057675e1cb", + "runUrl": "https://github.com/taras/executable.md/actions/runs/36319908505", "attempt": 1, "runner": "ubuntu-latest", "deno": "deno 2.9.5 (stable, release, x86_64-unknown-linux-gnu)", @@ -11,1085 +11,1088 @@ }, "runtimes": { "deno": { - "packages/acp/tests/acceptance.test.ts": 3169, - "packages/acp/tests/acpx-checkpoint-meta.test.ts": 6815, - "packages/acp/tests/acpx-materialization.test.ts": 7495, - "packages/acp/tests/acpx-transient-env.test.ts": 1214, - "packages/acp/tests/adapter-materialization.test.ts": 50704, - "packages/acp/tests/adapter-protocol.test.ts": 39473, - "packages/acp/tests/codex-surfaces.test.ts": 1214, - "packages/acp/tests/loaded-copy.test.ts": 3149, - "packages/acp/tests/native-launch.test.ts": 3748, - "packages/acp/tests/permission-bridge.test.ts": 2752, - "packages/acp/tests/provider.test.ts": 3685, - "packages/acp/tests/serial-queue.test.ts": 1094, - "packages/acp/tests/session-key.test.ts": 2712, - "packages/acp/tests/session-route.test.ts": 3035, - "packages/acp/tests/terminal-screen.test.ts": 1140, - "packages/cli/tests/agent-adapters.test.ts": 3834, - "packages/cli/tests/agent-cli.test.ts": 26979, - "packages/cli/tests/agent-config.test.ts": 2484, - "packages/cli/tests/agent-options-cli.test.ts": 10818, - "packages/cli/tests/agent-options.test.ts": 2340, - "packages/cli/tests/agent-session-coordinator.test.ts": 2842, - "packages/cli/tests/cli-help.test.ts": 10193, - "packages/cli/tests/command.test.ts": 7673, - "packages/cli/tests/compiled-upgrade.test.ts": 7320, - "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 5694, - "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 95451, - "packages/cli/tests/document-suites/props/props-markdown.test.ts": 5355, - "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 7616, - "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 6254, - "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 9688, - "packages/cli/tests/evaluate-component.test.ts": 7891, - "packages/cli/tests/evaluate-workflow.test.ts": 3856, - "packages/cli/tests/fetch-cli.test.ts": 2528, - "packages/cli/tests/file-stream.test.ts": 1242, - "packages/cli/tests/github-zero-effect.test.ts": 4707, - "packages/cli/tests/inline-cli.test.ts": 21873, - "packages/cli/tests/launch-timeout.test.ts": 11118, - "packages/cli/tests/packaged-document.test.ts": 3582, - "packages/cli/tests/plan-args.test.ts": 1128, - "packages/cli/tests/plan-cli.test.ts": 71295, - "packages/cli/tests/plan-command-document.test.ts": 6998, - "packages/cli/tests/plan-component.test.ts": 8773, - "packages/cli/tests/plan-host-acts.test.ts": 3814, - "packages/cli/tests/plan.test.ts": 11119, - "packages/cli/tests/plugin-cli.test.ts": 27684, - "packages/cli/tests/plugin-host.test.ts": 4210, - "packages/cli/tests/plugin-modules.test.ts": 1067, - "packages/cli/tests/plugin-selection.test.ts": 1085, - "packages/cli/tests/process-retention.test.ts": 4061, - "packages/cli/tests/props-cli.test.ts": 20490, - "packages/cli/tests/props-schema.test.ts": 2853, - "packages/cli/tests/props-sources.test.ts": 1583, - "packages/cli/tests/pull-request-read-fork.test.ts": 5117, - "packages/cli/tests/repl-composition.test.ts": 1343, - "packages/cli/tests/repl-execution.test.ts": 3803, - "packages/cli/tests/repl-model.test.ts": 3191, - "packages/cli/tests/repl-route.test.ts": 3216, - "packages/cli/tests/run-composition-deno.test.ts": 5885, - "packages/cli/tests/run-composition-nested.test.ts": 15691, - "packages/cli/tests/run-composition.test.ts": 4767, - "packages/cli/tests/run-deadline.test.ts": 6166, - "packages/cli/tests/run-profile.test.ts": 3006, - "packages/cli/tests/run-timeouts.test.ts": 10022, - "packages/cli/tests/secret-detection-cli.test.ts": 12764, - "packages/cli/tests/service-document.test.ts": 3581, - "packages/cli/tests/service-host.test.ts": 3534, - "packages/cli/tests/session-launch-cli.test.ts": 7736, - "packages/cli/tests/stdin-cli.test.ts": 24407, - "packages/cli/tests/stdout-delivery.test.ts": 1031, - "packages/cli/tests/syntax-cli.test.ts": 23624, - "packages/cli/tests/targets-cli.test.ts": 18035, - "packages/cli/tests/test-root-profile.test.ts": 5164, - "packages/cli/tests/test-target.test.ts": 23252, - "packages/cli/tests/testing-activation.test.ts": 3051, - "packages/cli/tests/testing-execution-host.test.ts": 38621, - "packages/cli/tests/upgrade-cli.test.ts": 13145, - "packages/cli/tests/upgrade-command-document.test.ts": 4957, - "packages/cli/tests/upgrade-output.test.ts": 2988, - "packages/cli/tests/value-root.test.ts": 11364, - "packages/cli/tests/verbose-component.test.ts": 3365, - "packages/cli/tests/workflow-agent-sessions.test.ts": 13330, - "packages/cli/tests/workflow-agent.test.ts": 10656, - "packages/cli/tests/workflow-cli.test.ts": 75608, - "packages/cli/tests/workflow-crash.test.ts": 73495, - "packages/cli/tests/workflow-declaration.test.ts": 2954, - "packages/cli/tests/workflow-export-publication.test.ts": 12331, - "packages/cli/tests/workflow-fetch.test.ts": 5623, - "packages/cli/tests/workflow-fork.test.ts": 54119, - "packages/cli/tests/workflow-host.test.ts": 9719, - "packages/cli/tests/workflow-inspection.test.ts": 79159, - "packages/cli/tests/workflow-installation.test.ts": 4329, - "packages/cli/tests/workflow-lifecycle-control.test.ts": 14615, - "packages/cli/tests/workflow-retention.test.ts": 16426, - "packages/cli/tests/workflow-suspension.test.ts": 5619, - "packages/code-review-agent/tests/doctor.test.ts": 1014, - "packages/code-review-agent/tests/parse-diagnostics.test.ts": 1159, - "packages/code-review-agent/tests/parse-diff.test.ts": 1077, - "packages/code-review-agent/tests/parse-doctor.test.ts": 2657, - "packages/code-review-agent/tests/parse-oxlint.test.ts": 982, - "packages/code-review-agent/tests/policy.test.ts": 1023, - "packages/core/tests/agent-api.test.ts": 1089, - "packages/core/tests/agent-components.test.ts": 3386, - "packages/core/tests/agent-function-components.test.ts": 4052, - "packages/core/tests/agent-session-launch.test.ts": 4152, - "packages/core/tests/agent-session-placement.test.ts": 1040, - "packages/core/tests/agent-session-use-loaded-copy.test.ts": 1048, - "packages/core/tests/agent-session-use.test.ts": 992, - "packages/core/tests/agent-stack.test.ts": 2690, - "packages/core/tests/answer-identity.test.ts": 2316, - "packages/core/tests/answers-component.test.ts": 3154, - "packages/core/tests/answers-expansion-recursion.test.ts": 2438, - "packages/core/tests/capture-errors.test.ts": 2617, - "packages/core/tests/capture-props.test.ts": 2517, - "packages/core/tests/cli-journal.test.ts": 7925, - "packages/core/tests/code-block-component.test.ts": 3302, - "packages/core/tests/compiler-boundary.test.ts": 2617, - "packages/core/tests/component-api.test.ts": 2275, - "packages/core/tests/component-registration.test.ts": 2896, - "packages/core/tests/component-returns.test.ts": 3236, - "packages/core/tests/component-schema-conformance.test.ts": 2608, - "packages/core/tests/config-api.test.ts": 2930, - "packages/core/tests/construct-error-observation.test.ts": 2689, - "packages/core/tests/contextual-cwd.test.ts": 3745, - "packages/core/tests/daemon-integration.test.ts": 3206, - "packages/core/tests/daemon.test.ts": 2369, - "packages/core/tests/declared-markdown-component.test.ts": 3554, - "packages/core/tests/document-output-api.test.ts": 983, - "packages/core/tests/document-target-execution.test.ts": 4285, - "packages/core/tests/document-targets.test.ts": 2510, - "packages/core/tests/document-validation.test.ts": 3395, - "packages/core/tests/documentation-index.test.ts": 2776, - "packages/core/tests/each.test.ts": 2610, - "packages/core/tests/elicit-component.test.ts": 2936, - "packages/core/tests/ephemeral-service.test.ts": 2757, - "packages/core/tests/eval-bindings.test.ts": 2395, - "packages/core/tests/eval-context.test.ts": 1356, - "packages/core/tests/eval-durable.test.ts": 2451, - "packages/core/tests/eval-error-mode.test.ts": 2768, - "packages/core/tests/eval-interpolate.test.ts": 955, - "packages/core/tests/eval-middleware.test.ts": 2559, - "packages/core/tests/eval-persist.test.ts": 2817, - "packages/core/tests/eval-return.test.ts": 2489, - "packages/core/tests/eval-scope.test.ts": 2489, - "packages/core/tests/eval-timeout.test.ts": 2477, - "packages/core/tests/eval-transform.test.ts": 2178, - "packages/core/tests/evaluate-component.test.ts": 3601, - "packages/core/tests/evaluate-loaded-copy.test.ts": 2547, - "packages/core/tests/evaluate-provider-lifetime.test.ts": 2438, - "packages/core/tests/evaluation-profile.test.ts": 2309, - "packages/core/tests/exec-timeout.test.ts": 2588, - "packages/core/tests/execute.test.ts": 2884, - "packages/core/tests/execution-protocol.test.ts": 3356, - "packages/core/tests/expand.test.ts": 2972, - "packages/core/tests/expansion-identity.test.ts": 2941, - "packages/core/tests/expansion-metadata.test.ts": 2439, - "packages/core/tests/expression-props.test.ts": 2859, - "packages/core/tests/fail-component.test.ts": 2732, - "packages/core/tests/failure-printing.test.ts": 2634, - "packages/core/tests/fatal-cause.test.ts": 2358, - "packages/core/tests/fetch-component.test.ts": 3421, - "packages/core/tests/file-component.test.ts": 4085, - "packages/core/tests/file-delete-component.test.ts": 2556, - "packages/core/tests/files-fatal.test.ts": 3148, - "packages/core/tests/foreground-exec.test.ts": 3422, - "packages/core/tests/frontmatter.test.ts": 2409, - "packages/core/tests/function-components.test.ts": 3054, - "packages/core/tests/generated-composition.test.ts": 3009, - "packages/core/tests/generated-xmd.test.ts": 4020, - "packages/core/tests/glob-api.test.ts": 1269, - "packages/core/tests/glob-component.test.ts": 3197, - "packages/core/tests/guarded-journal.test.ts": 2736, - "packages/core/tests/has-content.test.ts": 2494, - "packages/core/tests/heal.test.ts": 2519, - "packages/core/tests/if.test.ts": 2906, - "packages/core/tests/inline-root.test.ts": 2678, - "packages/core/tests/invocation-failures.test.ts": 2421, - "packages/core/tests/invocation-identity.test.ts": 2989, - "packages/core/tests/invocation-scope.test.ts": 2829, - "packages/core/tests/journal-source-position.test.ts": 2860, - "packages/core/tests/json-component.test.ts": 2884, - "packages/core/tests/json.test.ts": 2506, - "packages/core/tests/let.test.ts": 2870, - "packages/core/tests/loaded-copy-files.test.ts": 2736, - "packages/core/tests/loop.test.ts": 3443, - "packages/core/tests/named-slots.test.ts": 3244, - "packages/core/tests/output-error-mode.test.ts": 3330, - "packages/core/tests/output-normalize.test.ts": 1055, - "packages/core/tests/output-terminal.test.ts": 1115, - "packages/core/tests/parse-components.test.ts": 2779, - "packages/core/tests/plan-response.test.ts": 1113, - "packages/core/tests/plugin-api.test.ts": 2355, - "packages/core/tests/plugin.test.ts": 2544, - "packages/core/tests/props-binding.test.ts": 2752, - "packages/core/tests/protected-content.test.ts": 2634, - "packages/core/tests/registered-printed-errors.test.ts": 2648, - "packages/core/tests/replay-stream.test.ts": 1134, - "packages/core/tests/retain.test.ts": 2856, - "packages/core/tests/root-composition.test.ts": 2883, - "packages/core/tests/root-props.test.ts": 2908, - "packages/core/tests/root-provider.test.ts": 2815, - "packages/core/tests/sample-component.test.ts": 3499, - "packages/core/tests/scanner.test.ts": 2512, - "packages/core/tests/scope-local.test.ts": 1041, - "packages/core/tests/secret-detection.test.ts": 3461, - "packages/core/tests/secret-files.test.ts": 1370, - "packages/core/tests/secret-rules.test.ts": 1293, - "packages/core/tests/secret-scanner-baseline.test.ts": 1086, - "packages/core/tests/secret-scanner.test.ts": 1210, - "packages/core/tests/source-position.test.ts": 2478, - "packages/core/tests/state-ownership.test.ts": 2487, - "packages/core/tests/streaming-emission.test.ts": 2539, - "packages/core/tests/switch.test.ts": 2906, - "packages/core/tests/syntax-catalog.test.ts": 2870, - "packages/core/tests/syntax-component.test.ts": 3419, - "packages/core/tests/syntax-loaded-copy.test.ts": 2603, - "packages/core/tests/temp-dir.test.ts": 4836, - "packages/core/tests/temp-file-compiler.test.ts": 1409, - "packages/core/tests/text-interpolation.test.ts": 2412, - "packages/core/tests/try-content.test.ts": 2413, - "packages/core/tests/unused-in-diff.test.ts": 2690, - "packages/core/tests/validate.test.ts": 2628, - "packages/core/tests/validation-integration.test.ts": 2675, - "packages/core/tests/workflow-component-bundle.test.ts": 2810, - "packages/durable-streams/tests/context.test.ts": 1199, - "packages/durable-streams/tests/deterministic-id.test.ts": 1254, - "packages/durable-streams/tests/divergence-api.test.ts": 1248, - "packages/durable-streams/tests/divergence.test.ts": 1260, - "packages/durable-streams/tests/durable-each.test.ts": 1267, - "packages/durable-streams/tests/durable-position.test.ts": 1236, - "packages/durable-streams/tests/durable-run.test.ts": 1284, - "packages/durable-streams/tests/ephemeral.test.ts": 1252, - "packages/durable-streams/tests/fail-stop.test.ts": 1251, - "packages/durable-streams/tests/guard-stream.test.ts": 1283, - "packages/durable-streams/tests/live-coordinator.test.ts": 1256, - "packages/durable-streams/tests/parse.test.ts": 1263, - "packages/durable-streams/tests/replay-guard.test.ts": 1279, - "packages/durable-streams/tests/replay-index.test.ts": 1120, - "packages/durable-streams/tests/retained.test.ts": 1116, - "packages/durable-streams/tests/serialize-event.test.ts": 1214, - "packages/durable-streams/tests/smoke.test.ts": 1208, - "packages/durable-streams/tests/structured-concurrency.test.ts": 1298, - "packages/durable-streams/tests/terminal-boundary.test.ts": 1278, - "packages/durable-streams/tests/types.test.ts": 1005, - "packages/git/tests/ambient-authentication.test.ts": 21003, - "packages/git/tests/api-entrypoint.test.ts": 3322, - "packages/git/tests/credential-helper.test.ts": 4228, - "packages/git/tests/git-add-crash.test.ts": 4763, - "packages/git/tests/git-add-durability.test.ts": 20972, - "packages/git/tests/git-add.test.ts": 27364, - "packages/git/tests/git-commit-crash.test.ts": 4999, - "packages/git/tests/git-commit-durability.test.ts": 25818, - "packages/git/tests/git-commit.test.ts": 29919, - "packages/git/tests/git-host-effect.test.ts": 5375, - "packages/git/tests/git-push-crash.test.ts": 6427, - "packages/git/tests/git-push-durability.test.ts": 31602, - "packages/git/tests/git-push.test.ts": 41850, - "packages/git/tests/git-switch-crash.test.ts": 4407, - "packages/git/tests/git-switch-durability.test.ts": 14722, - "packages/git/tests/git-switch.test.ts": 15789, - "packages/git/tests/git.test.ts": 2693, - "packages/git/tests/github-activation.test.ts": 3559, - "packages/git/tests/github-issues.test.ts": 2772, - "packages/git/tests/github-pull-requests.test.ts": 2491, - "packages/git/tests/github-workflow-activation.test.ts": 3686, - "packages/git/tests/issue-markdown.test.ts": 6440, - "packages/git/tests/issue-records.test.ts": 2619, - "packages/git/tests/materialization.test.ts": 3044, - "packages/git/tests/module-partition.test.ts": 3000, - "packages/git/tests/plugin.test.ts": 2910, - "packages/git/tests/provider-neutrality.test.ts": 1977, - "packages/git/tests/public-entrypoint.test.ts": 6055, - "packages/git/tests/pull-request-crash.test.ts": 5072, - "packages/git/tests/pull-request-durability.test.ts": 29552, - "packages/git/tests/pull-request-read.test.ts": 4951, - "packages/git/tests/pull-request-records.test.ts": 2699, - "packages/git/tests/pull-request.test.ts": 54743, - "packages/git/tests/repository-components.test.ts": 7912, - "packages/git/tests/repository-control-plane.test.ts": 7089, - "packages/git/tests/repository-materialization.test.ts": 5185, - "packages/git/tests/repository-replay.test.ts": 10523, - "packages/git/tests/repository-storage.test.ts": 12922, - "packages/git/tests/run-composition-ambient.test.ts": 8589, - "packages/git/tests/run-composition-lazy.test.ts": 3480, - "packages/git/tests/run-composition-managed.test.ts": 10424, - "packages/git/tests/run-composition-remote.test.ts": 11300, - "packages/git/tests/selection-authentication.test.ts": 1134, - "packages/git/tests/worktree-replay.test.ts": 4166, - "packages/runtime/tests/agent-session-coordinator.test.ts": 1349, - "packages/runtime/tests/duration.test.ts": 927, - "packages/runtime/tests/executable-observer.test.ts": 1195, - "packages/runtime/tests/fetch.test.ts": 1634, - "packages/runtime/tests/fs.test.ts": 1112, - "packages/runtime/tests/host-files.test.ts": 1820, - "packages/runtime/tests/native-launcher.test.ts": 3293, - "packages/runtime/tests/process.test.ts": 1052, - "packages/runtime/tests/service.test.ts": 1132, - "packages/test-agent/tests/acp-server.test.ts": 1011, - "packages/test-agent/tests/behavior-engine.test.ts": 2573, - "packages/test-agent/tests/bridge.test.ts": 2359, - "packages/test-agent/tests/command-lazy.test.ts": 5319, - "packages/test-agent/tests/components.test.ts": 43184, - "packages/test-agent/tests/controller.test.ts": 1763, - "packages/test-agent/tests/cross-package-resolution.test.ts": 3539, - "packages/test-agent/tests/native-launch.test.ts": 19687, - "packages/test-agent/tests/net.test.ts": 961, - "packages/test-agent/tests/profile.test.ts": 2370, - "packages/test-agent/tests/protocol.test.ts": 1543, - "packages/test-agent/tests/provider.test.ts": 2619, - "packages/test-agent/tests/public-api.test.ts": 3036, - "packages/test-agent/tests/route-slot.test.ts": 970, - "packages/test-agent/tests/smoke.test.ts": 14866, - "packages/test-agent/tests/template.test.ts": 2333, - "packages/test-agent/tests/worker-lifecycle.test.ts": 8364, - "packages/test-support/tests/journal.test.ts": 1140, - "packages/testing/tests/agent-composition.test.ts": 2622, - "packages/testing/tests/assert-throws.test.ts": 3349, - "packages/testing/tests/assert.test.ts": 977, - "packages/testing/tests/assertions.test.ts": 3723, - "packages/testing/tests/boundary-contract.test.ts": 3225, - "packages/testing/tests/cli.test.ts": 4140, - "packages/testing/tests/execution-harness.test.ts": 5270, - "packages/testing/tests/replay.test.ts": 3360, - "packages/testing/tests/smoke.test.ts": 3651, - "packages/testing/tests/test-component.test.ts": 3895, - "packages/testing/tests/testing-activation.test.ts": 3548, - "packages/testing/tests/testing-boundary.test.ts": 2889, - "packages/testing/tests/testing-mode.test.ts": 3817, - "packages/testing/tests/use-testing.test.ts": 3327, - "packages/web/tests/assets.test.ts": 1050, - "packages/web/tests/client-logic.test.ts": 1038, - "packages/web/tests/compile.test.ts": 2939, - "packages/web/tests/component.test.ts": 3338, - "packages/web/tests/declaration.test.ts": 2803, - "packages/web/tests/document.test.ts": 2963, - "packages/web/tests/elicitation.test.ts": 2745, - "packages/web/tests/live-form.test.ts": 2812, - "packages/web/tests/markdown.test.ts": 1110, - "packages/web/tests/opener.test.ts": 2731, - "packages/web/tests/page.test.ts": 1032, - "packages/web/tests/responder.test.ts": 3080, - "packages/web/tests/server-lifecycle.test.ts": 2915, - "packages/web/tests/server.test.ts": 3021, - "packages/workflow/tests/generated-agent-component.test.ts": 4494, - "packages/workflow/tests/generated-observations.test.ts": 2732, - "packages/workflow/tests/public-entrypoint.test.ts": 3872, - "packages/workflow/tests/retained-run.test.ts": 3071, - "packages/workflow/tests/service-denial.test.ts": 1189, - "packages/workflow/tests/workflow-agent-checkpoints.test.ts": 3946, - "packages/workflow/tests/workflow-agent-sessions.test.ts": 3244, - "packages/workflow/tests/workflow-bundle.test.ts": 2808, - "packages/workflow/tests/workflow-checkpoint.test.ts": 4029, - "packages/workflow/tests/workflow-definition.test.ts": 2614, - "packages/workflow/tests/workflow-export.test.ts": 5038, - "packages/workflow/tests/workflow-fork-source.test.ts": 3176, - "packages/workflow/tests/workflow-fork.test.ts": 2627, - "packages/workflow/tests/workflow-lifecycle-authority.test.ts": 3999, - "packages/workflow/tests/workflow-lifecycle-control.test.ts": 4113, - "packages/workflow/tests/workflow-lifecycle-inspection.test.ts": 8212, - "packages/workflow/tests/workflow-run-journal.test.ts": 4574, - "packages/workflow/tests/workflow-run-storage.test.ts": 5897, - "packages/workflow/tests/workflow-run.test.ts": 3322, - "packages/workflow/tests/workflow-suspension-answer.test.ts": 3816, - "packages/workflow/tests/workflow-suspension.test.ts": 3394, - "packages/workflow/tests/workspace-crash-recovery.test.ts": 4966, - "packages/workflow/tests/workspace-effect-loaded-copy.test.ts": 3201, - "packages/workflow/tests/workspace-effect-transaction.test.ts": 3862, - "packages/workflow/tests/workspace-effect.test.ts": 3487, - "packages/workflow/tests/workspace-files.test.ts": 8146, - "packages/workflow/tests/workspace-root-restoration.test.ts": 4396, - "packages/workflow/tests/workspace-root.test.ts": 3237, - "packages/workflow/tests/workspace-transaction.test.ts": 3206, - "packages/workflow/tests/xmd-artifact.test.ts": 4445, - "scripts/tests/acpx-vendor.test.ts": 1396, - "scripts/tests/adapter-distribution.test.ts": 2977, - "scripts/tests/adapter-npm-package.test.ts": 22618, - "scripts/tests/adapter-vendor.test.ts": 2615, - "scripts/tests/bootstrap-npm-package.test.ts": 8604, - "scripts/tests/build-npm.test.ts": 25291, - "scripts/tests/build-web-client.test.ts": 20579, - "scripts/tests/bump-version.test.ts": 1055, - "scripts/tests/changed-paths.test.ts": 992, - "scripts/tests/ci-workflow.test.ts": 3227, - "scripts/tests/cli-npm-bin.test.ts": 155690, - "scripts/tests/cloudflare-dofs-vendor.test.ts": 2738, - "scripts/tests/consumer-cycle.test.ts": 1012, - "scripts/tests/documentation-validation.test.ts": 3898, - "scripts/tests/filesystem-contract-workflow.test.ts": 977, - "scripts/tests/frozen-entry.test.ts": 6493, - "scripts/tests/jsr-consumer-documentation.test.ts": 11143, - "scripts/tests/main-green.test.ts": 1254, - "scripts/tests/main-health.test.ts": 1053, - "scripts/tests/measure-test-weights.test.ts": 1244, - "scripts/tests/no-module-scoped-registry.test.ts": 1493, - "scripts/tests/no-redundant-test-scope.test.ts": 4111, - "scripts/tests/no-sync-filesystem.test.ts": 3917, - "scripts/tests/no-yield-in-finally.test.ts": 1492, - "scripts/tests/oxlint-policy.test.ts": 7492, - "scripts/tests/packaged-document.test.ts": 989, - "scripts/tests/prefer-effection-operation.test.ts": 4659, - "scripts/tests/prefer-effection-result.test.ts": 3518, - "scripts/tests/prepared-state.test.ts": 1764, - "scripts/tests/publish-workflow-generator.test.ts": 3644, - "scripts/tests/publish-workflow-membership.test.ts": 1037, - "scripts/tests/publishable-membership-agreement.test.ts": 2425, - "scripts/tests/readme-targets.test.ts": 11201, - "scripts/tests/release-targets.test.ts": 974, - "scripts/tests/review-infrastructure.test.ts": 3707, - "scripts/tests/runtime-exclusions.test.ts": 1209, - "scripts/tests/runtime-tests.test.ts": 1033, - "scripts/tests/scope-bound-event-registration.test.ts": 3730, - "scripts/tests/shard-execution.test.ts": 8404, - "scripts/tests/staged-write.test.ts": 953, - "scripts/tests/test-file-discovery.test.ts": 1094, - "scripts/tests/test-shards.test.ts": 932, - "scripts/tests/test-weights.test.ts": 1177, - "scripts/tests/tracked.test.ts": 968, - "scripts/tests/verify-adapter.test.ts": 6181, - "scripts/tests/verify-clean.test.ts": 1111, - "scripts/tests/verify-coordinator.test.ts": 1269, - "scripts/tests/version-lockstep.test.ts": 1055, - "scripts/tests/web-client-module.test.ts": 967, - "scripts/tests/workspace.test.ts": 942 + "packages/acp/tests/acceptance.test.ts": 4424, + "packages/acp/tests/acpx-checkpoint-meta.test.ts": 7578, + "packages/acp/tests/acpx-materialization.test.ts": 7886, + "packages/acp/tests/acpx-transient-env.test.ts": 1594, + "packages/acp/tests/adapter-materialization.test.ts": 71437, + "packages/acp/tests/adapter-protocol.test.ts": 54597, + "packages/acp/tests/codex-surfaces.test.ts": 1607, + "packages/acp/tests/loaded-copy.test.ts": 3996, + "packages/acp/tests/native-launch.test.ts": 4703, + "packages/acp/tests/permission-bridge.test.ts": 3504, + "packages/acp/tests/provider.test.ts": 4604, + "packages/acp/tests/serial-queue.test.ts": 1383, + "packages/acp/tests/session-key.test.ts": 3417, + "packages/acp/tests/session-route.test.ts": 3642, + "packages/acp/tests/terminal-screen.test.ts": 1467, + "packages/cli/tests/agent-adapters.test.ts": 4822, + "packages/cli/tests/agent-cli.test.ts": 34852, + "packages/cli/tests/agent-config.test.ts": 3147, + "packages/cli/tests/agent-options-cli.test.ts": 14309, + "packages/cli/tests/agent-options.test.ts": 3162, + "packages/cli/tests/agent-session-coordinator.test.ts": 3791, + "packages/cli/tests/cli-help.test.ts": 12834, + "packages/cli/tests/command.test.ts": 9005, + "packages/cli/tests/compiled-upgrade.test.ts": 9187, + "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 7688, + "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 106746, + "packages/cli/tests/document-suites/props/props-markdown.test.ts": 7176, + "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 10090, + "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 8942, + "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 12701, + "packages/cli/tests/evaluate-component.test.ts": 9823, + "packages/cli/tests/evaluate-workflow.test.ts": 4862, + "packages/cli/tests/fetch-cli.test.ts": 3226, + "packages/cli/tests/file-stream.test.ts": 1584, + "packages/cli/tests/github-zero-effect.test.ts": 6274, + "packages/cli/tests/inline-cli.test.ts": 26698, + "packages/cli/tests/launch-timeout.test.ts": 11426, + "packages/cli/tests/packaged-document.test.ts": 4260, + "packages/cli/tests/plan-args.test.ts": 1382, + "packages/cli/tests/plan-cli.test.ts": 88923, + "packages/cli/tests/plan-command-document.test.ts": 8750, + "packages/cli/tests/plan-component.test.ts": 10892, + "packages/cli/tests/plan-host-acts.test.ts": 4787, + "packages/cli/tests/plan.test.ts": 14517, + "packages/cli/tests/plugin-cli.test.ts": 32667, + "packages/cli/tests/plugin-host.test.ts": 5344, + "packages/cli/tests/plugin-modules.test.ts": 1381, + "packages/cli/tests/plugin-selection.test.ts": 1373, + "packages/cli/tests/process-retention.test.ts": 5082, + "packages/cli/tests/props-cli.test.ts": 24282, + "packages/cli/tests/props-schema.test.ts": 3610, + "packages/cli/tests/props-sources.test.ts": 2011, + "packages/cli/tests/pull-request-read-fork.test.ts": 6259, + "packages/cli/tests/repl-composition.test.ts": 1823, + "packages/cli/tests/repl-execution.test.ts": 4755, + "packages/cli/tests/repl-model.test.ts": 3988, + "packages/cli/tests/repl-route.test.ts": 3896, + "packages/cli/tests/repl-terminal.test.ts": 2683, + "packages/cli/tests/run-composition-deno.test.ts": 6681, + "packages/cli/tests/run-composition-nested.test.ts": 18901, + "packages/cli/tests/run-composition.test.ts": 6238, + "packages/cli/tests/run-deadline.test.ts": 7605, + "packages/cli/tests/run-profile.test.ts": 3765, + "packages/cli/tests/run-timeouts.test.ts": 11458, + "packages/cli/tests/secret-detection-cli.test.ts": 15679, + "packages/cli/tests/service-document.test.ts": 4634, + "packages/cli/tests/service-host.test.ts": 4545, + "packages/cli/tests/session-launch-cli.test.ts": 10090, + "packages/cli/tests/stdin-cli.test.ts": 30552, + "packages/cli/tests/stdout-delivery.test.ts": 1377, + "packages/cli/tests/syntax-cli.test.ts": 30181, + "packages/cli/tests/targets-cli.test.ts": 23314, + "packages/cli/tests/test-root-profile.test.ts": 6783, + "packages/cli/tests/test-target.test.ts": 30382, + "packages/cli/tests/testing-activation.test.ts": 3995, + "packages/cli/tests/testing-execution-host.test.ts": 47616, + "packages/cli/tests/upgrade-cli.test.ts": 16284, + "packages/cli/tests/upgrade-command-document.test.ts": 6450, + "packages/cli/tests/upgrade-output.test.ts": 3951, + "packages/cli/tests/value-root.test.ts": 14567, + "packages/cli/tests/verbose-component.test.ts": 4400, + "packages/cli/tests/workflow-agent-sessions.test.ts": 18435, + "packages/cli/tests/workflow-agent.test.ts": 14865, + "packages/cli/tests/workflow-cli.test.ts": 95078, + "packages/cli/tests/workflow-crash.test.ts": 78332, + "packages/cli/tests/workflow-declaration.test.ts": 3899, + "packages/cli/tests/workflow-export-publication.test.ts": 16177, + "packages/cli/tests/workflow-fetch.test.ts": 7205, + "packages/cli/tests/workflow-fork.test.ts": 71027, + "packages/cli/tests/workflow-host.test.ts": 13119, + "packages/cli/tests/workflow-inspection.test.ts": 96455, + "packages/cli/tests/workflow-installation.test.ts": 5249, + "packages/cli/tests/workflow-lifecycle-control.test.ts": 17955, + "packages/cli/tests/workflow-retention.test.ts": 20614, + "packages/cli/tests/workflow-suspension.test.ts": 6970, + "packages/code-review-agent/tests/doctor.test.ts": 1350, + "packages/code-review-agent/tests/parse-diagnostics.test.ts": 1390, + "packages/code-review-agent/tests/parse-diff.test.ts": 1358, + "packages/code-review-agent/tests/parse-doctor.test.ts": 3524, + "packages/code-review-agent/tests/parse-oxlint.test.ts": 1375, + "packages/code-review-agent/tests/policy.test.ts": 1347, + "packages/core/tests/agent-api.test.ts": 1408, + "packages/core/tests/agent-components.test.ts": 4063, + "packages/core/tests/agent-function-components.test.ts": 5307, + "packages/core/tests/agent-session-launch.test.ts": 5840, + "packages/core/tests/agent-session-placement.test.ts": 1433, + "packages/core/tests/agent-session-use-loaded-copy.test.ts": 1455, + "packages/core/tests/agent-session-use.test.ts": 1413, + "packages/core/tests/agent-stack.test.ts": 3680, + "packages/core/tests/answer-identity.test.ts": 3169, + "packages/core/tests/answers-component.test.ts": 4212, + "packages/core/tests/answers-expansion-recursion.test.ts": 3393, + "packages/core/tests/capture-errors.test.ts": 3569, + "packages/core/tests/capture-props.test.ts": 3371, + "packages/core/tests/cli-journal.test.ts": 9912, + "packages/core/tests/code-block-component.test.ts": 4810, + "packages/core/tests/compiler-boundary.test.ts": 3363, + "packages/core/tests/component-api.test.ts": 3113, + "packages/core/tests/component-registration.test.ts": 4113, + "packages/core/tests/component-returns.test.ts": 4502, + "packages/core/tests/component-schema-conformance.test.ts": 3500, + "packages/core/tests/config-api.test.ts": 3526, + "packages/core/tests/construct-error-observation.test.ts": 3615, + "packages/core/tests/contextual-cwd.test.ts": 4650, + "packages/core/tests/daemon-integration.test.ts": 4166, + "packages/core/tests/daemon.test.ts": 3179, + "packages/core/tests/declared-markdown-component.test.ts": 4814, + "packages/core/tests/document-output-api.test.ts": 1362, + "packages/core/tests/document-target-execution.test.ts": 6032, + "packages/core/tests/document-targets.test.ts": 3566, + "packages/core/tests/document-validation.test.ts": 4876, + "packages/core/tests/documentation-index.test.ts": 3296, + "packages/core/tests/each.test.ts": 3510, + "packages/core/tests/elicit-component.test.ts": 4087, + "packages/core/tests/ephemeral-service.test.ts": 3859, + "packages/core/tests/eval-bindings.test.ts": 3481, + "packages/core/tests/eval-context.test.ts": 1930, + "packages/core/tests/eval-durable.test.ts": 3556, + "packages/core/tests/eval-error-mode.test.ts": 3742, + "packages/core/tests/eval-interpolate.test.ts": 1360, + "packages/core/tests/eval-middleware.test.ts": 3130, + "packages/core/tests/eval-persist.test.ts": 3608, + "packages/core/tests/eval-return.test.ts": 3484, + "packages/core/tests/eval-scope.test.ts": 3447, + "packages/core/tests/eval-timeout.test.ts": 3504, + "packages/core/tests/eval-transform.test.ts": 3218, + "packages/core/tests/evaluate-component.test.ts": 5249, + "packages/core/tests/evaluate-loaded-copy.test.ts": 3566, + "packages/core/tests/evaluate-provider-lifetime.test.ts": 3581, + "packages/core/tests/evaluation-profile.test.ts": 3307, + "packages/core/tests/exec-timeout.test.ts": 3535, + "packages/core/tests/execute.test.ts": 4007, + "packages/core/tests/execution-protocol.test.ts": 4649, + "packages/core/tests/expand.test.ts": 4108, + "packages/core/tests/expansion-identity.test.ts": 4041, + "packages/core/tests/expansion-metadata.test.ts": 3413, + "packages/core/tests/expression-props.test.ts": 3980, + "packages/core/tests/fail-component.test.ts": 3720, + "packages/core/tests/failure-printing.test.ts": 3659, + "packages/core/tests/fatal-cause.test.ts": 3284, + "packages/core/tests/fetch-component.test.ts": 4673, + "packages/core/tests/file-component.test.ts": 5289, + "packages/core/tests/file-delete-component.test.ts": 3630, + "packages/core/tests/files-fatal.test.ts": 4348, + "packages/core/tests/foreground-exec.test.ts": 4296, + "packages/core/tests/frontmatter.test.ts": 3181, + "packages/core/tests/function-components.test.ts": 3910, + "packages/core/tests/generated-composition.test.ts": 3965, + "packages/core/tests/generated-xmd.test.ts": 5249, + "packages/core/tests/glob-api.test.ts": 1703, + "packages/core/tests/glob-component.test.ts": 4091, + "packages/core/tests/guarded-journal.test.ts": 3461, + "packages/core/tests/has-content.test.ts": 3307, + "packages/core/tests/heal.test.ts": 3335, + "packages/core/tests/if.test.ts": 3870, + "packages/core/tests/inline-root.test.ts": 3506, + "packages/core/tests/invocation-failures.test.ts": 3231, + "packages/core/tests/invocation-identity.test.ts": 3841, + "packages/core/tests/invocation-scope.test.ts": 3560, + "packages/core/tests/journal-source-position.test.ts": 3626, + "packages/core/tests/json-component.test.ts": 3650, + "packages/core/tests/json.test.ts": 3147, + "packages/core/tests/let.test.ts": 3507, + "packages/core/tests/loaded-copy-files.test.ts": 3398, + "packages/core/tests/loop.test.ts": 4242, + "packages/core/tests/named-slots.test.ts": 4184, + "packages/core/tests/output-error-mode.test.ts": 4420, + "packages/core/tests/output-normalize.test.ts": 1366, + "packages/core/tests/output-terminal.test.ts": 1461, + "packages/core/tests/parse-components.test.ts": 3611, + "packages/core/tests/plan-response.test.ts": 1407, + "packages/core/tests/plugin-api.test.ts": 3190, + "packages/core/tests/plugin.test.ts": 3272, + "packages/core/tests/props-binding.test.ts": 3585, + "packages/core/tests/protected-content.test.ts": 3135, + "packages/core/tests/registered-printed-errors.test.ts": 3300, + "packages/core/tests/replay-stream.test.ts": 1345, + "packages/core/tests/retain.test.ts": 3494, + "packages/core/tests/root-composition.test.ts": 3529, + "packages/core/tests/root-props.test.ts": 3588, + "packages/core/tests/root-provider.test.ts": 3522, + "packages/core/tests/sample-component.test.ts": 4207, + "packages/core/tests/scanner.test.ts": 3193, + "packages/core/tests/scope-local.test.ts": 1345, + "packages/core/tests/secret-detection.test.ts": 4135, + "packages/core/tests/secret-files.test.ts": 1807, + "packages/core/tests/secret-rules.test.ts": 1826, + "packages/core/tests/secret-scanner-baseline.test.ts": 1583, + "packages/core/tests/secret-scanner.test.ts": 1698, + "packages/core/tests/source-position.test.ts": 3353, + "packages/core/tests/state-ownership.test.ts": 3390, + "packages/core/tests/streaming-emission.test.ts": 3389, + "packages/core/tests/switch.test.ts": 3887, + "packages/core/tests/syntax-catalog.test.ts": 3913, + "packages/core/tests/syntax-component.test.ts": 4609, + "packages/core/tests/syntax-loaded-copy.test.ts": 3505, + "packages/core/tests/temp-dir.test.ts": 5795, + "packages/core/tests/temp-file-compiler.test.ts": 1861, + "packages/core/tests/text-interpolation.test.ts": 3310, + "packages/core/tests/try-content.test.ts": 3296, + "packages/core/tests/unused-in-diff.test.ts": 3528, + "packages/core/tests/validate.test.ts": 3352, + "packages/core/tests/validation-integration.test.ts": 3414, + "packages/core/tests/workflow-component-bundle.test.ts": 3634, + "packages/durable-streams/tests/context.test.ts": 1576, + "packages/durable-streams/tests/deterministic-id.test.ts": 1600, + "packages/durable-streams/tests/divergence-api.test.ts": 1606, + "packages/durable-streams/tests/divergence.test.ts": 1638, + "packages/durable-streams/tests/durable-each.test.ts": 1650, + "packages/durable-streams/tests/durable-position.test.ts": 1600, + "packages/durable-streams/tests/durable-run.test.ts": 1625, + "packages/durable-streams/tests/ephemeral.test.ts": 1620, + "packages/durable-streams/tests/fail-stop.test.ts": 1644, + "packages/durable-streams/tests/guard-stream.test.ts": 1678, + "packages/durable-streams/tests/live-coordinator.test.ts": 1627, + "packages/durable-streams/tests/parse.test.ts": 1611, + "packages/durable-streams/tests/replay-guard.test.ts": 1678, + "packages/durable-streams/tests/replay-index.test.ts": 1424, + "packages/durable-streams/tests/retained.test.ts": 1456, + "packages/durable-streams/tests/serialize-event.test.ts": 1595, + "packages/durable-streams/tests/smoke.test.ts": 1580, + "packages/durable-streams/tests/structured-concurrency.test.ts": 1706, + "packages/durable-streams/tests/terminal-boundary.test.ts": 1663, + "packages/durable-streams/tests/types.test.ts": 1304, + "packages/git/tests/ambient-authentication.test.ts": 22496, + "packages/git/tests/api-entrypoint.test.ts": 4301, + "packages/git/tests/credential-helper.test.ts": 5315, + "packages/git/tests/git-add-crash.test.ts": 6108, + "packages/git/tests/git-add-durability.test.ts": 23187, + "packages/git/tests/git-add.test.ts": 30526, + "packages/git/tests/git-commit-crash.test.ts": 6315, + "packages/git/tests/git-commit-durability.test.ts": 29041, + "packages/git/tests/git-commit.test.ts": 34181, + "packages/git/tests/git-host-effect.test.ts": 8105, + "packages/git/tests/git-push-crash.test.ts": 8524, + "packages/git/tests/git-push-durability.test.ts": 35860, + "packages/git/tests/git-push.test.ts": 41808, + "packages/git/tests/git-switch-crash.test.ts": 6124, + "packages/git/tests/git-switch-durability.test.ts": 18852, + "packages/git/tests/git-switch.test.ts": 19532, + "packages/git/tests/git.test.ts": 3879, + "packages/git/tests/github-activation.test.ts": 4893, + "packages/git/tests/github-issues.test.ts": 3848, + "packages/git/tests/github-pull-requests.test.ts": 3543, + "packages/git/tests/github-workflow-activation.test.ts": 5247, + "packages/git/tests/issue-markdown.test.ts": 10253, + "packages/git/tests/issue-records.test.ts": 3746, + "packages/git/tests/materialization.test.ts": 4410, + "packages/git/tests/module-partition.test.ts": 4585, + "packages/git/tests/plugin.test.ts": 4163, + "packages/git/tests/provider-neutrality.test.ts": 2991, + "packages/git/tests/public-entrypoint.test.ts": 7837, + "packages/git/tests/pull-request-crash.test.ts": 7172, + "packages/git/tests/pull-request-durability.test.ts": 34453, + "packages/git/tests/pull-request-read.test.ts": 7415, + "packages/git/tests/pull-request-records.test.ts": 3776, + "packages/git/tests/pull-request.test.ts": 61232, + "packages/git/tests/repository-components.test.ts": 11337, + "packages/git/tests/repository-control-plane.test.ts": 10024, + "packages/git/tests/repository-materialization.test.ts": 7531, + "packages/git/tests/repository-replay.test.ts": 14210, + "packages/git/tests/repository-storage.test.ts": 17123, + "packages/git/tests/run-composition-ambient.test.ts": 11137, + "packages/git/tests/run-composition-lazy.test.ts": 5053, + "packages/git/tests/run-composition-managed.test.ts": 12698, + "packages/git/tests/run-composition-remote.test.ts": 12758, + "packages/git/tests/selection-authentication.test.ts": 1635, + "packages/git/tests/worktree-replay.test.ts": 6088, + "packages/runtime/tests/agent-session-coordinator.test.ts": 1930, + "packages/runtime/tests/duration.test.ts": 1367, + "packages/runtime/tests/executable-observer.test.ts": 1655, + "packages/runtime/tests/fetch.test.ts": 2173, + "packages/runtime/tests/fs.test.ts": 1553, + "packages/runtime/tests/host-files.test.ts": 2332, + "packages/runtime/tests/native-launcher.test.ts": 3774, + "packages/runtime/tests/process.test.ts": 1538, + "packages/runtime/tests/service.test.ts": 1662, + "packages/test-agent/tests/acp-server.test.ts": 1495, + "packages/test-agent/tests/behavior-engine.test.ts": 3766, + "packages/test-agent/tests/bridge.test.ts": 3279, + "packages/test-agent/tests/command-lazy.test.ts": 7029, + "packages/test-agent/tests/components.test.ts": 52910, + "packages/test-agent/tests/controller.test.ts": 2587, + "packages/test-agent/tests/cross-package-resolution.test.ts": 5474, + "packages/test-agent/tests/native-launch.test.ts": 25742, + "packages/test-agent/tests/net.test.ts": 1467, + "packages/test-agent/tests/profile.test.ts": 3508, + "packages/test-agent/tests/protocol.test.ts": 2226, + "packages/test-agent/tests/provider.test.ts": 3817, + "packages/test-agent/tests/public-api.test.ts": 4500, + "packages/test-agent/tests/route-slot.test.ts": 1372, + "packages/test-agent/tests/smoke.test.ts": 18550, + "packages/test-agent/tests/template.test.ts": 3469, + "packages/test-agent/tests/worker-lifecycle.test.ts": 11058, + "packages/test-support/tests/journal.test.ts": 1615, + "packages/testing/tests/agent-composition.test.ts": 3843, + "packages/testing/tests/assert-throws.test.ts": 5007, + "packages/testing/tests/assert.test.ts": 1407, + "packages/testing/tests/assertions.test.ts": 5541, + "packages/testing/tests/boundary-contract.test.ts": 4575, + "packages/testing/tests/cli.test.ts": 5696, + "packages/testing/tests/execution-harness.test.ts": 8139, + "packages/testing/tests/replay.test.ts": 4940, + "packages/testing/tests/smoke.test.ts": 5117, + "packages/testing/tests/test-component.test.ts": 5579, + "packages/testing/tests/testing-activation.test.ts": 5204, + "packages/testing/tests/testing-boundary.test.ts": 4126, + "packages/testing/tests/testing-mode.test.ts": 5629, + "packages/testing/tests/use-testing.test.ts": 4696, + "packages/web/tests/assets.test.ts": 1438, + "packages/web/tests/client-logic.test.ts": 1487, + "packages/web/tests/compile.test.ts": 4113, + "packages/web/tests/component.test.ts": 4592, + "packages/web/tests/declaration.test.ts": 3930, + "packages/web/tests/document.test.ts": 4128, + "packages/web/tests/elicitation.test.ts": 3903, + "packages/web/tests/live-form.test.ts": 3831, + "packages/web/tests/markdown.test.ts": 1561, + "packages/web/tests/opener.test.ts": 3788, + "packages/web/tests/page.test.ts": 1420, + "packages/web/tests/responder.test.ts": 4048, + "packages/web/tests/server-lifecycle.test.ts": 3937, + "packages/web/tests/server.test.ts": 4322, + "packages/workflow/tests/generated-agent-component.test.ts": 6153, + "packages/workflow/tests/generated-observations.test.ts": 3674, + "packages/workflow/tests/public-entrypoint.test.ts": 5334, + "packages/workflow/tests/retained-run.test.ts": 4451, + "packages/workflow/tests/service-denial.test.ts": 1668, + "packages/workflow/tests/workflow-agent-checkpoints.test.ts": 5434, + "packages/workflow/tests/workflow-agent-sessions.test.ts": 4432, + "packages/workflow/tests/workflow-bundle.test.ts": 3963, + "packages/workflow/tests/workflow-checkpoint.test.ts": 5425, + "packages/workflow/tests/workflow-definition.test.ts": 3662, + "packages/workflow/tests/workflow-export.test.ts": 6869, + "packages/workflow/tests/workflow-fork-source.test.ts": 4445, + "packages/workflow/tests/workflow-fork.test.ts": 3552, + "packages/workflow/tests/workflow-lifecycle-authority.test.ts": 5209, + "packages/workflow/tests/workflow-lifecycle-control.test.ts": 5737, + "packages/workflow/tests/workflow-lifecycle-inspection.test.ts": 10270, + "packages/workflow/tests/workflow-run-journal.test.ts": 6295, + "packages/workflow/tests/workflow-run-storage.test.ts": 7706, + "packages/workflow/tests/workflow-run.test.ts": 4308, + "packages/workflow/tests/workflow-suspension-answer.test.ts": 5313, + "packages/workflow/tests/workflow-suspension.test.ts": 4992, + "packages/workflow/tests/workspace-crash-recovery.test.ts": 6630, + "packages/workflow/tests/workspace-effect-loaded-copy.test.ts": 4477, + "packages/workflow/tests/workspace-effect-transaction.test.ts": 5230, + "packages/workflow/tests/workspace-effect.test.ts": 5127, + "packages/workflow/tests/workspace-files.test.ts": 11932, + "packages/workflow/tests/workspace-root-restoration.test.ts": 6018, + "packages/workflow/tests/workspace-root.test.ts": 4403, + "packages/workflow/tests/workspace-transaction.test.ts": 4469, + "packages/workflow/tests/xmd-artifact.test.ts": 6309, + "scripts/tests/acpx-vendor.test.ts": 1854, + "scripts/tests/adapter-distribution.test.ts": 3955, + "scripts/tests/adapter-npm-package.test.ts": 28449, + "scripts/tests/adapter-vendor.test.ts": 3547, + "scripts/tests/bootstrap-npm-package.test.ts": 9311, + "scripts/tests/build-npm.test.ts": 34883, + "scripts/tests/build-web-client.test.ts": 25828, + "scripts/tests/bump-version.test.ts": 1462, + "scripts/tests/changed-paths.test.ts": 1348, + "scripts/tests/ci-workflow.test.ts": 4145, + "scripts/tests/cli-npm-bin.test.ts": 202292, + "scripts/tests/cloudflare-dofs-vendor.test.ts": 3760, + "scripts/tests/consumer-cycle.test.ts": 1461, + "scripts/tests/documentation-validation.test.ts": 5282, + "scripts/tests/filesystem-contract-workflow.test.ts": 1398, + "scripts/tests/frozen-entry.test.ts": 7108, + "scripts/tests/jsr-consumer-documentation.test.ts": 16048, + "scripts/tests/main-green.test.ts": 1711, + "scripts/tests/main-health.test.ts": 1558, + "scripts/tests/measure-test-weights.test.ts": 1729, + "scripts/tests/no-module-scoped-registry.test.ts": 2111, + "scripts/tests/no-redundant-test-scope.test.ts": 5639, + "scripts/tests/no-sync-filesystem.test.ts": 5497, + "scripts/tests/no-yield-in-finally.test.ts": 2072, + "scripts/tests/oxlint-policy.test.ts": 10227, + "scripts/tests/packaged-document.test.ts": 1419, + "scripts/tests/prefer-effection-operation.test.ts": 6711, + "scripts/tests/prefer-effection-result.test.ts": 4848, + "scripts/tests/prepared-state.test.ts": 2214, + "scripts/tests/publish-workflow-generator.test.ts": 5123, + "scripts/tests/publish-workflow-membership.test.ts": 1527, + "scripts/tests/publishable-membership-agreement.test.ts": 3436, + "scripts/tests/readme-targets.test.ts": 15416, + "scripts/tests/release-targets.test.ts": 1465, + "scripts/tests/review-infrastructure.test.ts": 5411, + "scripts/tests/runtime-exclusions.test.ts": 1827, + "scripts/tests/runtime-tests.test.ts": 1526, + "scripts/tests/scope-bound-event-registration.test.ts": 5395, + "scripts/tests/shard-execution.test.ts": 10601, + "scripts/tests/staged-write.test.ts": 1446, + "scripts/tests/test-file-discovery.test.ts": 1579, + "scripts/tests/test-shards.test.ts": 1406, + "scripts/tests/test-weights.test.ts": 1665, + "scripts/tests/tracked.test.ts": 1390, + "scripts/tests/verify-adapter.test.ts": 6517, + "scripts/tests/verify-clean.test.ts": 1574, + "scripts/tests/verify-coordinator.test.ts": 1748, + "scripts/tests/version-lockstep.test.ts": 1532, + "scripts/tests/web-client-module.test.ts": 1425, + "scripts/tests/workspace.test.ts": 1352 }, "node": { - "packages/acp/tests/acceptance.test.ts": 1304, - "packages/acp/tests/acpx-checkpoint-meta.test.ts": 4546, - "packages/acp/tests/acpx-materialization.test.ts": 6716, - "packages/acp/tests/acpx-transient-env.test.ts": 495, - "packages/acp/tests/adapter-materialization.test.ts": 48474, - "packages/acp/tests/adapter-protocol.test.ts": 33278, - "packages/acp/tests/codex-surfaces.test.ts": 388, - "packages/acp/tests/native-launch.test.ts": 1361, - "packages/acp/tests/permission-bridge.test.ts": 696, - "packages/acp/tests/provider.test.ts": 1407, - "packages/acp/tests/serial-queue.test.ts": 320, - "packages/acp/tests/session-key.test.ts": 341, - "packages/acp/tests/terminal-screen.test.ts": 377, - "packages/cli/tests/agent-adapters.test.ts": 1483, - "packages/cli/tests/agent-cli.test.ts": 23455, - "packages/cli/tests/agent-config.test.ts": 295, - "packages/cli/tests/agent-options-cli.test.ts": 9063, - "packages/cli/tests/agent-options.test.ts": 300, - "packages/cli/tests/agent-session-coordinator.test.ts": 818, - "packages/cli/tests/cli-help.test.ts": 16739, - "packages/cli/tests/command.test.ts": 9476, - "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 2429, - "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 120309, - "packages/cli/tests/document-suites/props/props-markdown.test.ts": 2141, - "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 5882, - "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 3614, - "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 10297, - "packages/cli/tests/evaluate-component.test.ts": 8585, - "packages/cli/tests/fetch-cli.test.ts": 2900, - "packages/cli/tests/file-stream.test.ts": 335, - "packages/cli/tests/inline-cli.test.ts": 38091, - "packages/cli/tests/launch-timeout.test.ts": 20363, - "packages/cli/tests/packaged-document.test.ts": 932, - "packages/cli/tests/plan-args.test.ts": 333, - "packages/cli/tests/plan-cli.test.ts": 67983, - "packages/cli/tests/plan-command-document.test.ts": 5120, - "packages/cli/tests/plan-component.test.ts": 7232, - "packages/cli/tests/plan-host-acts.test.ts": 1508, - "packages/cli/tests/plan.test.ts": 10598, - "packages/cli/tests/plugin-cli.test.ts": 49674, - "packages/cli/tests/plugin-host.test.ts": 1937, - "packages/cli/tests/plugin-modules.test.ts": 351, - "packages/cli/tests/plugin-selection.test.ts": 348, - "packages/cli/tests/process-retention.test.ts": 2984, - "packages/cli/tests/props-cli.test.ts": 38353, - "packages/cli/tests/props-schema.test.ts": 892, - "packages/cli/tests/props-sources.test.ts": 447, - "packages/cli/tests/repl-composition.test.ts": 489, - "packages/cli/tests/repl-execution.test.ts": 1968, - "packages/cli/tests/repl-model.test.ts": 1198, - "packages/cli/tests/repl-route.test.ts": 1077, - "packages/cli/tests/run-composition.test.ts": 2446, - "packages/cli/tests/run-deadline.test.ts": 2785, - "packages/cli/tests/run-profile.test.ts": 772, - "packages/cli/tests/run-timeouts.test.ts": 13623, - "packages/cli/tests/secret-detection-cli.test.ts": 22616, - "packages/cli/tests/service-document.test.ts": 1590, - "packages/cli/tests/service-host.test.ts": 1455, - "packages/cli/tests/session-launch-cli.test.ts": 6415, - "packages/cli/tests/stdin-cli.test.ts": 37484, - "packages/cli/tests/stdout-delivery.test.ts": 355, - "packages/cli/tests/syntax-cli.test.ts": 35401, - "packages/cli/tests/targets-cli.test.ts": 28067, - "packages/cli/tests/test-root-profile.test.ts": 4004, - "packages/cli/tests/test-target.test.ts": 41290, - "packages/cli/tests/testing-activation.test.ts": 845, - "packages/cli/tests/testing-execution-host.test.ts": 53218, - "packages/cli/tests/upgrade-cli.test.ts": 25171, - "packages/cli/tests/upgrade-command-document.test.ts": 3217, - "packages/cli/tests/upgrade-output.test.ts": 1048, - "packages/cli/tests/value-root.test.ts": 16692, - "packages/cli/tests/verbose-component.test.ts": 1059, - "packages/cli/tests/workflow-declaration.test.ts": 823, - "packages/cli/tests/workflow-host.test.ts": 5691, - "packages/code-review-agent/tests/doctor.test.ts": 294, - "packages/code-review-agent/tests/parse-diagnostics.test.ts": 311, - "packages/code-review-agent/tests/parse-diff.test.ts": 302, - "packages/code-review-agent/tests/parse-doctor.test.ts": 661, - "packages/code-review-agent/tests/parse-oxlint.test.ts": 289, - "packages/code-review-agent/tests/policy.test.ts": 298, - "packages/core/tests/agent-api.test.ts": 304, - "packages/core/tests/agent-components.test.ts": 1138, - "packages/core/tests/agent-function-components.test.ts": 2069, - "packages/core/tests/agent-session-launch.test.ts": 2607, - "packages/core/tests/agent-session-placement.test.ts": 309, - "packages/core/tests/agent-session-use.test.ts": 297, - "packages/core/tests/agent-stack.test.ts": 852, - "packages/core/tests/answer-identity.test.ts": 313, - "packages/core/tests/answers-component.test.ts": 1370, - "packages/core/tests/answers-expansion-recursion.test.ts": 580, - "packages/core/tests/capture-errors.test.ts": 710, - "packages/core/tests/capture-props.test.ts": 555, - "packages/core/tests/cli-journal.test.ts": 11814, - "packages/core/tests/code-block-component.test.ts": 1712, - "packages/core/tests/compiler-boundary.test.ts": 648, - "packages/core/tests/component-api.test.ts": 332, - "packages/core/tests/component-registration.test.ts": 1156, - "packages/core/tests/component-returns.test.ts": 1606, - "packages/core/tests/component-schema-conformance.test.ts": 661, - "packages/core/tests/config-api.test.ts": 726, - "packages/core/tests/construct-error-observation.test.ts": 750, - "packages/core/tests/contextual-cwd.test.ts": 1808, - "packages/core/tests/daemon-integration.test.ts": 1319, - "packages/core/tests/daemon.test.ts": 335, - "packages/core/tests/declared-markdown-component.test.ts": 1651, - "packages/core/tests/document-output-api.test.ts": 298, - "packages/core/tests/document-target-execution.test.ts": 2539, - "packages/core/tests/document-targets.test.ts": 671, - "packages/core/tests/document-validation.test.ts": 1711, - "packages/core/tests/documentation-index.test.ts": 535, - "packages/core/tests/each.test.ts": 730, - "packages/core/tests/elicit-component.test.ts": 1222, - "packages/core/tests/ephemeral-service.test.ts": 1037, - "packages/core/tests/eval-bindings.test.ts": 719, - "packages/core/tests/eval-context.test.ts": 364, - "packages/core/tests/eval-durable.test.ts": 746, - "packages/core/tests/eval-error-mode.test.ts": 975, - "packages/core/tests/eval-interpolate.test.ts": 303, - "packages/core/tests/eval-middleware.test.ts": 355, - "packages/core/tests/eval-persist.test.ts": 804, - "packages/core/tests/eval-return.test.ts": 779, - "packages/core/tests/eval-scope.test.ts": 666, - "packages/core/tests/eval-timeout.test.ts": 718, - "packages/core/tests/eval-transform.test.ts": 331, - "packages/core/tests/evaluate-component.test.ts": 2027, - "packages/core/tests/evaluate-loaded-copy.test.ts": 765, - "packages/core/tests/evaluate-provider-lifetime.test.ts": 815, - "packages/core/tests/evaluation-profile.test.ts": 488, - "packages/core/tests/exec-timeout.test.ts": 762, - "packages/core/tests/execute.test.ts": 1100, - "packages/core/tests/execution-protocol.test.ts": 1541, - "packages/core/tests/expand.test.ts": 1106, - "packages/core/tests/expansion-identity.test.ts": 1223, - "packages/core/tests/expansion-metadata.test.ts": 569, - "packages/core/tests/expression-props.test.ts": 1063, - "packages/core/tests/fail-component.test.ts": 990, - "packages/core/tests/failure-printing.test.ts": 810, - "packages/core/tests/fatal-cause.test.ts": 508, - "packages/core/tests/fetch-component.test.ts": 1696, - "packages/core/tests/file-component.test.ts": 2324, - "packages/core/tests/file-delete-component.test.ts": 870, - "packages/core/tests/files-fatal.test.ts": 1359, - "packages/core/tests/foreground-exec.test.ts": 1347, - "packages/core/tests/frontmatter.test.ts": 332, - "packages/core/tests/function-components.test.ts": 1158, - "packages/core/tests/generated-composition.test.ts": 1158, - "packages/core/tests/generated-xmd.test.ts": 2048, - "packages/core/tests/glob-api.test.ts": 357, - "packages/core/tests/glob-component.test.ts": 1277, - "packages/core/tests/guarded-journal.test.ts": 775, - "packages/core/tests/has-content.test.ts": 557, - "packages/core/tests/heal.test.ts": 548, - "packages/core/tests/if.test.ts": 1069, - "packages/core/tests/inline-root.test.ts": 771, - "packages/core/tests/invocation-failures.test.ts": 363, - "packages/core/tests/invocation-identity.test.ts": 991, - "packages/core/tests/invocation-scope.test.ts": 706, - "packages/core/tests/journal-source-position.test.ts": 845, - "packages/core/tests/json-component.test.ts": 873, - "packages/core/tests/json.test.ts": 303, - "packages/core/tests/let.test.ts": 737, - "packages/core/tests/loop.test.ts": 1369, - "packages/core/tests/named-slots.test.ts": 1343, - "packages/core/tests/output-error-mode.test.ts": 1455, - "packages/core/tests/output-normalize.test.ts": 316, - "packages/core/tests/output-terminal.test.ts": 396, - "packages/core/tests/parse-components.test.ts": 808, - "packages/core/tests/plan-response.test.ts": 347, - "packages/core/tests/plugin-api.test.ts": 325, - "packages/core/tests/plugin.test.ts": 565, - "packages/core/tests/props-binding.test.ts": 810, - "packages/core/tests/protected-content.test.ts": 332, - "packages/core/tests/registered-printed-errors.test.ts": 494, - "packages/core/tests/replay-stream.test.ts": 292, - "packages/core/tests/retain.test.ts": 670, - "packages/core/tests/root-composition.test.ts": 792, - "packages/core/tests/root-props.test.ts": 829, - "packages/core/tests/root-provider.test.ts": 791, - "packages/core/tests/sample-component.test.ts": 1342, - "packages/core/tests/scanner.test.ts": 347, - "packages/core/tests/scope-local.test.ts": 292, - "packages/core/tests/secret-detection.test.ts": 1295, - "packages/core/tests/secret-files.test.ts": 444, - "packages/core/tests/secret-rules.test.ts": 489, - "packages/core/tests/secret-scanner-baseline.test.ts": 442, - "packages/core/tests/secret-scanner.test.ts": 409, - "packages/core/tests/source-position.test.ts": 647, - "packages/core/tests/state-ownership.test.ts": 680, - "packages/core/tests/streaming-emission.test.ts": 658, - "packages/core/tests/switch.test.ts": 1053, - "packages/core/tests/syntax-catalog.test.ts": 1021, - "packages/core/tests/syntax-component.test.ts": 1636, - "packages/core/tests/temp-dir.test.ts": 2979, - "packages/core/tests/temp-file-compiler.test.ts": 328, - "packages/core/tests/text-interpolation.test.ts": 518, - "packages/core/tests/try-content.test.ts": 532, - "packages/core/tests/unused-in-diff.test.ts": 788, - "packages/core/tests/validate.test.ts": 474, - "packages/core/tests/validation-integration.test.ts": 746, - "packages/core/tests/workflow-component-bundle.test.ts": 922, - "packages/durable-streams/tests/context.test.ts": 317, - "packages/durable-streams/tests/deterministic-id.test.ts": 333, - "packages/durable-streams/tests/divergence-api.test.ts": 329, - "packages/durable-streams/tests/divergence.test.ts": 337, - "packages/durable-streams/tests/durable-each.test.ts": 344, - "packages/durable-streams/tests/durable-position.test.ts": 342, - "packages/durable-streams/tests/durable-run.test.ts": 353, - "packages/durable-streams/tests/ephemeral.test.ts": 384, - "packages/durable-streams/tests/fail-stop.test.ts": 354, - "packages/durable-streams/tests/guard-stream.test.ts": 381, - "packages/durable-streams/tests/live-coordinator.test.ts": 342, - "packages/durable-streams/tests/parse.test.ts": 351, - "packages/durable-streams/tests/replay-guard.test.ts": 356, - "packages/durable-streams/tests/replay-index.test.ts": 334, - "packages/durable-streams/tests/retained.test.ts": 326, - "packages/durable-streams/tests/serialize-event.test.ts": 342, - "packages/durable-streams/tests/smoke.test.ts": 325, - "packages/durable-streams/tests/structured-concurrency.test.ts": 361, - "packages/durable-streams/tests/terminal-boundary.test.ts": 349, - "packages/durable-streams/tests/types.test.ts": 303, - "packages/git/tests/api-entrypoint.test.ts": 769, - "packages/git/tests/git-host-effect.test.ts": 3784, - "packages/git/tests/git.test.ts": 798, - "packages/git/tests/github-activation.test.ts": 961, - "packages/git/tests/github-issues.test.ts": 707, - "packages/git/tests/github-pull-requests.test.ts": 366, - "packages/git/tests/issue-markdown.test.ts": 5401, - "packages/git/tests/issue-records.test.ts": 687, - "packages/git/tests/module-partition.test.ts": 1744, - "packages/git/tests/plugin.test.ts": 966, - "packages/git/tests/provider-neutrality.test.ts": 1305, - "packages/git/tests/pull-request-records.test.ts": 715, - "packages/git/tests/run-composition-lazy.test.ts": 1116, - "packages/git/tests/selection-authentication.test.ts": 301, - "packages/runtime/tests/duration.test.ts": 291, - "packages/runtime/tests/fetch.test.ts": 901, - "packages/runtime/tests/fs.test.ts": 339, - "packages/runtime/tests/host-files.test.ts": 1003, - "packages/runtime/tests/native-launcher.test.ts": 2661, - "packages/runtime/tests/process.test.ts": 351, - "packages/runtime/tests/service.test.ts": 342, - "packages/test-agent/tests/acp-server.test.ts": 408, - "packages/test-agent/tests/behavior-engine.test.ts": 936, - "packages/test-agent/tests/bridge.test.ts": 338, - "packages/test-agent/tests/command-lazy.test.ts": 4134, - "packages/test-agent/tests/components.test.ts": 65382, - "packages/test-agent/tests/controller.test.ts": 468, - "packages/test-agent/tests/cross-package-resolution.test.ts": 1671, - "packages/test-agent/tests/native-launch.test.ts": 30997, - "packages/test-agent/tests/net.test.ts": 363, - "packages/test-agent/tests/profile.test.ts": 672, - "packages/test-agent/tests/protocol.test.ts": 342, - "packages/test-agent/tests/provider.test.ts": 761, - "packages/test-agent/tests/public-api.test.ts": 877, - "packages/test-agent/tests/route-slot.test.ts": 307, - "packages/test-agent/tests/smoke.test.ts": 18154, - "packages/test-agent/tests/template.test.ts": 673, - "packages/test-agent/tests/worker-lifecycle.test.ts": 12945, - "packages/test-support/tests/journal.test.ts": 316, - "packages/testing/tests/agent-composition.test.ts": 852, - "packages/testing/tests/assert-throws.test.ts": 1760, - "packages/testing/tests/assert.test.ts": 310, - "packages/testing/tests/assertions.test.ts": 2104, - "packages/testing/tests/boundary-contract.test.ts": 1409, - "packages/testing/tests/cli.test.ts": 5888, - "packages/testing/tests/execution-harness.test.ts": 4042, - "packages/testing/tests/replay.test.ts": 1748, - "packages/testing/tests/smoke.test.ts": 1874, - "packages/testing/tests/test-component.test.ts": 2444, - "packages/testing/tests/testing-activation.test.ts": 2000, - "packages/testing/tests/testing-boundary.test.ts": 1160, - "packages/testing/tests/testing-mode.test.ts": 2336, - "packages/testing/tests/use-testing.test.ts": 1506, - "packages/web/tests/assets.test.ts": 318, - "packages/web/tests/client-logic.test.ts": 363, - "packages/web/tests/compile.test.ts": 1124, - "packages/web/tests/component.test.ts": 1474, - "packages/web/tests/declaration.test.ts": 940, - "packages/web/tests/document.test.ts": 1030, - "packages/web/tests/elicitation.test.ts": 892, - "packages/web/tests/live-form.test.ts": 831, - "packages/web/tests/markdown.test.ts": 396, - "packages/web/tests/opener.test.ts": 827, - "packages/web/tests/page.test.ts": 295, - "packages/web/tests/responder.test.ts": 1040, - "packages/web/tests/server-lifecycle.test.ts": 1014, - "packages/web/tests/server.test.ts": 1192, - "packages/workflow/tests/generated-observations.test.ts": 830, - "packages/workflow/tests/retained-run.test.ts": 1263, - "packages/workflow/tests/service-denial.test.ts": 319, - "packages/workflow/tests/workflow-bundle.test.ts": 945, - "packages/workflow/tests/workflow-definition.test.ts": 739, - "packages/workflow/tests/workflow-fork.test.ts": 677, - "packages/workflow/tests/workflow-run.test.ts": 1245, - "packages/workflow/tests/workspace-effect.test.ts": 1700, - "packages/workflow/tests/xmd-artifact.test.ts": 2045, - "scripts/tests/adapter-vendor.test.ts": 435, - "scripts/tests/bootstrap-npm-package.test.ts": 5289, - "scripts/tests/bump-version.test.ts": 440, - "scripts/tests/changed-paths.test.ts": 316, - "scripts/tests/ci-workflow.test.ts": 2291, - "scripts/tests/consumer-cycle.test.ts": 445, - "scripts/tests/filesystem-contract-workflow.test.ts": 382, - "scripts/tests/main-green.test.ts": 546, - "scripts/tests/main-health.test.ts": 367, - "scripts/tests/no-module-scoped-registry.test.ts": 807, - "scripts/tests/no-redundant-test-scope.test.ts": 3255, - "scripts/tests/no-sync-filesystem.test.ts": 3364, - "scripts/tests/no-yield-in-finally.test.ts": 797, - "scripts/tests/oxlint-policy.test.ts": 6473, - "scripts/tests/packaged-document.test.ts": 354, - "scripts/tests/prefer-effection-operation.test.ts": 3848, - "scripts/tests/prefer-effection-result.test.ts": 2779, - "scripts/tests/publish-workflow-membership.test.ts": 392, - "scripts/tests/review-infrastructure.test.ts": 1838, - "scripts/tests/runtime-exclusions.test.ts": 573, - "scripts/tests/runtime-tests.test.ts": 333, - "scripts/tests/scope-bound-event-registration.test.ts": 3197, - "scripts/tests/staged-write.test.ts": 334, - "scripts/tests/test-file-discovery.test.ts": 480, - "scripts/tests/test-shards.test.ts": 314, - "scripts/tests/test-weights.test.ts": 467, - "scripts/tests/tracked.test.ts": 320, - "scripts/tests/verify-coordinator.test.ts": 488, - "scripts/tests/version-lockstep.test.ts": 413, - "scripts/tests/web-client-module.test.ts": 327, - "scripts/tests/workspace.test.ts": 311 + "packages/acp/tests/acceptance.test.ts": 1938, + "packages/acp/tests/acpx-checkpoint-meta.test.ts": 4778, + "packages/acp/tests/acpx-materialization.test.ts": 7002, + "packages/acp/tests/acpx-transient-env.test.ts": 729, + "packages/acp/tests/adapter-materialization.test.ts": 72138, + "packages/acp/tests/adapter-protocol.test.ts": 50348, + "packages/acp/tests/codex-surfaces.test.ts": 594, + "packages/acp/tests/native-launch.test.ts": 1960, + "packages/acp/tests/permission-bridge.test.ts": 1103, + "packages/acp/tests/provider.test.ts": 1931, + "packages/acp/tests/serial-queue.test.ts": 448, + "packages/acp/tests/session-key.test.ts": 477, + "packages/acp/tests/terminal-screen.test.ts": 550, + "packages/cli/tests/agent-adapters.test.ts": 2170, + "packages/cli/tests/agent-cli.test.ts": 35674, + "packages/cli/tests/agent-config.test.ts": 445, + "packages/cli/tests/agent-options-cli.test.ts": 13495, + "packages/cli/tests/agent-options.test.ts": 445, + "packages/cli/tests/agent-session-coordinator.test.ts": 1178, + "packages/cli/tests/cli-help.test.ts": 25826, + "packages/cli/tests/command.test.ts": 13058, + "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 3789, + "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 152031, + "packages/cli/tests/document-suites/props/props-markdown.test.ts": 3190, + "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 8329, + "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 5469, + "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 15256, + "packages/cli/tests/evaluate-component.test.ts": 13048, + "packages/cli/tests/fetch-cli.test.ts": 4161, + "packages/cli/tests/file-stream.test.ts": 489, + "packages/cli/tests/inline-cli.test.ts": 56000, + "packages/cli/tests/launch-timeout.test.ts": 20499, + "packages/cli/tests/packaged-document.test.ts": 1361, + "packages/cli/tests/plan-args.test.ts": 464, + "packages/cli/tests/plan-cli.test.ts": 98200, + "packages/cli/tests/plan-command-document.test.ts": 7235, + "packages/cli/tests/plan-component.test.ts": 9842, + "packages/cli/tests/plan-host-acts.test.ts": 2046, + "packages/cli/tests/plan.test.ts": 14362, + "packages/cli/tests/plugin-cli.test.ts": 68128, + "packages/cli/tests/plugin-host.test.ts": 2999, + "packages/cli/tests/plugin-modules.test.ts": 438, + "packages/cli/tests/plugin-selection.test.ts": 463, + "packages/cli/tests/process-retention.test.ts": 4143, + "packages/cli/tests/props-cli.test.ts": 52873, + "packages/cli/tests/props-schema.test.ts": 1178, + "packages/cli/tests/props-sources.test.ts": 629, + "packages/cli/tests/repl-composition.test.ts": 672, + "packages/cli/tests/repl-execution.test.ts": 2560, + "packages/cli/tests/repl-model.test.ts": 1692, + "packages/cli/tests/repl-route.test.ts": 1522, + "packages/cli/tests/repl-terminal.test.ts": 1196, + "packages/cli/tests/run-composition.test.ts": 3677, + "packages/cli/tests/run-deadline.test.ts": 3354, + "packages/cli/tests/run-profile.test.ts": 1115, + "packages/cli/tests/run-timeouts.test.ts": 17659, + "packages/cli/tests/secret-detection-cli.test.ts": 31779, + "packages/cli/tests/service-document.test.ts": 2391, + "packages/cli/tests/service-host.test.ts": 2125, + "packages/cli/tests/session-launch-cli.test.ts": 9257, + "packages/cli/tests/stdin-cli.test.ts": 53126, + "packages/cli/tests/stdout-delivery.test.ts": 458, + "packages/cli/tests/syntax-cli.test.ts": 52374, + "packages/cli/tests/targets-cli.test.ts": 42219, + "packages/cli/tests/test-root-profile.test.ts": 5964, + "packages/cli/tests/test-target.test.ts": 60898, + "packages/cli/tests/testing-activation.test.ts": 1273, + "packages/cli/tests/testing-execution-host.test.ts": 77038, + "packages/cli/tests/upgrade-cli.test.ts": 37529, + "packages/cli/tests/upgrade-command-document.test.ts": 4894, + "packages/cli/tests/upgrade-output.test.ts": 1615, + "packages/cli/tests/value-root.test.ts": 26199, + "packages/cli/tests/verbose-component.test.ts": 1659, + "packages/cli/tests/workflow-declaration.test.ts": 1279, + "packages/cli/tests/workflow-host.test.ts": 8807, + "packages/code-review-agent/tests/doctor.test.ts": 427, + "packages/code-review-agent/tests/parse-diagnostics.test.ts": 479, + "packages/code-review-agent/tests/parse-diff.test.ts": 455, + "packages/code-review-agent/tests/parse-doctor.test.ts": 1053, + "packages/code-review-agent/tests/parse-oxlint.test.ts": 448, + "packages/code-review-agent/tests/policy.test.ts": 448, + "packages/core/tests/agent-api.test.ts": 450, + "packages/core/tests/agent-components.test.ts": 1797, + "packages/core/tests/agent-function-components.test.ts": 3393, + "packages/core/tests/agent-session-launch.test.ts": 4061, + "packages/core/tests/agent-session-placement.test.ts": 462, + "packages/core/tests/agent-session-use.test.ts": 441, + "packages/core/tests/agent-stack.test.ts": 1342, + "packages/core/tests/answer-identity.test.ts": 471, + "packages/core/tests/answers-component.test.ts": 2116, + "packages/core/tests/answers-expansion-recursion.test.ts": 838, + "packages/core/tests/capture-errors.test.ts": 1092, + "packages/core/tests/capture-props.test.ts": 866, + "packages/core/tests/cli-journal.test.ts": 18551, + "packages/core/tests/code-block-component.test.ts": 2737, + "packages/core/tests/compiler-boundary.test.ts": 1017, + "packages/core/tests/component-api.test.ts": 490, + "packages/core/tests/component-registration.test.ts": 1860, + "packages/core/tests/component-returns.test.ts": 2465, + "packages/core/tests/component-schema-conformance.test.ts": 1012, + "packages/core/tests/config-api.test.ts": 1107, + "packages/core/tests/construct-error-observation.test.ts": 1179, + "packages/core/tests/contextual-cwd.test.ts": 2284, + "packages/core/tests/daemon-integration.test.ts": 1708, + "packages/core/tests/daemon.test.ts": 505, + "packages/core/tests/declared-markdown-component.test.ts": 2808, + "packages/core/tests/document-output-api.test.ts": 458, + "packages/core/tests/document-target-execution.test.ts": 4148, + "packages/core/tests/document-targets.test.ts": 1089, + "packages/core/tests/document-validation.test.ts": 2803, + "packages/core/tests/documentation-index.test.ts": 737, + "packages/core/tests/each.test.ts": 1142, + "packages/core/tests/elicit-component.test.ts": 1760, + "packages/core/tests/ephemeral-service.test.ts": 1587, + "packages/core/tests/eval-bindings.test.ts": 1132, + "packages/core/tests/eval-context.test.ts": 526, + "packages/core/tests/eval-durable.test.ts": 1158, + "packages/core/tests/eval-error-mode.test.ts": 1448, + "packages/core/tests/eval-interpolate.test.ts": 448, + "packages/core/tests/eval-middleware.test.ts": 539, + "packages/core/tests/eval-persist.test.ts": 1228, + "packages/core/tests/eval-return.test.ts": 1119, + "packages/core/tests/eval-scope.test.ts": 999, + "packages/core/tests/eval-timeout.test.ts": 1108, + "packages/core/tests/eval-transform.test.ts": 535, + "packages/core/tests/evaluate-component.test.ts": 3200, + "packages/core/tests/evaluate-loaded-copy.test.ts": 1184, + "packages/core/tests/evaluate-provider-lifetime.test.ts": 1256, + "packages/core/tests/evaluation-profile.test.ts": 774, + "packages/core/tests/exec-timeout.test.ts": 1151, + "packages/core/tests/execute.test.ts": 1740, + "packages/core/tests/execution-protocol.test.ts": 2560, + "packages/core/tests/expand.test.ts": 1768, + "packages/core/tests/expansion-identity.test.ts": 1780, + "packages/core/tests/expansion-metadata.test.ts": 904, + "packages/core/tests/expression-props.test.ts": 1695, + "packages/core/tests/fail-component.test.ts": 1453, + "packages/core/tests/failure-printing.test.ts": 1311, + "packages/core/tests/fatal-cause.test.ts": 782, + "packages/core/tests/fetch-component.test.ts": 2676, + "packages/core/tests/file-component.test.ts": 3120, + "packages/core/tests/file-delete-component.test.ts": 1306, + "packages/core/tests/files-fatal.test.ts": 2177, + "packages/core/tests/foreground-exec.test.ts": 2091, + "packages/core/tests/frontmatter.test.ts": 498, + "packages/core/tests/function-components.test.ts": 1795, + "packages/core/tests/generated-composition.test.ts": 1816, + "packages/core/tests/generated-xmd.test.ts": 3229, + "packages/core/tests/glob-api.test.ts": 523, + "packages/core/tests/glob-component.test.ts": 1907, + "packages/core/tests/guarded-journal.test.ts": 1182, + "packages/core/tests/has-content.test.ts": 807, + "packages/core/tests/heal.test.ts": 843, + "packages/core/tests/if.test.ts": 1619, + "packages/core/tests/inline-root.test.ts": 1202, + "packages/core/tests/invocation-failures.test.ts": 544, + "packages/core/tests/invocation-identity.test.ts": 1548, + "packages/core/tests/invocation-scope.test.ts": 1068, + "packages/core/tests/journal-source-position.test.ts": 1292, + "packages/core/tests/json-component.test.ts": 1360, + "packages/core/tests/json.test.ts": 462, + "packages/core/tests/let.test.ts": 1152, + "packages/core/tests/loop.test.ts": 2036, + "packages/core/tests/named-slots.test.ts": 2073, + "packages/core/tests/output-error-mode.test.ts": 2340, + "packages/core/tests/output-normalize.test.ts": 464, + "packages/core/tests/output-terminal.test.ts": 626, + "packages/core/tests/parse-components.test.ts": 1304, + "packages/core/tests/plan-response.test.ts": 538, + "packages/core/tests/plugin-api.test.ts": 486, + "packages/core/tests/plugin.test.ts": 884, + "packages/core/tests/props-binding.test.ts": 1295, + "packages/core/tests/protected-content.test.ts": 497, + "packages/core/tests/registered-printed-errors.test.ts": 767, + "packages/core/tests/replay-stream.test.ts": 451, + "packages/core/tests/retain.test.ts": 1037, + "packages/core/tests/root-composition.test.ts": 1279, + "packages/core/tests/root-props.test.ts": 1298, + "packages/core/tests/root-provider.test.ts": 1195, + "packages/core/tests/sample-component.test.ts": 2088, + "packages/core/tests/scanner.test.ts": 521, + "packages/core/tests/scope-local.test.ts": 456, + "packages/core/tests/secret-detection.test.ts": 1897, + "packages/core/tests/secret-files.test.ts": 687, + "packages/core/tests/secret-rules.test.ts": 794, + "packages/core/tests/secret-scanner-baseline.test.ts": 704, + "packages/core/tests/secret-scanner.test.ts": 641, + "packages/core/tests/source-position.test.ts": 1014, + "packages/core/tests/state-ownership.test.ts": 1068, + "packages/core/tests/streaming-emission.test.ts": 1022, + "packages/core/tests/switch.test.ts": 1684, + "packages/core/tests/syntax-catalog.test.ts": 1636, + "packages/core/tests/syntax-component.test.ts": 2426, + "packages/core/tests/temp-dir.test.ts": 3533, + "packages/core/tests/temp-file-compiler.test.ts": 508, + "packages/core/tests/text-interpolation.test.ts": 794, + "packages/core/tests/try-content.test.ts": 818, + "packages/core/tests/unused-in-diff.test.ts": 1205, + "packages/core/tests/validate.test.ts": 735, + "packages/core/tests/validation-integration.test.ts": 1052, + "packages/core/tests/workflow-component-bundle.test.ts": 1387, + "packages/durable-streams/tests/context.test.ts": 479, + "packages/durable-streams/tests/deterministic-id.test.ts": 510, + "packages/durable-streams/tests/divergence-api.test.ts": 495, + "packages/durable-streams/tests/divergence.test.ts": 521, + "packages/durable-streams/tests/durable-each.test.ts": 522, + "packages/durable-streams/tests/durable-position.test.ts": 504, + "packages/durable-streams/tests/durable-run.test.ts": 507, + "packages/durable-streams/tests/ephemeral.test.ts": 506, + "packages/durable-streams/tests/fail-stop.test.ts": 562, + "packages/durable-streams/tests/guard-stream.test.ts": 563, + "packages/durable-streams/tests/live-coordinator.test.ts": 500, + "packages/durable-streams/tests/parse.test.ts": 512, + "packages/durable-streams/tests/replay-guard.test.ts": 522, + "packages/durable-streams/tests/replay-index.test.ts": 483, + "packages/durable-streams/tests/retained.test.ts": 471, + "packages/durable-streams/tests/serialize-event.test.ts": 492, + "packages/durable-streams/tests/smoke.test.ts": 508, + "packages/durable-streams/tests/structured-concurrency.test.ts": 551, + "packages/durable-streams/tests/terminal-boundary.test.ts": 517, + "packages/durable-streams/tests/types.test.ts": 450, + "packages/git/tests/api-entrypoint.test.ts": 1119, + "packages/git/tests/git-host-effect.test.ts": 6430, + "packages/git/tests/git.test.ts": 1131, + "packages/git/tests/github-activation.test.ts": 1434, + "packages/git/tests/github-issues.test.ts": 1073, + "packages/git/tests/github-pull-requests.test.ts": 542, + "packages/git/tests/issue-markdown.test.ts": 9143, + "packages/git/tests/issue-records.test.ts": 1047, + "packages/git/tests/module-partition.test.ts": 2661, + "packages/git/tests/plugin.test.ts": 1514, + "packages/git/tests/provider-neutrality.test.ts": 2039, + "packages/git/tests/pull-request-records.test.ts": 1105, + "packages/git/tests/run-composition-lazy.test.ts": 1805, + "packages/git/tests/selection-authentication.test.ts": 461, + "packages/runtime/tests/duration.test.ts": 446, + "packages/runtime/tests/fetch.test.ts": 1049, + "packages/runtime/tests/fs.test.ts": 499, + "packages/runtime/tests/host-files.test.ts": 1219, + "packages/runtime/tests/native-launcher.test.ts": 2801, + "packages/runtime/tests/process.test.ts": 493, + "packages/runtime/tests/service.test.ts": 478, + "packages/test-agent/tests/acp-server.test.ts": 607, + "packages/test-agent/tests/behavior-engine.test.ts": 1403, + "packages/test-agent/tests/bridge.test.ts": 475, + "packages/test-agent/tests/command-lazy.test.ts": 5674, + "packages/test-agent/tests/components.test.ts": 91334, + "packages/test-agent/tests/controller.test.ts": 715, + "packages/test-agent/tests/cross-package-resolution.test.ts": 2578, + "packages/test-agent/tests/native-launch.test.ts": 45647, + "packages/test-agent/tests/net.test.ts": 538, + "packages/test-agent/tests/profile.test.ts": 1044, + "packages/test-agent/tests/protocol.test.ts": 518, + "packages/test-agent/tests/provider.test.ts": 1162, + "packages/test-agent/tests/public-api.test.ts": 1360, + "packages/test-agent/tests/route-slot.test.ts": 460, + "packages/test-agent/tests/smoke.test.ts": 24811, + "packages/test-agent/tests/template.test.ts": 1030, + "packages/test-agent/tests/worker-lifecycle.test.ts": 19686, + "packages/test-support/tests/journal.test.ts": 449, + "packages/testing/tests/agent-composition.test.ts": 1315, + "packages/testing/tests/assert-throws.test.ts": 2789, + "packages/testing/tests/assert.test.ts": 474, + "packages/testing/tests/assertions.test.ts": 3439, + "packages/testing/tests/boundary-contract.test.ts": 2288, + "packages/testing/tests/cli.test.ts": 9209, + "packages/testing/tests/execution-harness.test.ts": 6818, + "packages/testing/tests/replay.test.ts": 2793, + "packages/testing/tests/smoke.test.ts": 2855, + "packages/testing/tests/test-component.test.ts": 3610, + "packages/testing/tests/testing-activation.test.ts": 3089, + "packages/testing/tests/testing-boundary.test.ts": 1788, + "packages/testing/tests/testing-mode.test.ts": 3703, + "packages/testing/tests/use-testing.test.ts": 2450, + "packages/web/tests/assets.test.ts": 455, + "packages/web/tests/client-logic.test.ts": 480, + "packages/web/tests/compile.test.ts": 1703, + "packages/web/tests/component.test.ts": 2198, + "packages/web/tests/declaration.test.ts": 1459, + "packages/web/tests/document.test.ts": 1514, + "packages/web/tests/elicitation.test.ts": 1370, + "packages/web/tests/live-form.test.ts": 1291, + "packages/web/tests/markdown.test.ts": 597, + "packages/web/tests/opener.test.ts": 1279, + "packages/web/tests/page.test.ts": 453, + "packages/web/tests/responder.test.ts": 1571, + "packages/web/tests/server-lifecycle.test.ts": 1398, + "packages/web/tests/server.test.ts": 1813, + "packages/workflow/tests/generated-observations.test.ts": 1253, + "packages/workflow/tests/retained-run.test.ts": 1926, + "packages/workflow/tests/service-denial.test.ts": 461, + "packages/workflow/tests/workflow-bundle.test.ts": 1460, + "packages/workflow/tests/workflow-definition.test.ts": 1172, + "packages/workflow/tests/workflow-fork.test.ts": 1038, + "packages/workflow/tests/workflow-run.test.ts": 1839, + "packages/workflow/tests/workspace-effect.test.ts": 2652, + "packages/workflow/tests/xmd-artifact.test.ts": 3522, + "scripts/tests/adapter-vendor.test.ts": 657, + "scripts/tests/bootstrap-npm-package.test.ts": 7338, + "scripts/tests/bump-version.test.ts": 563, + "scripts/tests/changed-paths.test.ts": 477, + "scripts/tests/ci-workflow.test.ts": 3191, + "scripts/tests/consumer-cycle.test.ts": 615, + "scripts/tests/filesystem-contract-workflow.test.ts": 523, + "scripts/tests/main-green.test.ts": 722, + "scripts/tests/main-health.test.ts": 558, + "scripts/tests/no-module-scoped-registry.test.ts": 1169, + "scripts/tests/no-redundant-test-scope.test.ts": 4617, + "scripts/tests/no-sync-filesystem.test.ts": 4929, + "scripts/tests/no-yield-in-finally.test.ts": 1166, + "scripts/tests/oxlint-policy.test.ts": 9186, + "scripts/tests/packaged-document.test.ts": 542, + "scripts/tests/prefer-effection-operation.test.ts": 5702, + "scripts/tests/prefer-effection-result.test.ts": 4004, + "scripts/tests/publish-workflow-membership.test.ts": 597, + "scripts/tests/review-infrastructure.test.ts": 2845, + "scripts/tests/runtime-exclusions.test.ts": 904, + "scripts/tests/runtime-tests.test.ts": 508, + "scripts/tests/scope-bound-event-registration.test.ts": 4708, + "scripts/tests/staged-write.test.ts": 492, + "scripts/tests/test-file-discovery.test.ts": 732, + "scripts/tests/test-shards.test.ts": 497, + "scripts/tests/test-weights.test.ts": 660, + "scripts/tests/tracked.test.ts": 479, + "scripts/tests/verify-coordinator.test.ts": 670, + "scripts/tests/version-lockstep.test.ts": 613, + "scripts/tests/web-client-module.test.ts": 464, + "scripts/tests/workspace.test.ts": 453 }, "bun": { - "packages/acp/tests/acceptance.test.ts": 476, - "packages/acp/tests/acpx-checkpoint-meta.test.ts": 4212, - "packages/acp/tests/acpx-materialization.test.ts": 6394, - "packages/acp/tests/acpx-transient-env.test.ts": 161, - "packages/acp/tests/adapter-materialization.test.ts": 43821, - "packages/acp/tests/adapter-protocol.test.ts": 32539, - "packages/acp/tests/codex-surfaces.test.ts": 73, - "packages/acp/tests/native-launch.test.ts": 734, - "packages/acp/tests/permission-bridge.test.ts": 234, - "packages/acp/tests/provider.test.ts": 843, - "packages/acp/tests/serial-queue.test.ts": 53, - "packages/acp/tests/session-key.test.ts": 52, - "packages/acp/tests/terminal-screen.test.ts": 56, - "packages/cli/tests/agent-adapters.test.ts": 695, - "packages/cli/tests/agent-cli.test.ts": 12465, - "packages/cli/tests/agent-config.test.ts": 20, - "packages/cli/tests/agent-options-cli.test.ts": 4835, - "packages/cli/tests/agent-options.test.ts": 21, - "packages/cli/tests/agent-session-coordinator.test.ts": 238, - "packages/cli/tests/cli-help.test.ts": 6709, - "packages/cli/tests/command.test.ts": 5933, - "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 1671, - "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 84321, - "packages/cli/tests/document-suites/props/props-markdown.test.ts": 1438, - "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 2935, - "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 2792, - "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 5180, - "packages/cli/tests/evaluate-component.test.ts": 4505, - "packages/cli/tests/fetch-cli.test.ts": 1349, - "packages/cli/tests/file-stream.test.ts": 41, - "packages/cli/tests/inline-cli.test.ts": 18600, - "packages/cli/tests/launch-timeout.test.ts": 10064, - "packages/cli/tests/packaged-document.test.ts": 288, - "packages/cli/tests/plan-args.test.ts": 23, - "packages/cli/tests/plan-cli.test.ts": 36529, - "packages/cli/tests/plan-command-document.test.ts": 3735, - "packages/cli/tests/plan-component.test.ts": 5651, - "packages/cli/tests/plan-host-acts.test.ts": 702, - "packages/cli/tests/plan.test.ts": 8228, - "packages/cli/tests/plugin-cli.test.ts": 23767, - "packages/cli/tests/plugin-host.test.ts": 1239, - "packages/cli/tests/plugin-modules.test.ts": 25, - "packages/cli/tests/plugin-selection.test.ts": 25, - "packages/cli/tests/process-retention.test.ts": 1589, - "packages/cli/tests/props-cli.test.ts": 16657, - "packages/cli/tests/props-schema.test.ts": 239, - "packages/cli/tests/props-sources.test.ts": 88, - "packages/cli/tests/repl-composition.test.ts": 92, - "packages/cli/tests/repl-execution.test.ts": 1275, - "packages/cli/tests/repl-model.test.ts": 606, - "packages/cli/tests/repl-route.test.ts": 492, - "packages/cli/tests/run-composition.test.ts": 1941, - "packages/cli/tests/run-deadline.test.ts": 2030, - "packages/cli/tests/run-profile.test.ts": 198, - "packages/cli/tests/run-timeouts.test.ts": 8337, - "packages/cli/tests/secret-detection-cli.test.ts": 10975, - "packages/cli/tests/service-document.test.ts": 1070, - "packages/cli/tests/service-host.test.ts": 957, - "packages/cli/tests/session-launch-cli.test.ts": 3591, - "packages/cli/tests/stdin-cli.test.ts": 19893, - "packages/cli/tests/stdout-delivery.test.ts": 35, - "packages/cli/tests/syntax-cli.test.ts": 19614, - "packages/cli/tests/targets-cli.test.ts": 12552, - "packages/cli/tests/test-root-profile.test.ts": 2298, - "packages/cli/tests/test-target.test.ts": 21000, - "packages/cli/tests/testing-activation.test.ts": 361, - "packages/cli/tests/testing-execution-host.test.ts": 33370, - "packages/cli/tests/upgrade-cli.test.ts": 11035, - "packages/cli/tests/upgrade-command-document.test.ts": 2550, - "packages/cli/tests/upgrade-output.test.ts": 552, - "packages/cli/tests/value-root.test.ts": 9996, - "packages/cli/tests/verbose-component.test.ts": 472, - "packages/cli/tests/workflow-declaration.test.ts": 323, - "packages/cli/tests/workflow-host.test.ts": 2653, - "packages/code-review-agent/tests/doctor.test.ts": 21, - "packages/code-review-agent/tests/parse-diagnostics.test.ts": 26, - "packages/code-review-agent/tests/parse-diff.test.ts": 24, - "packages/code-review-agent/tests/parse-doctor.test.ts": 194, - "packages/code-review-agent/tests/parse-oxlint.test.ts": 21, - "packages/code-review-agent/tests/policy.test.ts": 22, - "packages/core/tests/agent-api.test.ts": 26, - "packages/core/tests/agent-components.test.ts": 769, - "packages/core/tests/agent-function-components.test.ts": 1895, - "packages/core/tests/agent-session-launch.test.ts": 2171, - "packages/core/tests/agent-session-placement.test.ts": 25, - "packages/core/tests/agent-session-use.test.ts": 23, - "packages/core/tests/agent-stack.test.ts": 437, - "packages/core/tests/answer-identity.test.ts": 44, - "packages/core/tests/answers-component.test.ts": 1019, - "packages/core/tests/answers-expansion-recursion.test.ts": 203, - "packages/core/tests/capture-errors.test.ts": 374, - "packages/core/tests/capture-props.test.ts": 232, - "packages/core/tests/cli-journal.test.ts": 6124, - "packages/core/tests/code-block-component.test.ts": 1576, - "packages/core/tests/compiler-boundary.test.ts": 208, - "packages/core/tests/component-api.test.ts": 43, - "packages/core/tests/component-registration.test.ts": 672, - "packages/core/tests/component-returns.test.ts": 1007, - "packages/core/tests/component-schema-conformance.test.ts": 330, - "packages/core/tests/config-api.test.ts": 213, - "packages/core/tests/construct-error-observation.test.ts": 455, - "packages/core/tests/contextual-cwd.test.ts": 1344, - "packages/core/tests/daemon-integration.test.ts": 825, - "packages/core/tests/daemon.test.ts": 46, - "packages/core/tests/declared-markdown-component.test.ts": 1257, - "packages/core/tests/document-output-api.test.ts": 25, - "packages/core/tests/document-target-execution.test.ts": 2275, - "packages/core/tests/document-targets.test.ts": 255, - "packages/core/tests/document-validation.test.ts": 1233, - "packages/core/tests/documentation-index.test.ts": 110, - "packages/core/tests/each.test.ts": 248, - "packages/core/tests/elicit-component.test.ts": 710, - "packages/core/tests/ephemeral-service.test.ts": 514, - "packages/core/tests/eval-bindings.test.ts": 245, - "packages/core/tests/eval-context.test.ts": 53, - "packages/core/tests/eval-durable.test.ts": 254, - "packages/core/tests/eval-error-mode.test.ts": 427, - "packages/core/tests/eval-interpolate.test.ts": 55, - "packages/core/tests/eval-middleware.test.ts": 65, - "packages/core/tests/eval-persist.test.ts": 317, - "packages/core/tests/eval-return.test.ts": 275, - "packages/core/tests/eval-scope.test.ts": 209, - "packages/core/tests/eval-timeout.test.ts": 283, - "packages/core/tests/eval-transform.test.ts": 62, - "packages/core/tests/evaluate-component.test.ts": 1588, - "packages/core/tests/evaluate-loaded-copy.test.ts": 318, - "packages/core/tests/evaluate-provider-lifetime.test.ts": 407, - "packages/core/tests/evaluation-profile.test.ts": 135, - "packages/core/tests/exec-timeout.test.ts": 303, - "packages/core/tests/execute.test.ts": 647, - "packages/core/tests/execution-protocol.test.ts": 1044, - "packages/core/tests/expand.test.ts": 795, - "packages/core/tests/expansion-identity.test.ts": 766, - "packages/core/tests/expansion-metadata.test.ts": 206, - "packages/core/tests/expression-props.test.ts": 562, - "packages/core/tests/fail-component.test.ts": 493, - "packages/core/tests/failure-printing.test.ts": 460, - "packages/core/tests/fatal-cause.test.ts": 113, - "packages/core/tests/fetch-component.test.ts": 1172, - "packages/core/tests/file-component.test.ts": 1914, - "packages/core/tests/file-delete-component.test.ts": 457, - "packages/core/tests/files-fatal.test.ts": 946, - "packages/core/tests/foreground-exec.test.ts": 812, - "packages/core/tests/frontmatter.test.ts": 30, - "packages/core/tests/function-components.test.ts": 576, - "packages/core/tests/generated-composition.test.ts": 833, - "packages/core/tests/generated-xmd.test.ts": 1598, - "packages/core/tests/glob-api.test.ts": 66, - "packages/core/tests/glob-component.test.ts": 871, - "packages/core/tests/guarded-journal.test.ts": 284, - "packages/core/tests/has-content.test.ts": 167, - "packages/core/tests/heal.test.ts": 163, - "packages/core/tests/if.test.ts": 491, - "packages/core/tests/inline-root.test.ts": 298, - "packages/core/tests/invocation-failures.test.ts": 78, - "packages/core/tests/invocation-identity.test.ts": 506, - "packages/core/tests/invocation-scope.test.ts": 365, - "packages/core/tests/journal-source-position.test.ts": 346, - "packages/core/tests/json-component.test.ts": 1494, - "packages/core/tests/json.test.ts": 22, - "packages/core/tests/let.test.ts": 270, - "packages/core/tests/loop.test.ts": 768, - "packages/core/tests/named-slots.test.ts": 818, - "packages/core/tests/output-error-mode.test.ts": 1085, - "packages/core/tests/output-normalize.test.ts": 29, - "packages/core/tests/output-terminal.test.ts": 99, - "packages/core/tests/parse-components.test.ts": 396, - "packages/core/tests/plan-response.test.ts": 67, - "packages/core/tests/plugin-api.test.ts": 30, - "packages/core/tests/plugin.test.ts": 114, - "packages/core/tests/props-binding.test.ts": 351, - "packages/core/tests/protected-content.test.ts": 31, - "packages/core/tests/registered-printed-errors.test.ts": 131, - "packages/core/tests/replay-stream.test.ts": 25, - "packages/core/tests/retain.test.ts": 324, - "packages/core/tests/root-composition.test.ts": 365, - "packages/core/tests/root-props.test.ts": 369, - "packages/core/tests/root-provider.test.ts": 364, - "packages/core/tests/sample-component.test.ts": 760, - "packages/core/tests/scanner.test.ts": 48, - "packages/core/tests/scope-local.test.ts": 24, - "packages/core/tests/secret-detection.test.ts": 794, - "packages/core/tests/secret-files.test.ts": 82, - "packages/core/tests/secret-rules.test.ts": 90, - "packages/core/tests/secret-scanner-baseline.test.ts": 86, - "packages/core/tests/secret-scanner.test.ts": 78, - "packages/core/tests/source-position.test.ts": 210, - "packages/core/tests/state-ownership.test.ts": 247, - "packages/core/tests/streaming-emission.test.ts": 196, - "packages/core/tests/switch.test.ts": 549, - "packages/core/tests/syntax-catalog.test.ts": 579, - "packages/core/tests/syntax-component.test.ts": 1224, - "packages/core/tests/temp-dir.test.ts": 2545, - "packages/core/tests/temp-file-compiler.test.ts": 50, - "packages/core/tests/text-interpolation.test.ts": 143, - "packages/core/tests/try-content.test.ts": 166, - "packages/core/tests/unused-in-diff.test.ts": 319, - "packages/core/tests/validate.test.ts": 200, - "packages/core/tests/validation-integration.test.ts": 235, - "packages/core/tests/workflow-component-bundle.test.ts": 463, - "packages/durable-streams/tests/context.test.ts": 58, - "packages/durable-streams/tests/deterministic-id.test.ts": 61, - "packages/durable-streams/tests/divergence-api.test.ts": 32, - "packages/durable-streams/tests/divergence.test.ts": 38, - "packages/durable-streams/tests/durable-each.test.ts": 42, - "packages/durable-streams/tests/durable-position.test.ts": 34, - "packages/durable-streams/tests/durable-run.test.ts": 41, - "packages/durable-streams/tests/ephemeral.test.ts": 38, - "packages/durable-streams/tests/fail-stop.test.ts": 46, - "packages/durable-streams/tests/guard-stream.test.ts": 82, - "packages/durable-streams/tests/live-coordinator.test.ts": 37, - "packages/durable-streams/tests/parse.test.ts": 32, - "packages/durable-streams/tests/replay-guard.test.ts": 45, - "packages/durable-streams/tests/replay-index.test.ts": 30, - "packages/durable-streams/tests/retained.test.ts": 29, - "packages/durable-streams/tests/serialize-event.test.ts": 31, - "packages/durable-streams/tests/smoke.test.ts": 24, - "packages/durable-streams/tests/structured-concurrency.test.ts": 56, - "packages/durable-streams/tests/terminal-boundary.test.ts": 49, - "packages/durable-streams/tests/types.test.ts": 21, - "packages/git/tests/api-entrypoint.test.ts": 212, - "packages/git/tests/git-host-effect.test.ts": 4120, - "packages/git/tests/git.test.ts": 238, - "packages/git/tests/github-activation.test.ts": 414, - "packages/git/tests/github-issues.test.ts": 190, - "packages/git/tests/github-pull-requests.test.ts": 51, - "packages/git/tests/issue-markdown.test.ts": 5438, - "packages/git/tests/issue-records.test.ts": 198, - "packages/git/tests/module-partition.test.ts": 1119, - "packages/git/tests/plugin.test.ts": 533, - "packages/git/tests/provider-neutrality.test.ts": 688, - "packages/git/tests/pull-request-records.test.ts": 201, - "packages/git/tests/run-composition-lazy.test.ts": 496, - "packages/git/tests/selection-authentication.test.ts": 27, - "packages/runtime/tests/duration.test.ts": 22, - "packages/runtime/tests/fetch.test.ts": 584, - "packages/runtime/tests/fs.test.ts": 52, - "packages/runtime/tests/host-files.test.ts": 661, - "packages/runtime/tests/native-launcher.test.ts": 2354, - "packages/runtime/tests/process.test.ts": 59, - "packages/runtime/tests/service.test.ts": 42, - "packages/test-agent/tests/acp-server.test.ts": 66, - "packages/test-agent/tests/behavior-engine.test.ts": 394, - "packages/test-agent/tests/bridge.test.ts": 55, - "packages/test-agent/tests/command-lazy.test.ts": 2395, - "packages/test-agent/tests/components.test.ts": 38421, - "packages/test-agent/tests/controller.test.ts": 129, - "packages/test-agent/tests/cross-package-resolution.test.ts": 1112, - "packages/test-agent/tests/native-launch.test.ts": 15994, - "packages/test-agent/tests/net.test.ts": 76, - "packages/test-agent/tests/profile.test.ts": 182, - "packages/test-agent/tests/protocol.test.ts": 35, - "packages/test-agent/tests/provider.test.ts": 248, - "packages/test-agent/tests/public-api.test.ts": 283, - "packages/test-agent/tests/route-slot.test.ts": 46, - "packages/test-agent/tests/smoke.test.ts": 11864, - "packages/test-agent/tests/template.test.ts": 181, - "packages/test-agent/tests/worker-lifecycle.test.ts": 5563, - "packages/test-support/tests/journal.test.ts": 27, - "packages/testing/tests/agent-composition.test.ts": 382, - "packages/testing/tests/assert-throws.test.ts": 1421, - "packages/testing/tests/assert.test.ts": 35, - "packages/testing/tests/assertions.test.ts": 1950, - "packages/testing/tests/boundary-contract.test.ts": 1032, - "packages/testing/tests/cli.test.ts": 3048, - "packages/testing/tests/execution-harness.test.ts": 4299, - "packages/testing/tests/replay.test.ts": 1395, - "packages/testing/tests/smoke.test.ts": 1300, - "packages/testing/tests/test-component.test.ts": 2035, - "packages/testing/tests/testing-activation.test.ts": 1696, - "packages/testing/tests/testing-boundary.test.ts": 730, - "packages/testing/tests/testing-mode.test.ts": 2024, - "packages/testing/tests/use-testing.test.ts": 1221, - "packages/web/tests/assets.test.ts": 21, - "packages/web/tests/client-logic.test.ts": 25, - "packages/web/tests/compile.test.ts": 602, - "packages/web/tests/component.test.ts": 1023, - "packages/web/tests/declaration.test.ts": 411, - "packages/web/tests/document.test.ts": 488, - "packages/web/tests/elicitation.test.ts": 360, - "packages/web/tests/live-form.test.ts": 372, - "packages/web/tests/markdown.test.ts": 103, - "packages/web/tests/opener.test.ts": 310, - "packages/web/tests/page.test.ts": 22, - "packages/web/tests/responder.test.ts": 518, - "packages/web/tests/server-lifecycle.test.ts": 375, - "packages/web/tests/server.test.ts": 682, - "packages/workflow/tests/generated-observations.test.ts": 346, - "packages/workflow/tests/retained-run.test.ts": 816, - "packages/workflow/tests/service-denial.test.ts": 47, - "packages/workflow/tests/workflow-bundle.test.ts": 477, - "packages/workflow/tests/workflow-definition.test.ts": 217, - "packages/workflow/tests/workflow-fork.test.ts": 194, - "packages/workflow/tests/workflow-run.test.ts": 662, - "packages/workflow/tests/workspace-effect.test.ts": 902, - "scripts/tests/adapter-vendor.test.ts": 106, - "scripts/tests/bootstrap-npm-package.test.ts": 4054, - "scripts/tests/bump-version.test.ts": 57, - "scripts/tests/changed-paths.test.ts": 25, - "scripts/tests/ci-workflow.test.ts": 1249, - "scripts/tests/consumer-cycle.test.ts": 78, - "scripts/tests/filesystem-contract-workflow.test.ts": 57, - "scripts/tests/main-green.test.ts": 264, - "scripts/tests/main-health.test.ts": 70, - "scripts/tests/no-module-scoped-registry.test.ts": 574, - "scripts/tests/no-redundant-test-scope.test.ts": 3101, - "scripts/tests/no-sync-filesystem.test.ts": 2925, - "scripts/tests/no-yield-in-finally.test.ts": 599, - "scripts/tests/oxlint-policy.test.ts": 6486, - "scripts/tests/packaged-document.test.ts": 49, - "scripts/tests/prefer-effection-operation.test.ts": 3631, - "scripts/tests/prefer-effection-result.test.ts": 2532, - "scripts/tests/publish-workflow-membership.test.ts": 68, - "scripts/tests/review-infrastructure.test.ts": 1132, - "scripts/tests/runtime-exclusions.test.ts": 185, - "scripts/tests/runtime-tests.test.ts": 68, - "scripts/tests/scope-bound-event-registration.test.ts": 2706, - "scripts/tests/staged-write.test.ts": 47, - "scripts/tests/test-file-discovery.test.ts": 154, - "scripts/tests/test-shards.test.ts": 29, - "scripts/tests/test-weights.test.ts": 171, - "scripts/tests/tracked.test.ts": 25, - "scripts/tests/verify-coordinator.test.ts": 206, - "scripts/tests/version-lockstep.test.ts": 78, - "scripts/tests/web-client-module.test.ts": 39, - "scripts/tests/workspace.test.ts": 31 + "packages/acp/tests/acceptance.test.ts": 744, + "packages/acp/tests/acpx-checkpoint-meta.test.ts": 4286, + "packages/acp/tests/acpx-materialization.test.ts": 6484, + "packages/acp/tests/acpx-transient-env.test.ts": 248, + "packages/acp/tests/adapter-materialization.test.ts": 66479, + "packages/acp/tests/adapter-protocol.test.ts": 49439, + "packages/acp/tests/codex-surfaces.test.ts": 118, + "packages/acp/tests/native-launch.test.ts": 951, + "packages/acp/tests/permission-bridge.test.ts": 339, + "packages/acp/tests/provider.test.ts": 1055, + "packages/acp/tests/serial-queue.test.ts": 63, + "packages/acp/tests/session-key.test.ts": 67, + "packages/acp/tests/terminal-screen.test.ts": 80, + "packages/cli/tests/agent-adapters.test.ts": 1099, + "packages/cli/tests/agent-cli.test.ts": 19452, + "packages/cli/tests/agent-config.test.ts": 26, + "packages/cli/tests/agent-options-cli.test.ts": 7289, + "packages/cli/tests/agent-options.test.ts": 29, + "packages/cli/tests/agent-session-coordinator.test.ts": 367, + "packages/cli/tests/cli-help.test.ts": 10618, + "packages/cli/tests/command.test.ts": 7507, + "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 2750, + "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 100519, + "packages/cli/tests/document-suites/props/props-markdown.test.ts": 2160, + "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 4408, + "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 4564, + "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 8036, + "packages/cli/tests/evaluate-component.test.ts": 7095, + "packages/cli/tests/fetch-cli.test.ts": 2106, + "packages/cli/tests/file-stream.test.ts": 53, + "packages/cli/tests/inline-cli.test.ts": 28810, + "packages/cli/tests/launch-timeout.test.ts": 10079, + "packages/cli/tests/packaged-document.test.ts": 428, + "packages/cli/tests/plan-args.test.ts": 30, + "packages/cli/tests/plan-cli.test.ts": 54947, + "packages/cli/tests/plan-command-document.test.ts": 5393, + "packages/cli/tests/plan-component.test.ts": 8517, + "packages/cli/tests/plan-host-acts.test.ts": 1118, + "packages/cli/tests/plan.test.ts": 12187, + "packages/cli/tests/plugin-cli.test.ts": 35808, + "packages/cli/tests/plugin-host.test.ts": 1844, + "packages/cli/tests/plugin-modules.test.ts": 33, + "packages/cli/tests/plugin-selection.test.ts": 31, + "packages/cli/tests/process-retention.test.ts": 2291, + "packages/cli/tests/props-cli.test.ts": 25899, + "packages/cli/tests/props-schema.test.ts": 383, + "packages/cli/tests/props-sources.test.ts": 117, + "packages/cli/tests/repl-composition.test.ts": 132, + "packages/cli/tests/repl-execution.test.ts": 1763, + "packages/cli/tests/repl-model.test.ts": 890, + "packages/cli/tests/repl-route.test.ts": 724, + "packages/cli/tests/repl-terminal.test.ts": 799, + "packages/cli/tests/run-composition.test.ts": 2884, + "packages/cli/tests/run-deadline.test.ts": 2305, + "packages/cli/tests/run-profile.test.ts": 285, + "packages/cli/tests/run-timeouts.test.ts": 10667, + "packages/cli/tests/secret-detection-cli.test.ts": 16731, + "packages/cli/tests/service-document.test.ts": 1429, + "packages/cli/tests/service-host.test.ts": 1328, + "packages/cli/tests/session-launch-cli.test.ts": 5280, + "packages/cli/tests/stdin-cli.test.ts": 28646, + "packages/cli/tests/stdout-delivery.test.ts": 46, + "packages/cli/tests/syntax-cli.test.ts": 30236, + "packages/cli/tests/targets-cli.test.ts": 19960, + "packages/cli/tests/test-root-profile.test.ts": 3486, + "packages/cli/tests/test-target.test.ts": 32810, + "packages/cli/tests/testing-activation.test.ts": 531, + "packages/cli/tests/testing-execution-host.test.ts": 45619, + "packages/cli/tests/upgrade-cli.test.ts": 16213, + "packages/cli/tests/upgrade-command-document.test.ts": 3396, + "packages/cli/tests/upgrade-output.test.ts": 781, + "packages/cli/tests/value-root.test.ts": 14408, + "packages/cli/tests/verbose-component.test.ts": 708, + "packages/cli/tests/workflow-declaration.test.ts": 470, + "packages/cli/tests/workflow-host.test.ts": 3873, + "packages/code-review-agent/tests/doctor.test.ts": 27, + "packages/code-review-agent/tests/parse-diagnostics.test.ts": 34, + "packages/code-review-agent/tests/parse-diff.test.ts": 32, + "packages/code-review-agent/tests/parse-doctor.test.ts": 272, + "packages/code-review-agent/tests/parse-oxlint.test.ts": 27, + "packages/code-review-agent/tests/policy.test.ts": 28, + "packages/core/tests/agent-api.test.ts": 30, + "packages/core/tests/agent-components.test.ts": 1143, + "packages/core/tests/agent-function-components.test.ts": 2785, + "packages/core/tests/agent-session-launch.test.ts": 3217, + "packages/core/tests/agent-session-placement.test.ts": 32, + "packages/core/tests/agent-session-use.test.ts": 27, + "packages/core/tests/agent-stack.test.ts": 655, + "packages/core/tests/answer-identity.test.ts": 36, + "packages/core/tests/answers-component.test.ts": 1309, + "packages/core/tests/answers-expansion-recursion.test.ts": 281, + "packages/core/tests/capture-errors.test.ts": 525, + "packages/core/tests/capture-props.test.ts": 290, + "packages/core/tests/cli-journal.test.ts": 9215, + "packages/core/tests/code-block-component.test.ts": 2245, + "packages/core/tests/compiler-boundary.test.ts": 316, + "packages/core/tests/component-api.test.ts": 49, + "packages/core/tests/component-registration.test.ts": 978, + "packages/core/tests/component-returns.test.ts": 1439, + "packages/core/tests/component-schema-conformance.test.ts": 525, + "packages/core/tests/config-api.test.ts": 315, + "packages/core/tests/construct-error-observation.test.ts": 633, + "packages/core/tests/contextual-cwd.test.ts": 1490, + "packages/core/tests/daemon-integration.test.ts": 998, + "packages/core/tests/daemon.test.ts": 63, + "packages/core/tests/declared-markdown-component.test.ts": 1873, + "packages/core/tests/document-output-api.test.ts": 33, + "packages/core/tests/document-target-execution.test.ts": 3168, + "packages/core/tests/document-targets.test.ts": 381, + "packages/core/tests/document-validation.test.ts": 1906, + "packages/core/tests/documentation-index.test.ts": 149, + "packages/core/tests/each.test.ts": 384, + "packages/core/tests/elicit-component.test.ts": 1014, + "packages/core/tests/ephemeral-service.test.ts": 761, + "packages/core/tests/eval-bindings.test.ts": 355, + "packages/core/tests/eval-context.test.ts": 73, + "packages/core/tests/eval-durable.test.ts": 388, + "packages/core/tests/eval-error-mode.test.ts": 620, + "packages/core/tests/eval-interpolate.test.ts": 33, + "packages/core/tests/eval-middleware.test.ts": 67, + "packages/core/tests/eval-persist.test.ts": 449, + "packages/core/tests/eval-return.test.ts": 397, + "packages/core/tests/eval-scope.test.ts": 306, + "packages/core/tests/eval-timeout.test.ts": 386, + "packages/core/tests/eval-transform.test.ts": 57, + "packages/core/tests/evaluate-component.test.ts": 2345, + "packages/core/tests/evaluate-loaded-copy.test.ts": 472, + "packages/core/tests/evaluate-provider-lifetime.test.ts": 542, + "packages/core/tests/evaluation-profile.test.ts": 169, + "packages/core/tests/exec-timeout.test.ts": 447, + "packages/core/tests/execute.test.ts": 970, + "packages/core/tests/execution-protocol.test.ts": 1559, + "packages/core/tests/expand.test.ts": 1116, + "packages/core/tests/expansion-identity.test.ts": 1121, + "packages/core/tests/expansion-metadata.test.ts": 304, + "packages/core/tests/expression-props.test.ts": 800, + "packages/core/tests/fail-component.test.ts": 687, + "packages/core/tests/failure-printing.test.ts": 627, + "packages/core/tests/fatal-cause.test.ts": 174, + "packages/core/tests/fetch-component.test.ts": 1566, + "packages/core/tests/file-component.test.ts": 2428, + "packages/core/tests/file-delete-component.test.ts": 581, + "packages/core/tests/files-fatal.test.ts": 1402, + "packages/core/tests/foreground-exec.test.ts": 1136, + "packages/core/tests/frontmatter.test.ts": 44, + "packages/core/tests/function-components.test.ts": 858, + "packages/core/tests/generated-composition.test.ts": 1049, + "packages/core/tests/generated-xmd.test.ts": 2201, + "packages/core/tests/glob-api.test.ts": 94, + "packages/core/tests/glob-component.test.ts": 1141, + "packages/core/tests/guarded-journal.test.ts": 411, + "packages/core/tests/has-content.test.ts": 241, + "packages/core/tests/heal.test.ts": 225, + "packages/core/tests/if.test.ts": 730, + "packages/core/tests/inline-root.test.ts": 453, + "packages/core/tests/invocation-failures.test.ts": 97, + "packages/core/tests/invocation-identity.test.ts": 722, + "packages/core/tests/invocation-scope.test.ts": 491, + "packages/core/tests/journal-source-position.test.ts": 537, + "packages/core/tests/json-component.test.ts": 2648, + "packages/core/tests/json.test.ts": 29, + "packages/core/tests/let.test.ts": 436, + "packages/core/tests/loop.test.ts": 1126, + "packages/core/tests/named-slots.test.ts": 1181, + "packages/core/tests/output-error-mode.test.ts": 1468, + "packages/core/tests/output-normalize.test.ts": 36, + "packages/core/tests/output-terminal.test.ts": 143, + "packages/core/tests/parse-components.test.ts": 523, + "packages/core/tests/plan-response.test.ts": 76, + "packages/core/tests/plugin-api.test.ts": 40, + "packages/core/tests/plugin.test.ts": 170, + "packages/core/tests/props-binding.test.ts": 505, + "packages/core/tests/protected-content.test.ts": 44, + "packages/core/tests/registered-printed-errors.test.ts": 191, + "packages/core/tests/replay-stream.test.ts": 31, + "packages/core/tests/retain.test.ts": 428, + "packages/core/tests/root-composition.test.ts": 509, + "packages/core/tests/root-props.test.ts": 527, + "packages/core/tests/root-provider.test.ts": 524, + "packages/core/tests/sample-component.test.ts": 1067, + "packages/core/tests/scanner.test.ts": 69, + "packages/core/tests/scope-local.test.ts": 31, + "packages/core/tests/secret-detection.test.ts": 1139, + "packages/core/tests/secret-files.test.ts": 109, + "packages/core/tests/secret-rules.test.ts": 149, + "packages/core/tests/secret-scanner-baseline.test.ts": 139, + "packages/core/tests/secret-scanner.test.ts": 99, + "packages/core/tests/source-position.test.ts": 307, + "packages/core/tests/state-ownership.test.ts": 361, + "packages/core/tests/streaming-emission.test.ts": 284, + "packages/core/tests/switch.test.ts": 765, + "packages/core/tests/syntax-catalog.test.ts": 830, + "packages/core/tests/syntax-component.test.ts": 1646, + "packages/core/tests/temp-dir.test.ts": 2764, + "packages/core/tests/temp-file-compiler.test.ts": 67, + "packages/core/tests/text-interpolation.test.ts": 216, + "packages/core/tests/try-content.test.ts": 250, + "packages/core/tests/unused-in-diff.test.ts": 458, + "packages/core/tests/validate.test.ts": 296, + "packages/core/tests/validation-integration.test.ts": 360, + "packages/core/tests/workflow-component-bundle.test.ts": 690, + "packages/durable-streams/tests/context.test.ts": 32, + "packages/durable-streams/tests/deterministic-id.test.ts": 64, + "packages/durable-streams/tests/divergence-api.test.ts": 40, + "packages/durable-streams/tests/divergence.test.ts": 51, + "packages/durable-streams/tests/durable-each.test.ts": 57, + "packages/durable-streams/tests/durable-position.test.ts": 46, + "packages/durable-streams/tests/durable-run.test.ts": 60, + "packages/durable-streams/tests/ephemeral.test.ts": 53, + "packages/durable-streams/tests/fail-stop.test.ts": 65, + "packages/durable-streams/tests/guard-stream.test.ts": 103, + "packages/durable-streams/tests/live-coordinator.test.ts": 54, + "packages/durable-streams/tests/parse.test.ts": 43, + "packages/durable-streams/tests/replay-guard.test.ts": 65, + "packages/durable-streams/tests/replay-index.test.ts": 39, + "packages/durable-streams/tests/retained.test.ts": 43, + "packages/durable-streams/tests/serialize-event.test.ts": 35, + "packages/durable-streams/tests/smoke.test.ts": 30, + "packages/durable-streams/tests/structured-concurrency.test.ts": 79, + "packages/durable-streams/tests/terminal-boundary.test.ts": 71, + "packages/durable-streams/tests/types.test.ts": 24, + "packages/git/tests/api-entrypoint.test.ts": 280, + "packages/git/tests/git-host-effect.test.ts": 6090, + "packages/git/tests/git.test.ts": 347, + "packages/git/tests/github-activation.test.ts": 646, + "packages/git/tests/github-issues.test.ts": 283, + "packages/git/tests/github-pull-requests.test.ts": 76, + "packages/git/tests/issue-markdown.test.ts": 8192, + "packages/git/tests/issue-records.test.ts": 267, + "packages/git/tests/module-partition.test.ts": 1580, + "packages/git/tests/plugin.test.ts": 746, + "packages/git/tests/provider-neutrality.test.ts": 984, + "packages/git/tests/pull-request-records.test.ts": 297, + "packages/git/tests/run-composition-lazy.test.ts": 732, + "packages/git/tests/selection-authentication.test.ts": 75, + "packages/runtime/tests/duration.test.ts": 54, + "packages/runtime/tests/fetch.test.ts": 617, + "packages/runtime/tests/fs.test.ts": 65, + "packages/runtime/tests/host-files.test.ts": 725, + "packages/runtime/tests/native-launcher.test.ts": 2395, + "packages/runtime/tests/process.test.ts": 81, + "packages/runtime/tests/service.test.ts": 57, + "packages/test-agent/tests/acp-server.test.ts": 95, + "packages/test-agent/tests/behavior-engine.test.ts": 627, + "packages/test-agent/tests/bridge.test.ts": 63, + "packages/test-agent/tests/command-lazy.test.ts": 3054, + "packages/test-agent/tests/components.test.ts": 49393, + "packages/test-agent/tests/controller.test.ts": 192, + "packages/test-agent/tests/cross-package-resolution.test.ts": 1739, + "packages/test-agent/tests/native-launch.test.ts": 22544, + "packages/test-agent/tests/net.test.ts": 89, + "packages/test-agent/tests/profile.test.ts": 288, + "packages/test-agent/tests/protocol.test.ts": 52, + "packages/test-agent/tests/provider.test.ts": 381, + "packages/test-agent/tests/public-api.test.ts": 446, + "packages/test-agent/tests/route-slot.test.ts": 54, + "packages/test-agent/tests/smoke.test.ts": 14887, + "packages/test-agent/tests/template.test.ts": 297, + "packages/test-agent/tests/worker-lifecycle.test.ts": 8501, + "packages/test-support/tests/journal.test.ts": 38, + "packages/testing/tests/agent-composition.test.ts": 574, + "packages/testing/tests/assert-throws.test.ts": 2186, + "packages/testing/tests/assert.test.ts": 51, + "packages/testing/tests/assertions.test.ts": 2973, + "packages/testing/tests/boundary-contract.test.ts": 1553, + "packages/testing/tests/cli.test.ts": 4523, + "packages/testing/tests/execution-harness.test.ts": 6276, + "packages/testing/tests/replay.test.ts": 2131, + "packages/testing/tests/smoke.test.ts": 1803, + "packages/testing/tests/test-component.test.ts": 2875, + "packages/testing/tests/testing-activation.test.ts": 2504, + "packages/testing/tests/testing-boundary.test.ts": 1056, + "packages/testing/tests/testing-mode.test.ts": 3089, + "packages/testing/tests/use-testing.test.ts": 1818, + "packages/web/tests/assets.test.ts": 27, + "packages/web/tests/client-logic.test.ts": 32, + "packages/web/tests/compile.test.ts": 866, + "packages/web/tests/component.test.ts": 1393, + "packages/web/tests/declaration.test.ts": 624, + "packages/web/tests/document.test.ts": 724, + "packages/web/tests/elicitation.test.ts": 502, + "packages/web/tests/live-form.test.ts": 457, + "packages/web/tests/markdown.test.ts": 135, + "packages/web/tests/opener.test.ts": 475, + "packages/web/tests/page.test.ts": 30, + "packages/web/tests/responder.test.ts": 785, + "packages/web/tests/server-lifecycle.test.ts": 607, + "packages/web/tests/server.test.ts": 990, + "packages/workflow/tests/generated-observations.test.ts": 519, + "packages/workflow/tests/retained-run.test.ts": 1181, + "packages/workflow/tests/service-denial.test.ts": 53, + "packages/workflow/tests/workflow-bundle.test.ts": 740, + "packages/workflow/tests/workflow-definition.test.ts": 321, + "packages/workflow/tests/workflow-fork.test.ts": 283, + "packages/workflow/tests/workflow-run.test.ts": 1000, + "packages/workflow/tests/workspace-effect.test.ts": 1310, + "scripts/tests/adapter-vendor.test.ts": 146, + "scripts/tests/bootstrap-npm-package.test.ts": 4873, + "scripts/tests/bump-version.test.ts": 73, + "scripts/tests/changed-paths.test.ts": 32, + "scripts/tests/ci-workflow.test.ts": 1616, + "scripts/tests/consumer-cycle.test.ts": 91, + "scripts/tests/filesystem-contract-workflow.test.ts": 67, + "scripts/tests/main-green.test.ts": 279, + "scripts/tests/main-health.test.ts": 101, + "scripts/tests/no-module-scoped-registry.test.ts": 753, + "scripts/tests/no-redundant-test-scope.test.ts": 4157, + "scripts/tests/no-sync-filesystem.test.ts": 3835, + "scripts/tests/no-yield-in-finally.test.ts": 733, + "scripts/tests/oxlint-policy.test.ts": 8746, + "scripts/tests/packaged-document.test.ts": 64, + "scripts/tests/prefer-effection-operation.test.ts": 5312, + "scripts/tests/prefer-effection-result.test.ts": 3540, + "scripts/tests/publish-workflow-membership.test.ts": 84, + "scripts/tests/review-infrastructure.test.ts": 1660, + "scripts/tests/runtime-exclusions.test.ts": 277, + "scripts/tests/runtime-tests.test.ts": 74, + "scripts/tests/scope-bound-event-registration.test.ts": 3871, + "scripts/tests/staged-write.test.ts": 68, + "scripts/tests/test-file-discovery.test.ts": 211, + "scripts/tests/test-shards.test.ts": 38, + "scripts/tests/test-weights.test.ts": 205, + "scripts/tests/tracked.test.ts": 34, + "scripts/tests/verify-coordinator.test.ts": 216, + "scripts/tests/version-lockstep.test.ts": 111, + "scripts/tests/web-client-module.test.ts": 53, + "scripts/tests/workspace.test.ts": 42 } } }