@termwright/conformance 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gorce-ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # @termwright/conformance
2
+
3
+ The suites that decide whether an implementation of termwright is actually one:
4
+ runnable fixtures, the driver conformance matrix from the origin spec (§20), and
5
+ an **adapter contract suite you can run against any adapter, in any language**.
6
+
7
+ Nothing depends on this package; it is allowed to depend on everything.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pnpm add -D @termwright/conformance
13
+ ```
14
+
15
+ `vitest` is an optional peer dependency, needed only to run the exported suite.
16
+
17
+ ## Certifying an adapter
18
+
19
+ The only thing an adapter author writes is the binding between their app and the
20
+ suite. It drives the adapter as a subprocess and looks at bytes and frames, so a
21
+ Python, Go or Rust adapter certifies exactly like the TypeScript one.
22
+
23
+ ```ts
24
+ // my-adapter.conformance.test.ts
25
+ import { runAdapterConformance } from '@termwright/conformance';
26
+
27
+ await runAdapterConformance({
28
+ name: 'termwright-py',
29
+ spawn: () => ({ command: ['python', 'examples/demo_app.py'] }),
30
+ // Optional: the same UI with the adapter compiled out. When given, the
31
+ // dormant run is compared against it byte for byte.
32
+ baseline: () => ({ command: ['python', 'examples/demo_app.py'], env: { PLAIN: '1' } }),
33
+ ready: 'Ready',
34
+ interaction: { input: '\t', expect: '[Save]' },
35
+ quit: { input: '', exitCode: 0 },
36
+ columns: 80,
37
+ rows: 24,
38
+ expectAbsoluteBounds: true,
39
+ });
40
+ ```
41
+
42
+ The byte-for-byte comparison covers the **startup** stream, with nothing
43
+ written to the child: a pseudo-terminal echoes the suite's own keystrokes, so a
44
+ stream containing our input compares the tty's timing rather than the adapter's
45
+ output (measured on the Ink fixture: 3 mismatches in 30 pairs with input —
46
+ always a stray `0x09`, the tab the suite itself sent — and 0 in 40 without).
47
+
48
+ Two requirements the registration has to respect, because the suite exercises
49
+ the app rather than mocking it: `interaction.input` is sent **more than once**,
50
+ so pick something whose repetition is harmless; and `quit.input` must work from
51
+ **any** state that repetition can reach — a key that quits only while one widget
52
+ has focus is not a quit input.
53
+
54
+ Pass `requires` to declare the toolchain the adapter needs. When the probe
55
+ fails, the whole registration skips and the reason appears in the block's name,
56
+ exactly as a missing pseudo-terminal does:
57
+
58
+ ```ts
59
+ requires: {
60
+ probe: ['python3', '-c', 'import termwright, textual'],
61
+ label: 'python3 with termwright and textual installed',
62
+ },
63
+ ```
64
+
65
+ An adapter that announces the `logs` capability declares how to exercise it,
66
+ and the obligations above are then asserted rather than skipped:
67
+
68
+ ```ts
69
+ logs: { input: 'l', expect: 'conformance log record' },
70
+ ```
71
+
72
+ `input` is optional: omit it for an app that logs on its own, as the tview
73
+ example does at startup, and the obligation waits for the record instead of
74
+ provoking one.
75
+
76
+ Conventions the fixture has to opt into — an annotated test id, an empty
77
+ textbox, a container with no label — are declared under `conventions`; the rest
78
+ run for every adapter:
79
+
80
+ ```ts
81
+ conventions: {
82
+ emptyTextboxTestId: 'reason',
83
+ readmePath: 'clients/go/README.md',
84
+ },
85
+ ```
86
+
87
+ Deviations are **not** declared here. They are read from the adapter's own
88
+ `## Deviations` section, which is where rule 6 puts them and where a user reads
89
+ them; repeating them in a registration would give two copies of one fact that
90
+ eventually disagree. That gives three outcomes rather than two: compliant, a
91
+ failure the README declares (a documented limitation, not an error), and a
92
+ failure it does not (an error).
93
+
94
+ Every run writes a per-adapter roll-up of those declarations, which
95
+ `pnpm conformance` prints under the matrix. It is generated rather than
96
+ maintained: a hand-written table of per-adapter gaps went stale within a round
97
+ of being written, and a stale overview in a document people trust is worse than
98
+ none.
99
+
100
+ Rules 1, 2 and 4 cannot be judged in full from outside a subprocess, so the
101
+ README check is advisory: a missing `## Deviations` heading writes a warning to
102
+ stderr rather than failing the run.
103
+
104
+ It checks the five obligations an adapter has:
105
+
106
+ | Obligation | What is asserted |
107
+ |---|---|
108
+ | Dormant rule | Without `TERMWRIGHT_ENDPOINT` it opens no channel and writes no marker; with `baseline`, byte-for-byte identical startup output |
109
+ | Tree before input | Once the handshake completes and *before any input*, the tree is non-empty and has at least one node a locator could address. Opt out with `treeBeforeInput: {required: false, reason}` |
110
+ | Handshake | `hello` first and once, correct protocol id, non-empty adapter identity, capabilities from the closed set |
111
+ | Snapshot validity | Every snapshot passes `validateSnapshot`, carries this session's id, has resolvable parents and monotonic revisions |
112
+ | Revision ordering | For each revision: snapshot → `revision-commit` → a marker that verifies against the session token, markers strictly increasing |
113
+ | Channel loss | Cutting the socket leaves the application rendering and alive, and the adapter does not reconnect |
114
+ | Logs | An adapter that did not announce `logs` sends none. One that declares them in the registration must deliver a record whose `seq` is unique and increasing and whose message never appears on the terminal |
115
+ | Deltas (when announced) | With `subscribe: 'diffs'`, the deltas an adapter emits compose — through the protocol's own `applyTreeDelta` — to the same tree it reports when asked with `get-tree` |
116
+ | Conventions | The machine-checkable half of the protocol README's "Adapter semantics conventions": containers are not named from their content (rule 2), an annotated test id reaches the wire (rule 3), an empty textbox publishes `value: ''` and no value is derived outside `{textbox, progressbar}` or from a boolean (rule 5) |
117
+
118
+ `await` it at the top level: `vitest` is imported dynamically so the package can
119
+ also be used from a plain script.
120
+
121
+ ## Fixtures
122
+
123
+ `CONFORMANCE_FIXTURES` returns absolute paths to programs you can launch with
124
+ `node <path>`:
125
+
126
+ | Fixture | Purpose | Dependencies |
127
+ |---|---|---|
128
+ | `generic()` | Uninstrumented app: menu, colours, mouse/paste/focus modes, Unicode, alternate screen, scrollback (§20.1) | none |
129
+ | `prompt()` | Shell-shaped app emitting OSC 133 marks; `--marks=off` suppresses them, `--work=<ms>` sets the command duration | none |
130
+ | `adversarialPeer()` | Raw wire peer; takes a scenario name as `argv[2]` (§20.3) | none |
131
+ | `inkProbe()` | Ordinary `ink.render` app launched through the zero-config Ink probe | `ink`, `react`, `@termwright/probe-ink` |
132
+
133
+ The first three import nothing at all — the adversarial peer re-derives the
134
+ framing and the marker MAC from the specification rather than importing
135
+ `@termwright/protocol`, so a drift between spec and implementation shows up
136
+ instead of cancelling out.
137
+
138
+ ```ts
139
+ import { CONFORMANCE_FIXTURES } from '@termwright/conformance';
140
+ import { launchTerminal } from '@termwright/driver';
141
+
142
+ const terminal = await launchTerminal({ command: ['node', CONFORMANCE_FIXTURES.generic()] });
143
+ ```
144
+
145
+ ## Running the matrix
146
+
147
+ The MCP suite drives `@termwright/mcp` over real HTTP with several concurrent
148
+ sessions, and checks close ownership against real pids rather than against the
149
+ registry's bookkeeping — a registry can forget a session while its terminals
150
+ keep running, and only a pid probed afterwards tells the two apart.
151
+
152
+ ```sh
153
+ pnpm --filter @termwright/conformance conformance # every suite, one matrix
154
+ pnpm --filter @termwright/conformance test # plain vitest
155
+ pnpm --filter @termwright/conformance test:hostile # adversarial suite, 128 MB heap cap
156
+ ```
157
+
158
+ ```
159
+ area spec result tests time
160
+ generic fallback §20.1 pass 10/10 2.0s
161
+ hostile peer §20.3 pass 25/25 14.5s
162
+ interaction §20.4 pass 12/12 2.6s
163
+ readiness + env §5.3 pass 10/10 2.2s
164
+ adapter contract (py/go) §7 pass, 8 skip 6/14 0.6s
165
+ hostile peer @ 128 MB heap §10 pass 25/25 14.4s
166
+ ```
167
+
168
+ Certifying the py/go rows needs their toolchains on the runner
169
+ (`pip install -e clients/python[dev]` and a Go toolchain); without them those
170
+ rows skip honestly, with the probe's failure on stderr.
171
+
172
+ A partly-skipped area is reported as such rather than as a clean pass: the
173
+ language adapters skip their whole registration when the toolchain is absent,
174
+ and a matrix that hid it would claim coverage the machine never produced.
175
+
176
+ Sessions launch with the driver's secret-safe `envMode: 'replace'` default, so
177
+ a fixture only sees the documented allowlist plus what a suite declares.
178
+
179
+ Every suite needs a pseudo-terminal and skips itself where none can be opened;
180
+ `TERMWRIGHT_SKIP_PTY=1` skips them explicitly. A run where everything skipped
181
+ says so rather than reporting success.
182
+
183
+ ## Why a second, smaller driver
184
+
185
+ `AdapterProbe` speaks the protocol itself — endpoint, handshake, framing, marker
186
+ verification — instead of using `@termwright/driver`. The driver deliberately
187
+ hides frame ordering behind a settled tree, which is the right API for testing
188
+ applications and the wrong one for testing adapters. The probe is exported for
189
+ checks the suite does not cover; it never renders, locates or acts.
@@ -0,0 +1,351 @@
1
+ import { AdapterToDriverMessage, LogRecord, TreeDelta, SemanticSnapshot } from '@termwright/protocol';
2
+ import { LaunchOptions, TerminalHarness } from '@termwright/driver';
3
+
4
+ /** How a fixture is started. Everything else about it is opaque to the probe. */
5
+ interface AdapterCommand {
6
+ readonly command: readonly string[];
7
+ readonly env?: Readonly<Record<string, string>>;
8
+ readonly cwd?: string;
9
+ }
10
+ /** One message the adapter sent, stamped with the stdout position at arrival. */
11
+ interface RecordedMessage {
12
+ readonly message: AdapterToDriverMessage;
13
+ /** Bytes of stdout the probe had received when this frame was parsed. */
14
+ readonly stdoutBytes: number;
15
+ readonly atMs: number;
16
+ }
17
+ /** One verified render marker found in stdout. */
18
+ interface RecordedMarker {
19
+ readonly revision: number;
20
+ /** Offset of the marker's first byte in the stdout stream. */
21
+ readonly offset: number;
22
+ readonly atMs: number;
23
+ }
24
+ /** A frame the probe refused; a conforming adapter produces none. */
25
+ interface RecordedFault {
26
+ readonly code: string;
27
+ readonly detail: string;
28
+ }
29
+ interface ProbeOptions {
30
+ readonly columns?: number;
31
+ readonly rows?: number;
32
+ /** Set `false` to withhold the instrumentation env — the dormant-run case. */
33
+ readonly instrument?: boolean;
34
+ /**
35
+ * What the probe asks the adapter to push. `'diffs'` is a preference, not a
36
+ * prohibition: an adapter may still send a full tree when a delta would not
37
+ * pay for itself, and the first publication always is one.
38
+ */
39
+ readonly subscribe?: 'snapshots' | 'diffs';
40
+ }
41
+ /** Everything the probe observed, readable while the child is still running. */
42
+ interface ProbeObservation {
43
+ readonly messages: readonly RecordedMessage[];
44
+ readonly markers: readonly RecordedMarker[];
45
+ readonly faults: readonly RecordedFault[];
46
+ readonly connections: number;
47
+ readonly stdout: Uint8Array;
48
+ /** Raw bytes decoded as UTF-8: what was written, in order, escapes included. */
49
+ readonly text: string;
50
+ /** The visible grid, one row per line — what a user would actually see. */
51
+ readonly screen: string;
52
+ /** Application log records the adapter sent, in arrival order. */
53
+ readonly logs: readonly LogRecord[];
54
+ /** Deltas the adapter sent, in arrival order. */
55
+ readonly deltas: readonly TreeDelta[];
56
+ /**
57
+ * The tree obtained by composing every snapshot and delta received, in order,
58
+ * with the protocol's own `applyTreeDelta`. This is the oracle an adapter's
59
+ * deltas are checked against: a producer that also composed would only prove
60
+ * it agrees with itself.
61
+ */
62
+ readonly composed: SemanticSnapshot | null;
63
+ /** Why composition stopped, when it did. A conforming adapter produces none. */
64
+ readonly compositionError: string | null;
65
+ }
66
+ /**
67
+ * Runs one fixture under a pseudo-terminal with a protocol endpoint attached.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const probe = await AdapterProbe.start({ command: ['node', 'app.mjs'] }, {});
72
+ * await probe.waitForText('Ready');
73
+ * await probe.write('\t');
74
+ * const { messages, markers } = probe.observe();
75
+ * await probe.stop();
76
+ * ```
77
+ */
78
+ declare class AdapterProbe {
79
+ #private;
80
+ readonly sessionId: string;
81
+ readonly token: string;
82
+ private constructor();
83
+ /** Creates the endpoint (unless dormant), then spawns the fixture. */
84
+ static start(command: AdapterCommand, options?: ProbeOptions): Promise<AdapterProbe>;
85
+ /** Everything observed so far. Safe to call at any point. */
86
+ observe(): ProbeObservation;
87
+ /** The visible grid as text, trailing whitespace trimmed per row. */
88
+ screenText(): string;
89
+ /** The child's exit status, or `null` while it is still running. */
90
+ get exitStatus(): {
91
+ code: number | null;
92
+ signal: string | null;
93
+ } | null;
94
+ /** Writes raw bytes to the child, exactly as a terminal would. */
95
+ write(input: string): Promise<void>;
96
+ /**
97
+ * Resolves once `needle` appears on the rendered grid.
98
+ *
99
+ * Matching the byte stream instead would only work for adapters that happen
100
+ * to write their text contiguously: a framework that positions each run of
101
+ * cells never emits `focus: reject` as those twelve bytes in a row.
102
+ */
103
+ waitForText(needle: string | RegExp, timeoutMs?: number): Promise<void>;
104
+ /**
105
+ * Resolves once `predicate` holds over the current observation.
106
+ *
107
+ * `what` names the thing being waited for, and the failure carries what the
108
+ * probe could see when it gave up. "Condition never became true" is not a
109
+ * result anybody can act on: an adapter that never connected, one that
110
+ * connected and published nothing, and one whose binary died all produce it,
111
+ * and only the observation tells them apart.
112
+ */
113
+ waitFor(predicate: (observation: ProbeObservation) => boolean, timeoutMs?: number, what?: string): Promise<void>;
114
+ /** What the probe has seen so far, for a failure that has to explain itself. */
115
+ describe(): string;
116
+ /** Waits for the child to exit and returns its status. */
117
+ waitForExit(timeoutMs?: number): Promise<{
118
+ code: number | null;
119
+ signal: string | null;
120
+ }>;
121
+ /** Cuts the semantic channel without touching the child: the disconnect case. */
122
+ cutChannel(): void;
123
+ /** Stops the child and releases the endpoint. Idempotent. */
124
+ stop(): Promise<void>;
125
+ /**
126
+ * Asks the adapter for a full tree and resolves with it.
127
+ *
128
+ * This is what turns composition into a check rather than a belief: the
129
+ * locally composed tree is compared against one the adapter built itself.
130
+ */
131
+ requestTree(timeoutMs?: number): Promise<SemanticSnapshot | null>;
132
+ }
133
+ /** The marker prefix, re-exported so suites can assert on dormant output. */
134
+ declare const MARKER_TEXT_PREFIX = "\u001B]8487;twm;";
135
+
136
+ /** How to start, drive and stop the adapter under test. */
137
+ interface AdapterConformanceOptions {
138
+ /** Name of the adapter, used in the test titles. */
139
+ readonly name: string;
140
+ /** Command that starts the instrumented application. */
141
+ spawn(): AdapterCommand;
142
+ /**
143
+ * Optional command rendering the same UI with the adapter compiled out. When
144
+ * given, the dormant run is compared against it byte for byte — the strongest
145
+ * form of the dormant rule. Without it, a dormant run is only checked for
146
+ * silence on the wire.
147
+ */
148
+ baseline?(): AdapterCommand;
149
+ /** Text that proves the first frame reached the terminal. */
150
+ readonly ready: string | RegExp;
151
+ /**
152
+ * An input that changes the screen, and the text that proves it landed.
153
+ *
154
+ * The suite sends it **more than once** — a dormant run, a run that has to
155
+ * produce a second revision, and a run after the channel was cut all need a
156
+ * render. Pick something whose repetition is harmless.
157
+ */
158
+ readonly interaction: {
159
+ readonly input: string;
160
+ readonly expect: string | RegExp;
161
+ };
162
+ /**
163
+ * An input that makes the application exit, and the status it exits with.
164
+ *
165
+ * It must work from **any** state repeated `interaction` can reach. A key
166
+ * that quits only while a particular widget has focus is not a quit input:
167
+ * the tview example's documented `q` types into its text field once focus has
168
+ * cycled that far, so its registration uses Ctrl+C instead.
169
+ */
170
+ readonly quit: {
171
+ readonly input: string;
172
+ readonly exitCode?: number;
173
+ };
174
+ readonly columns?: number;
175
+ readonly rows?: number;
176
+ /** Assert that published bounds are viewport-absolute (an `absolute-bounds` claim). */
177
+ readonly expectAbsoluteBounds?: boolean;
178
+ /**
179
+ * Opt out of the "publishes a tree before any input" obligation.
180
+ *
181
+ * By default an adapter must publish a non-empty tree once the handshake
182
+ * completes, with no input sent — an app that is addressable only after the
183
+ * first keystroke is not addressable at all to a driver that has just
184
+ * launched it. Some apps legitimately render nothing until an event arrives;
185
+ * pass a reason, which is printed in the test title so the exemption stays
186
+ * visible rather than becoming folklore.
187
+ */
188
+ readonly treeBeforeInput?: {
189
+ readonly required: false;
190
+ readonly reason: string;
191
+ };
192
+ /**
193
+ * How to make the application log, for an adapter that announces the `logs`
194
+ * capability. Without it the log obligations are skipped; with it they are
195
+ * asserted, and an adapter that announces `logs` but never delivers one
196
+ * fails here rather than in a user's test.
197
+ */
198
+ /**
199
+ * How to check the normative adapter conventions (protocol README, "Adapter
200
+ * semantics conventions"). Rules 1, 2 and 4 are largely judgement calls from
201
+ * outside; what is listed here is what a subprocess can actually observe.
202
+ *
203
+ * A rule an adapter cannot follow is a *declared deviation*, not a failure:
204
+ * name it in `deviations` and the matching check is skipped with the reason
205
+ * in the test title, so the exemption stays visible instead of becoming
206
+ * folklore.
207
+ */
208
+ readonly conventions?: {
209
+ /** A test id the fixture sets by author annotation (rule 3). */
210
+ readonly annotatedTestId?: string;
211
+ /** A textbox whose field is empty, to prove `value: ''` (rule 5). */
212
+ readonly emptyTextboxTestId?: string;
213
+ /** A container with no name of its own, wrapping text (rule 2). */
214
+ readonly unnamedContainerTestId?: string;
215
+ /**
216
+ * Test ids whose `value` is author-annotated. The role gate in rule 5
217
+ * bounds *derived* values; an annotation may put one on any role, and only
218
+ * the registration knows which is which.
219
+ */
220
+ readonly annotatedValues?: readonly string[];
221
+ /**
222
+ * The adapter's README. Its `## Deviations` section is the single source of
223
+ * truth for what this adapter cannot do (rule 6), so a declared limitation
224
+ * is read from there rather than repeated in the registration — two copies
225
+ * of the same fact disagree eventually, and the README is the one a user
226
+ * reads.
227
+ */
228
+ readonly readmePath?: string;
229
+ };
230
+ readonly logs?: {
231
+ /**
232
+ * Input that makes the application write a record. Omit it for an app that
233
+ * logs on its own (at startup, say) — the obligation then waits for a
234
+ * record rather than provoking one.
235
+ */
236
+ readonly input?: string;
237
+ /** A substring of the logged message, used to prove it stayed off-screen. */
238
+ readonly expect: string;
239
+ };
240
+ /** How long the handshake may take. Default 10 s. */
241
+ readonly timeoutMs?: number;
242
+ /**
243
+ * A command that must succeed before this adapter can be certified here —
244
+ * its interpreter, or a build step that produces the binary `spawn` runs.
245
+ * When it fails the whole suite skips and the reason is in the block's name,
246
+ * exactly as a missing pseudo-terminal does.
247
+ */
248
+ readonly requires?: {
249
+ readonly probe: readonly string[];
250
+ readonly label: string;
251
+ readonly cwd?: string;
252
+ readonly timeoutMs?: number;
253
+ };
254
+ }
255
+ /**
256
+ * Rule numbers declared in an adapter's `## Deviations` section.
257
+ *
258
+ * Three shapes are in use and all are accepted, because the suite should not
259
+ * dictate anyone's prose: `**Rule 2 — …**` (a heading per entry, Ink),
260
+ * `- **…** (rule 3).` (the number at the end of a bullet, the language
261
+ * clients), and a markdown table whose first column is `2 — …` (OpenTUI).
262
+ *
263
+ * Entries that name no rule are kept under `other`: they are still declared
264
+ * limitations, and dropping them would make the roll-up quietly incomplete.
265
+ */
266
+ declare function parseDeclaredDeviations(readme: string): Map<string, string[]>;
267
+ /**
268
+ * Registers the adapter contract suite for one adapter.
269
+ *
270
+ * Call it at the top level of a test file; it declares its own `describe`.
271
+ *
272
+ * `vitest` is imported dynamically, so the package stays importable from a
273
+ * plain script that only wants the fixture paths or the probe. That is why the
274
+ * function is async: `await` it at the top level of the test file, which is
275
+ * where vitest collects the suite from.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * await runAdapterConformance({
280
+ * name: 'my-framework-probe',
281
+ * spawn: () => ({ command: ['node', 'app.mjs'] }),
282
+ * ready: 'Ready',
283
+ * interaction: { input: '\t', expect: '[Save]' },
284
+ * quit: { input: 'q', exitCode: 0 },
285
+ * });
286
+ * ```
287
+ */
288
+ declare function runAdapterConformance(options: AdapterConformanceOptions): Promise<void>;
289
+
290
+ /**
291
+ * Absolute path of a fixture that ships with this package.
292
+ *
293
+ * The fixtures are shipped as sources rather than bundled, because they are
294
+ * meant to be launched as `node <path>` by suites in other packages and in
295
+ * other languages' CI. They are therefore located from the package root, which
296
+ * is the one anchor that is the same whether this module was loaded from `src/`
297
+ * during development or from the bundled `dist/`.
298
+ */
299
+ declare function fixturePath(name: string): string;
300
+ /** The fixtures published for adapter authors and other packages' suites. */
301
+ declare const CONFORMANCE_FIXTURES: Readonly<{
302
+ /** Uninstrumented app: proves the generic fallback (§20.1). */
303
+ generic: () => string;
304
+ /** Shell-shaped app emitting OSC 133 marks; `--marks=off` suppresses them. */
305
+ prompt: () => string;
306
+ /** Normal-render Ink app used to exercise launch-time probe attachment. */
307
+ inkProbe: () => string;
308
+ /** Hostile wire peer; takes a scenario name as its first argument (§20.3). */
309
+ adversarialPeer: () => string;
310
+ }>;
311
+ /**
312
+ * Whether this machine can open a pseudo-terminal at all.
313
+ *
314
+ * @returns `false` when `TERMWRIGHT_SKIP_PTY=1` or spawning a trivial child
315
+ * through the PTY backend throws. Probed once and cached.
316
+ */
317
+ declare function ptyAvailable(): boolean;
318
+ /** `process.env` with the `undefined` values dropped, as PTY spawning requires. */
319
+ declare function environment(extra?: Readonly<Record<string, string>>): Record<string, string>;
320
+ /** Options every conformance session shares; suites override what they need. */
321
+ interface FixtureLaunchOptions extends Partial<Omit<LaunchOptions, 'command'>> {
322
+ /** Extra arguments appended to `node <fixture>`. */
323
+ readonly args?: readonly string[];
324
+ /** Attach the zero-config framework probe to the otherwise normal command. */
325
+ readonly probe?: 'ink';
326
+ /**
327
+ * Text that proves the fixture started drawing. Waiting for it here rather
328
+ * than in each suite is what lets the failure be diagnosed: a fixture that
329
+ * printed nothing at all failed to start, which is a very different problem
330
+ * from one that started and drew the wrong thing.
331
+ */
332
+ readonly ready?: string | RegExp;
333
+ }
334
+ /**
335
+ * A set of sessions closed together, so one wedged fixture cannot leak a child
336
+ * process into the next test.
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * const sessions = createSessionPool();
341
+ * afterEach(sessions.closeAll);
342
+ * const terminal = await sessions.launch(CONFORMANCE_FIXTURES.generic());
343
+ * ```
344
+ */
345
+ interface SessionPool {
346
+ launch(fixture: string, options?: FixtureLaunchOptions): Promise<TerminalHarness>;
347
+ closeAll(): Promise<void>;
348
+ }
349
+ declare function createSessionPool(): SessionPool;
350
+
351
+ export { type AdapterCommand, type AdapterConformanceOptions, AdapterProbe, CONFORMANCE_FIXTURES, type FixtureLaunchOptions, MARKER_TEXT_PREFIX, type ProbeObservation, type ProbeOptions, type RecordedFault, type RecordedMarker, type RecordedMessage, type SessionPool, createSessionPool, environment, fixturePath, parseDeclaredDeviations, ptyAvailable, runAdapterConformance };