@anureo/conformance 0.2.0 → 0.3.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/src/fake.js ADDED
@@ -0,0 +1,284 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { HarnessSdkError, } from "@anureo/sdk";
3
+ import { AHP_PROTOCOL_CONTRACT, assertRequest, negotiateVersion, } from "@anureo/protocol";
4
+ /** Default advertised capabilities. Adapter reads clone the current contents of this mutable template. */
5
+ export const FAKE_CAPABILITIES = {
6
+ session: { resume: true, fork: ["before", "through"], revert: ["conversation", "files", "both"], history: true },
7
+ input: { delivery: ['native', 'afterTurn'], steer: { target: 'turn', atomicMatch: true }, interrupt: { targets: ['turn', 'session'], reportsPendingInputs: true } },
8
+ turn: { outputSchema: true, inputModalities: ["text", "image", "file"], overrides: ["model"] },
9
+ goal: { actions: ["set", "clear"] }, compact: true, review: true,
10
+ interactions: ["approval", "userInput", "elicitation", "dynamicTool"],
11
+ config: { options: [{ id: "model", scope: "session", schema: { type: "string" } }] },
12
+ };
13
+ /** Create a fresh adapter to pass to createClient. Await disposal of the client when the fixture is done. */
14
+ export function createScriptedAdapter(options) {
15
+ return new ScriptedHarnessAdapter(options);
16
+ }
17
+ /**
18
+ * Supply runConformance with a fresh in-memory adapter per case, including held turns and
19
+ * observation faults. A supplied capability set replaces the defaults for applicability checks.
20
+ * The runner owns each client and its adapter.
21
+ */
22
+ export function createScriptedConformanceFactory(capabilities) {
23
+ return (scenario) => {
24
+ const adapter = createScriptedAdapter({ capabilities, execute: scenarioExecutor(scenario) });
25
+ const sessionId = "conformance-session";
26
+ const driver = {
27
+ adapter,
28
+ openParams: { requestId: "conformance-open", mode: "new", binding: { sessionId, adapterId: "scripted", runtimeConfigId: "conformance-runtime" }, cwd: "/tmp" },
29
+ };
30
+ // executeScenarioTurn calls afterStart after admission, when openSession has recorded the sink.
31
+ if (scenario === "backpressure") {
32
+ driver.afterStart = async () => {
33
+ adapter.sinks.get(sessionId).tryEmit({ type: "notice", payload: { severity: "info", code: "FLOOD", message: "x".repeat(8192) } });
34
+ };
35
+ driver.runtimeOptions = { queue: { maxFrameBytes: 4096 } };
36
+ }
37
+ if (scenario === "disconnect") {
38
+ driver.afterStart = async () => {
39
+ await adapter.sinks.get(sessionId).fail(new HarnessSdkError("CONNECTION_CLOSED", "Conformance disconnection", false, true));
40
+ };
41
+ }
42
+ return driver;
43
+ };
44
+ }
45
+ /**
46
+ * Exercise the SDK with an in-memory adapter that records opens and backend dispatches. By default
47
+ * a submission echoes text and publishes completion before acknowledging it. Hooks can supply
48
+ * other behavior. Await Client.dispose to release SDK ownership while retaining fixture records
49
+ * for assertions.
50
+ */
51
+ export class ScriptedHarnessAdapter {
52
+ options;
53
+ /** Retain fixture options by reference for subsequent capability reads, opens and dispatches. */
54
+ constructor(options = {}) {
55
+ this.options = options;
56
+ }
57
+ /** Backend dispatches in arrival order, including calls intercepted by execute. */
58
+ calls = [];
59
+ /** Detached open attempts, including attempts whose hook or fork validation fails. */
60
+ opened = [];
61
+ /** Latest supplied event sink per session ID, retained for fixture-controlled publication/failure. */
62
+ sinks = new Map();
63
+ /** Latest created backend per session ID, recorded before fork validation and onOpen. */
64
+ backends = new Map();
65
+ /** Number of disposal calls received, including any repeated direct calls. */
66
+ disposeCount = 0;
67
+ /** Return detached declared capabilities, using FAKE_CAPABILITIES when omitted. */
68
+ get capabilities() { return structuredClone(this.options.capabilities ?? FAKE_CAPABILITIES); }
69
+ /**
70
+ * Return scripted runtime metadata and cloned capabilities after negotiating AHP v1. The SDK
71
+ * adapter wrapper forwards the result to Client.initialize for validation and storage. Malformed
72
+ * requests and unsupported versions reject.
73
+ */
74
+ async initialize(params) {
75
+ assertRequest("initialize", params);
76
+ negotiateVersion(params.protocolVersions);
77
+ return { protocolVersion: 1, protocolContract: AHP_PROTOCOL_CONTRACT, runtime: { name: "scripted", version: "1.0.0", adapter: "conformance/0.1.0" }, capabilities: this.capabilities };
78
+ }
79
+ /**
80
+ * Supply OutputSession.open with a backend for SDK attachment. Record the backend and sink
81
+ * before copying fork history so a failed setup is observable. Missing source or boundary
82
+ * rejects with SESSION_NOT_FOUND or TURN_NOT_FOUND, respectively. Await onOpen before returning
83
+ * the backend. Hook failures reject and leave the recorded fixture state available.
84
+ */
85
+ async openSession(params, sink) {
86
+ this.opened.push(structuredClone(params));
87
+ this.sinks.set(params.binding.sessionId, sink);
88
+ const backend = new ScriptedBackendSession(this, params, sink);
89
+ this.backends.set(params.binding.sessionId, backend);
90
+ const fork = params.fork;
91
+ if (params.mode === "fork" && fork)
92
+ this.copyForkHistory(fork, backend);
93
+ await this.options.onOpen?.(params, sink);
94
+ return backend;
95
+ }
96
+ /** Count adapter release. The SDK wrapper calls this after session cleanup attempts. */
97
+ async dispose() { this.disposeCount++; }
98
+ /**
99
+ * Give openSession a detached prefix of the source history before setup hooks run. A missing
100
+ * source or boundary throws so open rejects with the fork's fixture records still available.
101
+ */
102
+ copyForkHistory(fork, backend) {
103
+ const source = this.backends.get(fork.sourceSessionId);
104
+ if (!source)
105
+ throw new HarnessSdkError("SESSION_NOT_FOUND");
106
+ const retained = historyBoundaryIndex(source.history, fork.boundary);
107
+ backend.history.push(...structuredClone(source.history.slice(0, retained)));
108
+ }
109
+ }
110
+ /**
111
+ * Inspect or customize the in-memory session recorded by ScriptedHarnessAdapter.openSession.
112
+ * OutputSession dispatches validated SDK requests to this backend. Direct fixture calls to execute
113
+ * bypass those SDK guards.
114
+ */
115
+ export class ScriptedBackendSession {
116
+ adapter;
117
+ sink;
118
+ /** Requested identity with a deterministic scripted nativeRef, adopted by the SDK at attachment. */
119
+ binding;
120
+ /** Capabilities cloned from the adapter at construction and shared with the fixture state. */
121
+ capabilities;
122
+ /** Mutable native history. Default backend history/read returns this array by reference. */
123
+ history = [];
124
+ /** Number of disposal calls received, including any repeated direct calls. */
125
+ disposeCount = 0;
126
+ /** Initial attachment snapshot. Default configuration dispatch replaces it without publishing a state event. */
127
+ state;
128
+ /** Build a ready attachment snapshot and retain the adapter and event sink for dispatch. */
129
+ constructor(adapter, params,
130
+ /** Callbacks supplied at open for event publication, interactions and source faults. */
131
+ sink) {
132
+ this.adapter = adapter;
133
+ this.sink = sink;
134
+ this.binding = { ...params.binding, nativeRef: { provider: "scripted", value: `native:${params.binding.sessionId}` } };
135
+ this.capabilities = adapter.capabilities;
136
+ this.state = {
137
+ binding: this.binding,
138
+ capabilities: this.capabilities,
139
+ status: "ready",
140
+ activeTurnIds: [],
141
+ config: params.config ?? {},
142
+ ...(params.model ? { model: params.model } : {}),
143
+ capabilityRevision: 1,
144
+ };
145
+ }
146
+ /** Await ordered sink publication. Supply the owning turn ID for item events. Sink failures reject. */
147
+ async emit(payload, turnId) { await this.sink.emit(payload, { turnId }); }
148
+ async execute(method, params) {
149
+ const call = { method, params };
150
+ this.adapter.calls.push(call);
151
+ const custom = await this.adapter.options.execute?.(call, this.sink);
152
+ if (custom !== undefined)
153
+ return custom;
154
+ switch (method) {
155
+ case "input/submit":
156
+ return await this.submitInput(params);
157
+ case "turn/steer": {
158
+ const request = params;
159
+ await this.emit({ type: "turn/completed", payload: { turnId: request.expectedTurnId, status: "completed" } });
160
+ return { submissionId: request.submissionId, turnId: request.expectedTurnId, accepted: true };
161
+ }
162
+ case "turn/interrupt": {
163
+ const { turnId } = params;
164
+ await this.emit({ type: "turn/completed", payload: { turnId, status: "interrupted" } });
165
+ return { accepted: true, turnId };
166
+ }
167
+ case "session/config/set": {
168
+ this.state = {
169
+ ...this.state,
170
+ config: { ...this.state.config, ...params.patch },
171
+ capabilityRevision: this.state.capabilityRevision + 1,
172
+ };
173
+ return this.state;
174
+ }
175
+ case "session/revert": {
176
+ const request = params;
177
+ if (request.scope !== "files") {
178
+ this.history.splice(historyBoundaryIndex(this.history, request.boundary));
179
+ }
180
+ return { appliedScope: request.scope, boundary: request.boundary };
181
+ }
182
+ case "history/read":
183
+ return { items: this.history, snapshotToken: "fake-snapshot", source: this.binding.nativeRef };
184
+ case "goal/set":
185
+ return { goal: params.goal, status: "active" };
186
+ case "goal/clear":
187
+ return { goal: null, status: "cleared" };
188
+ case "compact/start":
189
+ return { maintenanceId: "fake-maintenance", status: "completed" };
190
+ case "review/start": {
191
+ const turnId = randomUUID();
192
+ await this.emit({ type: "turn/started", payload: { turnId } });
193
+ await this.emit({ type: "turn/completed", payload: { turnId, status: "completed" } });
194
+ return startedReceipt(params.submissionId, turnId);
195
+ }
196
+ case "session/interrupt":
197
+ return { accepted: true, stillQueued: [] };
198
+ case "session/close":
199
+ return { closed: true, observationClosed: true, executionStopped: "unconfirmed" };
200
+ case "interaction/respond":
201
+ case "interaction/cancel":
202
+ throw new Error("Interactions must be handled by the SDK registry");
203
+ }
204
+ }
205
+ /** Count backend release calls from the SDK or direct fixture callers and retain state for assertions. */
206
+ async dispose() { this.disposeCount++; }
207
+ /**
208
+ * Produce the default submission receipt after publishing start, preview, final item and terminal.
209
+ * Reuse an item ID across turns to exercise SDK scoping. Store the completed item before terminal
210
+ * publication, where a sink failure can still reject the dispatch.
211
+ */
212
+ async submitInput(request) {
213
+ const turnId = randomUUID();
214
+ await this.emit({ type: "turn/started", payload: { turnId } });
215
+ const text = request.input.filter(part => part.type === "text").map(part => part.text).join("\n");
216
+ const item = { id: "native-reused", type: "agentMessage", text, phase: "final" };
217
+ await this.emit({ type: "item/started", payload: { item: { ...item, text: "" } } }, turnId);
218
+ await this.emit({ type: "item/delta", payload: { itemId: item.id, kind: "text", text: "preview " } }, turnId);
219
+ await this.emit({ type: "item/completed", payload: { item } }, turnId);
220
+ this.history.push({ turnId, item });
221
+ await this.emit({ type: "turn/completed", payload: { turnId, status: "completed" } });
222
+ return { submissionId: request.submissionId, turnId, accepted: true, disposition: "started" };
223
+ }
224
+ }
225
+ /** Associate an accepted submission with its started turn. Terminal publication is independent. */
226
+ function startedReceipt(submissionId, turnId) {
227
+ return { submissionId, accepted: true, disposition: "started", turnId };
228
+ }
229
+ /**
230
+ * Find the end of the retained prefix shared by fork and revert. Use the first matching history
231
+ * entry as the boundary. Throw TURN_NOT_FOUND before either caller changes history if it is absent.
232
+ */
233
+ function historyBoundaryIndex(history, boundary) {
234
+ const index = history.findIndex(entry => entry.turnId === boundary.turnId);
235
+ if (index < 0)
236
+ throw new HarnessSdkError("TURN_NOT_FOUND");
237
+ return boundary.relation === "before" ? index : index + 1;
238
+ }
239
+ /**
240
+ * Create the hook that ScriptedBackendSession.execute awaits. Only special submissions supply a
241
+ * receipt here. Held cases stop after start so the suite can drive control or observation loss.
242
+ * Interaction cases await SDK settlement before completion. Sink failures reject dispatch.
243
+ */
244
+ function scenarioExecutor(scenario) {
245
+ return async (call, sink) => {
246
+ if (call.method !== "input/submit")
247
+ return undefined;
248
+ const { submissionId } = call.params;
249
+ const turnId = randomUUID();
250
+ const hold = ["interrupt", "steer", "disconnect", "backpressure"].includes(scenario);
251
+ const interaction = hold ? undefined : scenarioInteraction(scenario, turnId);
252
+ if (!hold && !interaction && scenario !== "unknown-item")
253
+ return undefined;
254
+ await sink.emit({ type: "turn/started", payload: { turnId } });
255
+ if (hold)
256
+ return startedReceipt(submissionId, turnId);
257
+ if (interaction) {
258
+ // The SDK registry calls this responder when it settles the request. The fixture has no
259
+ // native peer to notify, but it still waits for the registry before completing the turn.
260
+ await sink.requestInteraction(interaction, async () => { });
261
+ }
262
+ else {
263
+ await sink.emit({
264
+ type: "item/completed",
265
+ payload: { item: { id: "unknown", type: "toolCall", kind: "other", title: "Unknown", nativeType: "futureNativeType", raw: { safe: "value" } } },
266
+ }, { turnId });
267
+ }
268
+ await sink.emit({ type: "turn/completed", payload: { turnId, status: "completed" } });
269
+ return startedReceipt(submissionId, turnId);
270
+ };
271
+ }
272
+ /** Supply the SDK's automatic conformance handler with the native request for an interaction case. */
273
+ function scenarioInteraction(scenario, turnId) {
274
+ switch (scenario) {
275
+ case "approval":
276
+ return { interactionId: "conformance-approval", kind: "approval", turnId, options: [{ id: "deny", label: "Deny", semantic: "deny" }] };
277
+ case "userInput":
278
+ return { interactionId: "conformance-input", kind: "userInput", turnId, questions: [{ id: "q", prompt: "Value" }] };
279
+ case "elicitation":
280
+ return { interactionId: "conformance-elicitation", kind: "elicitation", turnId, mode: "form" };
281
+ case "dynamicTool":
282
+ return { interactionId: "conformance-tool", kind: "dynamicTool", turnId, toolName: "conformance", arguments: {} };
283
+ }
284
+ }
@@ -0,0 +1,38 @@
1
+ import type { AhpEventPayload, AhpMethod, AhpRequestMap, AhpResultMap } from "@anureo/protocol";
2
+ /** Native peer protocol used to select a fixture. The JSONL runners accept Codex, ACP and AHP. */
3
+ export type WireFlavor = "codex" | "claude" | "acp" | "ahp";
4
+ /** One step in the JSONL peer script. The peer considers only the next step for each parsed message. */
5
+ export interface NativeFixtureFrame {
6
+ /** Match this method exactly. Omit to match any parsed message, including a response without a method. */
7
+ waitForMethod?: string;
8
+ /**
9
+ * Messages written in order after a match. The peer substitutes "$requestId" and
10
+ * "$params.<key>" string values recursively, using a single top-level parameter key.
11
+ * ACP and AHP default jsonrpc to "2.0". Codex leaves that field as authored.
12
+ */
13
+ emit: ReadonlyArray<Record<string, unknown>>;
14
+ /**
15
+ * Require an exact ID and JSON.stringify-equal result after the method filter matches.
16
+ * A mismatch suppresses emission, stops script processing, writes FIXTURE_RESPONSE_MISMATCH
17
+ * to stderr and sets exit code 2.
18
+ */
19
+ expectedResponse?: {
20
+ id: string | number;
21
+ result: unknown;
22
+ };
23
+ }
24
+ /** Describe a named native exchange and the AHP event payloads the consuming test should assert. */
25
+ export interface ConformanceFixture {
26
+ name: string;
27
+ flavor: WireFlavor;
28
+ /** Ordered peer steps. For JSONL peers, point HARNESS_FIXTURE_SCRIPT at a file containing this array. */
29
+ script: readonly NativeFixtureFrame[];
30
+ /** Expected observations for the consuming test to compare with the adapter's emitted payloads. */
31
+ expectedEvents: readonly AhpEventPayload[];
32
+ }
33
+ /** Pair an AHP request and expected result under their method key for a typed test fixture. */
34
+ export type MethodFixture<M extends AhpMethod> = {
35
+ method: M;
36
+ request: AhpRequestMap[M];
37
+ result: AhpResultMap[M];
38
+ };
package/src/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** Adapter acceptance runners and injectable fixtures for SDK, adapter and application tests. */
2
+ export * from "./fake.js";
3
+ export * from "./suite.js";
4
+ export * from "./fixtures.js";
5
+ export * from "./fake-claude.js";
6
+ export * from "./native.js";
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ /** Adapter acceptance runners and injectable fixtures for SDK, adapter and application tests. */
2
+ export * from "./fake.js";
3
+ export * from "./suite.js";
4
+ export * from "./fixtures.js";
5
+ export * from "./fake-claude.js";
6
+ export * from "./native.js";
@@ -0,0 +1,44 @@
1
+ import type { Client, Input, Session } from '@anureo/sdk';
2
+ /** Native execution association, active observation closure, and independent observer ownership cases. */
3
+ export type NativeScenario = 'overlap' | 'close-active' | 'observer-cancel';
4
+ /** Supply an already opened, isolated native session and transfer its client to the runner for disposal. */
5
+ export interface NativeFixture {
6
+ client: Client;
7
+ /** Open session owned by client. */
8
+ session: Session;
9
+ /** Input that starts an execution the fixture keeps active for the selected check. */
10
+ first: Input;
11
+ /** Follow-up input for overlap/observer-cancel; unused by close-active. */
12
+ second: Input;
13
+ /** Whether the two overlap submissions should resolve to the same execution ID. */
14
+ relation: 'same' | 'different';
15
+ /**
16
+ * The overlap and observer-cancel checks await this hook after initiating both submissions
17
+ * and before awaiting the second Turn. If the fixture holds execution, wait for evidence
18
+ * that the native backend received both inputs before allowing completion. Omit the hook
19
+ * when native behavior provides that ordering. The close-active check never calls it.
20
+ */
21
+ release?(): Promise<void>;
22
+ }
23
+ /** Supply a fresh client and open session for each case. Clean up partial setup on rejection and retain ownership of external resources. */
24
+ export type NativeFixtureFactory = (scenario: NativeScenario) => Promise<NativeFixture>;
25
+ /** Native case verdicts in the order requested by the caller. */
26
+ export interface NativeConformanceReport {
27
+ /** True when every selected case passed, including an empty selection. */
28
+ passed: boolean;
29
+ results: Array<{
30
+ scenario: NativeScenario;
31
+ passed: boolean;
32
+ /** Failure code or CONFORMANCE_FAILED. RESOURCE_RELEASE_FAILED is appended after a prior failure. */
33
+ error?: string;
34
+ }>;
35
+ }
36
+ /**
37
+ * Check native lifecycle behavior through the SDK, sequentially in the requested order.
38
+ * The default order is overlap, close-active, then observer-cancel. Submission association,
39
+ * result and close waits each use 3000 ms. Factory setup, the optional release hook and client
40
+ * disposal have no runner deadline. The runner awaits disposal of each returned client before
41
+ * proceeding and reports setup, check and disposal failures as case verdicts. Fixture providers
42
+ * still own external servers and controls.
43
+ */
44
+ export declare function runNativeConformance(factory: NativeFixtureFactory, scenarios?: readonly NativeScenario[]): Promise<NativeConformanceReport>;
package/src/native.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Check native lifecycle behavior through the SDK, sequentially in the requested order.
3
+ * The default order is overlap, close-active, then observer-cancel. Submission association,
4
+ * result and close waits each use 3000 ms. Factory setup, the optional release hook and client
5
+ * disposal have no runner deadline. The runner awaits disposal of each returned client before
6
+ * proceeding and reports setup, check and disposal failures as case verdicts. Fixture providers
7
+ * still own external servers and controls.
8
+ */
9
+ export async function runNativeConformance(factory, scenarios = ['overlap', 'close-active', 'observer-cancel']) {
10
+ const results = [];
11
+ for (const scenario of scenarios) {
12
+ results.push(await runNativeCase(factory, scenario));
13
+ }
14
+ return { passed: results.every(value => value.passed), results };
15
+ }
16
+ const nativeWaitMs = 3000;
17
+ /** Give runNativeConformance one verdict after running the selected check and disposing its fixture client. */
18
+ async function runNativeCase(factory, scenario) {
19
+ let fixture;
20
+ let failure;
21
+ try {
22
+ fixture = await factory(scenario);
23
+ const first = submitObserved(fixture.session, fixture.first);
24
+ if (scenario === 'overlap')
25
+ await checkOverlap(fixture, first);
26
+ else if (scenario === 'close-active')
27
+ await checkActiveClose(fixture, first);
28
+ else
29
+ await checkObserverCancellation(fixture, first);
30
+ }
31
+ catch (cause) {
32
+ failure = cause && typeof cause === 'object' && 'code' in cause ? String(cause.code) : 'CONFORMANCE_FAILED';
33
+ }
34
+ finally {
35
+ try {
36
+ await fixture?.client.dispose();
37
+ }
38
+ catch {
39
+ failure = failure ? `${failure}+RESOURCE_RELEASE_FAILED` : 'RESOURCE_RELEASE_FAILED';
40
+ }
41
+ }
42
+ return { scenario, passed: !failure, ...(failure ? { error: failure } : {}) };
43
+ }
44
+ /** Start association immediately. Observe early rejection while the case releases its fixture, and preserve it for later awaits. */
45
+ function submitObserved(session, input) {
46
+ const submission = session.submit({ input }, { timeoutMs: nativeWaitMs });
47
+ void submission.catch(() => { });
48
+ return submission;
49
+ }
50
+ /** Initiate both inputs before release, then verify the expected association and matching results for a shared execution. */
51
+ async function checkOverlap(fixture, first) {
52
+ const second = submitObserved(fixture.session, fixture.second);
53
+ await fixture.release?.();
54
+ const [firstTurn, secondTurn] = await Promise.all([first, second]);
55
+ if ((firstTurn.id === secondTurn.id) !== (fixture.relation === 'same'))
56
+ throw new Error('EXECUTION_ASSOCIATION');
57
+ const [firstResult, secondResult] = await Promise.all([
58
+ firstTurn.result({ timeoutMs: nativeWaitMs }),
59
+ secondTurn.result({ timeoutMs: nativeWaitMs }),
60
+ ]);
61
+ if (firstResult.status !== 'completed' || secondResult.status !== 'completed')
62
+ throw new Error('NATIVE_TERMINAL');
63
+ if (firstTurn.id === secondTurn.id && JSON.stringify(firstResult) !== JSON.stringify(secondResult))
64
+ throw new Error('SHARED_RESULT');
65
+ }
66
+ /** Closing an active session must end observation without synthesizing a native execution result. */
67
+ async function checkActiveClose(fixture, first) {
68
+ const turn = await first;
69
+ const result = await fixture.session.close({ timeoutMs: nativeWaitMs });
70
+ if (!result.observationClosed)
71
+ throw new Error('OBSERVATION_NOT_CLOSED');
72
+ await assertObservationClosed(turn);
73
+ }
74
+ /** Dispose the original observer before the second submission. Its sibling must still receive the native completion after release. */
75
+ async function checkObserverCancellation(fixture, first) {
76
+ const firstTurn = await first;
77
+ const siblingObserver = firstTurn.observe();
78
+ await firstTurn.dispose();
79
+ const second = submitObserved(fixture.session, fixture.second);
80
+ await fixture.release?.();
81
+ await second;
82
+ if ((await siblingObserver.result({ timeoutMs: nativeWaitMs })).status !== 'completed')
83
+ throw new Error('SIBLING_OBSERVER_CANCELLED');
84
+ }
85
+ /** Accept only observation closure; a native terminal or any other rejection fails the active-close case. */
86
+ async function assertObservationClosed(turn) {
87
+ try {
88
+ await turn.result({ timeoutMs: nativeWaitMs });
89
+ }
90
+ catch (cause) {
91
+ if (cause && typeof cause === 'object' && 'code' in cause && cause.code === 'OBSERVATION_CLOSED')
92
+ return;
93
+ throw cause;
94
+ }
95
+ throw new Error('FALSE_TERMINAL');
96
+ }
package/src/suite.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { type HarnessAdapter, type RuntimeOptions, type Session } from '@anureo/sdk';
2
+ import type { InteractionRequested, InteractionResponse, SessionOpenParams } from '@anureo/protocol';
3
+ /** Default shared acceptance cases in execution order. Use runNativeConformance for native lifecycle cases. */
4
+ export declare const CONFORMANCE_SCENARIOS: readonly ["lifecycle", "immediate", "identity", "approval", "userInput", "elicitation", "dynamicTool", "interrupt", "steer", "fork", "revert", "history", "unknown-item", "disconnect", "backpressure"];
5
+ /** Select a shared behavioral case to run or to supply fixture behavior for. */
6
+ export type ConformanceScenario = typeof CONFORMANCE_SCENARIOS[number];
7
+ /** Supply scenario-specific native behavior to the runner that manages the SDK client. */
8
+ export interface ConformanceDriver {
9
+ /** Fresh adapter passed to createClient for SDK-managed initialization and release. */
10
+ adapter: HarnessAdapter;
11
+ /** Native session setup passed to Client.openSession. Its binding also supplies the client identity. */
12
+ openParams: SessionOpenParams;
13
+ /** SDK policy overrides, including queue limits for backpressure fixtures. */
14
+ runtimeOptions?: RuntimeOptions;
15
+ /**
16
+ * runConformance awaits this hook after obtaining the initial Turn and completing any control request,
17
+ * before waiting for its result. Fixtures can use it to induce source loss. A rejection reaches the
18
+ * case as an execution failure and can satisfy a fault case if it carries one of that case's codes.
19
+ */
20
+ afterStart?: (session: Session) => Promise<void>;
21
+ /** Authoritative text expected by lifecycle and immediate. Defaults to 'conformance'. */
22
+ expectedText?: string;
23
+ /**
24
+ * Answer a native interaction with a response of the matching kind. The SDK calls the runner's
25
+ * registered handler after publishing the request and delivers the answer to the native responder.
26
+ * If the callback throws, the SDK cancels the interaction while it is still actionable.
27
+ * By default the runner selects the first approval option and the first option for each question,
28
+ * uses 'conformance' for free-text answers and tool output, and declines elicitation.
29
+ */
30
+ answer?: (request: InteractionRequested) => InteractionResponse;
31
+ }
32
+ /** Supply an isolated driver each time runConformance starts a selected case, even if it later skips the case. Release external resources and clean up failed factory setup yourself. */
33
+ export type ConformanceFactory = (scenario: ConformanceScenario) => Promise<ConformanceDriver> | ConformanceDriver;
34
+ /** Verdict for one selected case. */
35
+ export interface ConformanceCaseResult {
36
+ scenario: ConformanceScenario;
37
+ /** An undeclared required capability yields 'not-applicable'. A broken declared capability yields 'failed'. */
38
+ status: 'passed' | 'failed' | 'not-applicable';
39
+ /** Diagnostic for a failed case, replaced by RESOURCE_RELEASE_FAILED if disposal of its client rejects. */
40
+ message?: string;
41
+ }
42
+ /** Shared case verdicts in the order requested by the caller. */
43
+ export interface ConformanceReport {
44
+ /** True when no selected case failed, including an empty selection or cases that were skipped. */
45
+ passed: boolean;
46
+ cases: ConformanceCaseResult[];
47
+ }
48
+ /**
49
+ * Check an adapter through the public SDK, sequentially in the selected order (all shared cases by default).
50
+ * The timeoutMs option bounds initial submission, controls, afterStart and result observation.
51
+ * It defaults to 3000 ms and leaves setup, later session operations and cleanup on their own
52
+ * SDK or fixture wait policies. The runner awaits reader teardown before client disposal and
53
+ * starts the next case after cleanup. Setup and check failures become case verdicts. Reader
54
+ * teardown failure rejects the run before client disposal. Client disposal failure marks the
55
+ * case failed with RESOURCE_RELEASE_FAILED. Callers still own resources outside the SDK client.
56
+ */
57
+ export declare function runConformance(factory: ConformanceFactory, options?: {
58
+ timeoutMs?: number;
59
+ scenarios?: readonly ConformanceScenario[];
60
+ }): Promise<ConformanceReport>;