@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/suite.js ADDED
@@ -0,0 +1,289 @@
1
+ import { createClient } from '@anureo/sdk';
2
+ /** Default shared acceptance cases in execution order. Use runNativeConformance for native lifecycle cases. */
3
+ export const CONFORMANCE_SCENARIOS = [
4
+ 'lifecycle', 'immediate', 'identity', 'approval', 'userInput', 'elicitation', 'dynamicTool',
5
+ 'interrupt', 'steer', 'fork', 'revert', 'history', 'unknown-item', 'disconnect', 'backpressure',
6
+ ];
7
+ /**
8
+ * Check an adapter through the public SDK, sequentially in the selected order (all shared cases by default).
9
+ * The timeoutMs option bounds initial submission, controls, afterStart and result observation.
10
+ * It defaults to 3000 ms and leaves setup, later session operations and cleanup on their own
11
+ * SDK or fixture wait policies. The runner awaits reader teardown before client disposal and
12
+ * starts the next case after cleanup. Setup and check failures become case verdicts. Reader
13
+ * teardown failure rejects the run before client disposal. Client disposal failure marks the
14
+ * case failed with RESOURCE_RELEASE_FAILED. Callers still own resources outside the SDK client.
15
+ */
16
+ export async function runConformance(factory, options = {}) {
17
+ const cases = [];
18
+ for (const scenario of options.scenarios ?? CONFORMANCE_SCENARIOS) {
19
+ cases.push(await runConformanceCase({ factory, scenario, timeoutMs: options.timeoutMs ?? 3000 }));
20
+ }
21
+ return { passed: cases.every(entry => entry.status !== 'failed'), cases };
22
+ }
23
+ /**
24
+ * Supply runConformance with one verdict after fixture setup, applicable checks and ordered cleanup.
25
+ * Returning the reader first detaches it from the session and lets its collection task finish.
26
+ * Reader teardown failure propagates directly. Client disposal failure replaces any earlier verdict.
27
+ */
28
+ async function runConformanceCase({ factory, scenario, timeoutMs }) {
29
+ let client;
30
+ let observation;
31
+ let result;
32
+ try {
33
+ const driver = await factory(scenario);
34
+ client = await createCaseClient(driver);
35
+ const session = await client.openSession(driver.openParams);
36
+ if (!isApplicable(scenario, session.capabilities)) {
37
+ result = { scenario, status: 'not-applicable' };
38
+ }
39
+ else {
40
+ observation = collectEvents(session);
41
+ await checkScenario({ client, session, driver, scenario, observation, timeoutMs });
42
+ result = { scenario, status: 'passed' };
43
+ }
44
+ }
45
+ catch (cause) {
46
+ result = { scenario, status: 'failed', message: cause instanceof Error ? cause.message : 'Conformance failed' };
47
+ }
48
+ finally {
49
+ await observation?.reader.return?.();
50
+ await observation?.task;
51
+ try {
52
+ await client?.dispose();
53
+ }
54
+ catch {
55
+ result = { scenario, status: 'failed', message: 'RESOURCE_RELEASE_FAILED' };
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+ const interactionKinds = ['approval', 'userInput', 'elicitation', 'dynamicTool'];
61
+ /**
62
+ * Initialize the driver adapter for runConformanceCase, advertising every interaction kind.
63
+ * The SDK invokes the registered handler after publishing the request and uses its answer for
64
+ * native settlement. Return createClient's initialization promise so runConformanceCase can
65
+ * open a session and later dispose the client, or record an initialization failure.
66
+ */
67
+ function createCaseClient(driver) {
68
+ const handlers = Object.fromEntries(interactionKinds.map(kind => [
69
+ kind,
70
+ async (request) => (driver.answer ?? defaultAnswer)(request),
71
+ ]));
72
+ return createClient({
73
+ adapter: driver.adapter,
74
+ identity: driver.openParams.binding,
75
+ runtime: driver.runtimeOptions,
76
+ interactions: handlers,
77
+ });
78
+ }
79
+ /**
80
+ * Subscribe before submission so the case can inspect events published before association.
81
+ * The task records read failures and enforces a 10,000-event limit. checkScenario uses its
82
+ * completion and failure during verification. runConformanceCase owns reader teardown.
83
+ */
84
+ function collectEvents(session) {
85
+ const reader = session.events()[Symbol.asyncIterator]();
86
+ const events = [];
87
+ let failure;
88
+ const task = (async () => {
89
+ try {
90
+ for (;;) {
91
+ const event = await reader.next();
92
+ if (event.done)
93
+ return;
94
+ events.push(event.value);
95
+ if (events.length > 10_000)
96
+ throw new Error('Too many events without completion');
97
+ }
98
+ }
99
+ catch (cause) {
100
+ failure = cause;
101
+ }
102
+ })();
103
+ return { reader, events, task, get failure() { return failure; } };
104
+ }
105
+ /**
106
+ * Verify the initial execution before any follow-up operations add events. runConformanceCase
107
+ * awaits this before recording a pass. Only source fault cases expect observation to fail.
108
+ */
109
+ async function checkScenario({ client, session, driver, scenario, observation, timeoutMs }) {
110
+ const { events } = observation;
111
+ const capabilities = session.capabilities;
112
+ const execution = executeScenarioTurn({ session, driver, scenario, capabilities });
113
+ const expectsSourceFailure = scenario === 'disconnect' || scenario === 'backpressure';
114
+ if (expectsSourceFailure) {
115
+ await checkSourceFailure({ execution, scenario, events, timeoutMs });
116
+ }
117
+ else {
118
+ const { turn, result } = await deadline(Promise.race([
119
+ execution,
120
+ observation.task.then(() => { throw observation.failure ?? new Error('Source ended before result'); }),
121
+ ]), timeoutMs);
122
+ checkTurnResult({ scenario, driver, events, result });
123
+ if (scenario === 'identity' || scenario === 'history' || scenario === 'fork' || scenario === 'revert') {
124
+ await checkSessionOperation({ client, session, turn, scenario, capabilities });
125
+ }
126
+ }
127
+ requireCondition(!observation.failure || expectsSourceFailure, 'Unexpected source failure');
128
+ checkEventSequence(session, events);
129
+ }
130
+ /** Dispatch the selected follow-up after initial terminal evidence passes. Its rejection fails the case. */
131
+ async function checkSessionOperation({ client, session, turn, scenario, capabilities }) {
132
+ switch (scenario) {
133
+ case 'identity':
134
+ await checkDistinctExecution(session, turn);
135
+ break;
136
+ case 'history':
137
+ await checkHistoryIdentity(session);
138
+ break;
139
+ case 'fork': {
140
+ const relations = capabilities.session?.fork;
141
+ requireCondition(relations !== undefined, 'Fork capability missing');
142
+ await checkForkBoundaries({ client, session, turn, relations, history: capabilities.session?.history });
143
+ break;
144
+ }
145
+ case 'revert': {
146
+ const scope = capabilities.session?.revert?.[0];
147
+ requireCondition(scope !== undefined, 'Revert capability missing');
148
+ await checkRevertBoundary({ session, turn, scope, history: capabilities.session?.history });
149
+ break;
150
+ }
151
+ }
152
+ }
153
+ /**
154
+ * Start the initial input for checkScenario and return its associated Turn and retained result.
155
+ * Session-level interruption is issued before awaiting association, which turn controls require.
156
+ * Await afterStart after controls so its effects or failure precede the result wait.
157
+ */
158
+ async function executeScenarioTurn({ session, driver, scenario, capabilities }) {
159
+ const pendingTurn = session.submit({ input: 'conformance' });
160
+ // Interruption/release may reject submission before we await the original promise.
161
+ void pendingTurn.catch(() => { });
162
+ const supportsTurnInterrupt = scenario === 'interrupt' && capabilities.input?.interrupt?.targets.includes('turn') === true;
163
+ if (scenario === 'interrupt' && !supportsTurnInterrupt) {
164
+ await Promise.resolve();
165
+ await session.interrupt();
166
+ }
167
+ const turn = await pendingTurn;
168
+ if (scenario === 'steer')
169
+ await turn.steer({ input: 'redirect' });
170
+ if (scenario === 'interrupt' && supportsTurnInterrupt)
171
+ await turn.interrupt();
172
+ await driver.afterStart?.(session);
173
+ return { turn, result: await turn.result() };
174
+ }
175
+ /** Fail the case unless execution rejects with an allowed source-loss code and no terminal event was published. */
176
+ async function checkSourceFailure({ execution, scenario, events, timeoutMs }) {
177
+ try {
178
+ await deadline(execution, timeoutMs);
179
+ }
180
+ catch (cause) {
181
+ requireCondition(cause && typeof cause === 'object' && 'code' in cause, 'Source failure must be typed');
182
+ const expectedCodes = scenario === 'disconnect'
183
+ ? ['CONNECTION_CLOSED', 'ACP_CONNECTION_CLOSED', 'RPC_CONNECTION_CLOSED']
184
+ : ['BACKPRESSURE_EXCEEDED', 'FRAME_TOO_LARGE'];
185
+ requireCondition(expectedCodes.includes(String(cause.code)), `Unexpected observation error: ${String(cause.code)}`);
186
+ requireCondition(!events.some(event => event.type === 'turn/completed'), 'SDK must not fabricate native terminal');
187
+ return;
188
+ }
189
+ throw new Error('Lost source must fail observation');
190
+ }
191
+ /** Check native terminal evidence before identity/history operations add further executions and events. */
192
+ function checkTurnResult({ scenario, driver, events, result }) {
193
+ requireCondition(result.status === (scenario === 'interrupt' ? 'interrupted' : 'completed'), 'Unexpected native terminal');
194
+ requireCondition(events.filter(event => event.type === 'turn/started').length === 1, 'Execution must start once');
195
+ requireCondition(events.filter(event => event.type === 'turn/completed').length === 1, 'Execution must end once');
196
+ if (isInteractionScenario(scenario)) {
197
+ requireCondition(events.some(event => event.type === 'interaction/requested') && events.some(event => event.type === 'interaction/resolved'), 'Interaction not exercised');
198
+ }
199
+ if (scenario === 'lifecycle' || scenario === 'immediate') {
200
+ requireCondition(result.text === (driver.expectedText ?? 'conformance'), 'Authoritative final text missing');
201
+ }
202
+ if (scenario === 'unknown-item') {
203
+ requireCondition(result.items.some(item => item.type === 'toolCall' && item.kind === 'other' && item.nativeType), 'Unknown native item lost');
204
+ }
205
+ }
206
+ /** After the first terminal result, wait for a second execution and require a fresh identity. */
207
+ async function checkDistinctExecution(session, turn) {
208
+ const other = await session.submit({ input: 'conformance' });
209
+ await other.result();
210
+ requireCondition(other.id !== turn.id, 'Execution identity reused');
211
+ }
212
+ /** Verify the first history page retains both execution and item IDs after completion. */
213
+ async function checkHistoryIdentity(session) {
214
+ const page = await session.request('history/read', { sessionId: session.id, requestId: 'history' });
215
+ requireCondition(page.items.length && page.items.every(item => item.turnId && item.item.id), 'History identity missing');
216
+ }
217
+ /**
218
+ * Verify each advertised fork boundary in order. Check history when supported and close each
219
+ * verified fork before opening the next. The case client still owns a fork if a check rejects.
220
+ */
221
+ async function checkForkBoundaries({ client, session, turn, relations, history }) {
222
+ for (const relation of relations) {
223
+ const fork = await client.sessions.fork(session.reference, { boundary: { turnId: turn.id, relation } });
224
+ requireCondition(fork.id !== session.id && fork.binding.adapterId === session.binding.adapterId, 'Fork binding invalid');
225
+ if (history) {
226
+ const page = await fork.request('history/read', { sessionId: fork.id, requestId: 'fork-history' });
227
+ const includesBoundary = page.items.some(item => item.turnId === turn.id);
228
+ requireCondition(includesBoundary === (relation !== 'before'), 'Fork boundary lost');
229
+ }
230
+ await fork.close();
231
+ }
232
+ }
233
+ /** Verify the receipt for the first advertised scope, and check supported history when the scope includes conversation. */
234
+ async function checkRevertBoundary({ session, turn, scope, history }) {
235
+ const sessionId = session.id;
236
+ const result = await session.request('session/revert', { sessionId, requestId: 'revert', scope, boundary: { turnId: turn.id, relation: 'before' } });
237
+ requireCondition(result.appliedScope === scope && result.boundary.turnId === turn.id, 'Revert scope mismatch');
238
+ if (scope !== 'files' && history) {
239
+ const page = await session.request('history/read', { sessionId, requestId: 'reverted-history' });
240
+ requireCondition(!page.items.some(item => item.turnId === turn.id), 'Revert boundary retained');
241
+ }
242
+ }
243
+ /** Check the contiguous session stream from the first observed sequence, since subscriptions have no replay. */
244
+ function checkEventSequence(session, events) {
245
+ const firstEvent = events[0];
246
+ if (firstEvent === undefined)
247
+ return;
248
+ const contiguous = events.every((event, index) => event.seq === firstEvent.seq + index && event.sessionId === session.id && event.streamId === firstEvent.streamId);
249
+ requireCondition(contiguous, 'Source sequence gap');
250
+ }
251
+ /** Throw assertion messages that runConformanceCase records as failed-case diagnostics. */
252
+ function requireCondition(value, message) { if (!value)
253
+ throw new Error(message); }
254
+ /** Reject the wait at its deadline. The underlying promise stays observed and the case owner handles cleanup. */
255
+ function deadline(promise, timeoutMs) {
256
+ return new Promise((resolve, reject) => {
257
+ const timer = setTimeout(() => reject(new Error('Conformance timed out')), timeoutMs);
258
+ promise.then(value => { clearTimeout(timer); resolve(value); }, cause => { clearTimeout(timer); reject(cause); });
259
+ });
260
+ }
261
+ /** Let runConformanceCase skip scenarios whose required session capability was omitted. */
262
+ function isApplicable(scenario, capabilities) {
263
+ if (isInteractionScenario(scenario))
264
+ return capabilities.interactions?.includes(scenario) ?? false;
265
+ if (scenario === 'steer')
266
+ return capabilities.input?.steer?.atomicMatch === true;
267
+ if (scenario === 'interrupt')
268
+ return !!capabilities.input?.interrupt?.targets.length;
269
+ if (scenario === 'fork')
270
+ return !!capabilities.session?.fork?.length;
271
+ if (scenario === 'revert')
272
+ return !!capabilities.session?.revert?.length;
273
+ if (scenario === 'history')
274
+ return !!capabilities.session?.history;
275
+ return true;
276
+ }
277
+ /** Identify cases whose applicability and verdict depend on a native interaction. */
278
+ function isInteractionScenario(scenario) {
279
+ return interactionKinds.some(kind => kind === scenario);
280
+ }
281
+ /** Answer the SDK handler's request when the driver supplies no override. Approval fixtures must provide an option. */
282
+ function defaultAnswer(request) {
283
+ switch (request.kind) {
284
+ case 'approval': return { kind: request.kind, optionId: request.options[0].id };
285
+ case 'userInput': return { kind: request.kind, answers: Object.fromEntries(request.questions.map(question => [question.id, question.options?.[0]?.id ?? 'conformance'])) };
286
+ case 'elicitation': return { kind: request.kind, action: 'decline' };
287
+ case 'dynamicTool': return { kind: request.kind, content: [{ type: 'text', text: 'conformance' }] };
288
+ }
289
+ }
@@ -1,40 +0,0 @@
1
- import type { JsonObject, JsonValue } from "@anureo/protocol";
2
- export interface ClaudeQueryScript {
3
- messages: JsonObject[];
4
- onInput?: (input: JsonValue, emit: (message: JsonObject) => Promise<void>) => Promise<void>;
5
- models?: Array<{
6
- value: string;
7
- displayName: string;
8
- description: string;
9
- }>;
10
- canRewind?: boolean;
11
- }
12
- /** Minimal injectable query facade. The adapter supplies its SDK-facing structural types. */
13
- export declare class ScriptedClaudeQuery implements AsyncIterable<JsonObject> {
14
- private readonly script;
15
- readonly calls: Array<{
16
- method: string;
17
- value?: JsonValue;
18
- }>;
19
- private readonly queue;
20
- private closed;
21
- constructor(script: ClaudeQueryScript);
22
- [Symbol.asyncIterator](): AsyncIterator<JsonObject>;
23
- streamInput(stream: AsyncIterable<JsonValue>): Promise<void>;
24
- interrupt(): Promise<void>;
25
- setModel(model?: string): Promise<void>;
26
- setPermissionMode(mode: string): Promise<void>;
27
- setMaxThinkingTokens(tokens: number | null): Promise<void>;
28
- rewindFiles(id: string): Promise<{
29
- canRewind: boolean;
30
- error?: string;
31
- }>;
32
- supportedModels(): Promise<{
33
- value: string;
34
- displayName: string;
35
- description: string;
36
- }[]>;
37
- finish(): void;
38
- close(): void;
39
- }
40
- export declare function createFakeClaudeQuery(script: ClaudeQueryScript): ScriptedClaudeQuery;
package/dist/fake.d.ts DELETED
@@ -1,44 +0,0 @@
1
- import { type BackendSession, type HarnessAdapter, type SessionEventSink, type SessionMethod } from "@anureo/sdk";
2
- import { type Capabilities, type AhpRequestMap, type AhpResultMap, type HistoryItem, type InitializeParams, type InitializeResult, type SessionBinding, type SessionOpenParams, type AhpEventPayload } from "@anureo/protocol";
3
- import type { ConformanceFactory } from "./suite.js";
4
- export interface FakeCall {
5
- method: SessionMethod;
6
- params: AhpRequestMap[SessionMethod];
7
- }
8
- export interface FakeAdapterOptions {
9
- capabilities?: Capabilities;
10
- /** Hook can publish events, pause a response, or throw exactly at a chosen dispatch point. */
11
- execute?: (call: FakeCall, sink: SessionEventSink) => Promise<AhpResultMap[SessionMethod] | undefined>;
12
- onOpen?: (params: SessionOpenParams, sink: SessionEventSink) => Promise<void>;
13
- }
14
- export declare const FAKE_CAPABILITIES: Capabilities;
15
- /** In-memory official-API-shaped fake. It is test-only and never registered in the production Host. */
16
- export declare class ScriptedHarnessAdapter implements HarnessAdapter {
17
- readonly options: FakeAdapterOptions;
18
- readonly calls: FakeCall[];
19
- readonly opened: SessionOpenParams[];
20
- readonly sinks: Map<string, SessionEventSink>;
21
- readonly backends: Map<string, ScriptedBackendSession>;
22
- disposeCount: number;
23
- constructor(options?: FakeAdapterOptions);
24
- get capabilities(): Capabilities;
25
- initialize(params: InitializeParams): Promise<InitializeResult>;
26
- openSession(params: SessionOpenParams, sink: SessionEventSink): Promise<BackendSession>;
27
- dispose(): Promise<void>;
28
- }
29
- export declare class ScriptedBackendSession implements BackendSession {
30
- private readonly adapter;
31
- readonly sink: SessionEventSink;
32
- readonly binding: SessionBinding;
33
- readonly capabilities: Capabilities;
34
- readonly history: HistoryItem[];
35
- disposeCount: number;
36
- private state;
37
- constructor(adapter: ScriptedHarnessAdapter, params: SessionOpenParams, sink: SessionEventSink);
38
- emit(payload: AhpEventPayload, turnId?: string): Promise<void>;
39
- execute<M extends SessionMethod>(method: M, params: AhpRequestMap[M]): Promise<AhpResultMap[M]>;
40
- dispose(): Promise<void>;
41
- }
42
- export declare function createScriptedAdapter(options?: FakeAdapterOptions): ScriptedHarnessAdapter;
43
- /** Reusable deterministic fixtures for the public runner, including deliberate source faults. */
44
- export declare function createScriptedConformanceFactory(capabilities?: Capabilities): ConformanceFactory;
package/dist/fake.js DELETED
@@ -1,199 +0,0 @@
1
- // packages/conformance/src/fake.ts
2
- import {
3
- negotiateVersion
4
- } from "@anureo/protocol";
5
- import { HarnessSdkError } from "@anureo/sdk";
6
- var FAKE_CAPABILITIES = {
7
- session: { resume: true, fork: ["before", "through"], revert: ["conversation", "files", "both"], history: true },
8
- turn: { steer: true, interrupt: true, outputSchema: true, inputModalities: ["text", "image", "file"], overrides: ["model"] },
9
- goal: { actions: ["set", "clear"] },
10
- compact: true,
11
- review: true,
12
- interactions: ["approval", "userInput", "elicitation", "dynamicTool"],
13
- config: { options: [{ id: "model", scope: "session", schema: { type: "string" } }] }
14
- };
15
-
16
- class ScriptedHarnessAdapter {
17
- options;
18
- calls = [];
19
- opened = [];
20
- sinks = new Map;
21
- backends = new Map;
22
- disposeCount = 0;
23
- constructor(options = {}) {
24
- this.options = options;
25
- }
26
- get capabilities() {
27
- return structuredClone(this.options.capabilities ?? FAKE_CAPABILITIES);
28
- }
29
- async initialize(params) {
30
- negotiateVersion(params.protocolVersions);
31
- return { protocolVersion: 1, runtime: { name: "scripted", version: "1.0.0", adapter: "conformance/0.1.0" }, capabilities: this.capabilities };
32
- }
33
- async openSession(params, sink) {
34
- this.opened.push(structuredClone(params));
35
- this.sinks.set(params.binding.sessionId, sink);
36
- const backend = new ScriptedBackendSession(this, params, sink);
37
- this.backends.set(params.binding.sessionId, backend);
38
- if (params.mode === "fork" && params.fork) {
39
- const source = this.backends.get(params.fork.sourceSessionId);
40
- if (!source)
41
- throw new HarnessSdkError("SESSION_NOT_FOUND");
42
- const boundaryIndex = source.history.findIndex((entry) => entry.turnId === params.fork.boundary.turnId);
43
- if (boundaryIndex < 0)
44
- throw new HarnessSdkError("TURN_NOT_FOUND");
45
- const retained = params.fork.boundary.relation === "before" ? boundaryIndex : boundaryIndex + 1;
46
- backend.history.push(...structuredClone(source.history.slice(0, retained)));
47
- }
48
- await this.options.onOpen?.(params, sink);
49
- return backend;
50
- }
51
- async dispose() {
52
- this.disposeCount++;
53
- }
54
- }
55
-
56
- class ScriptedBackendSession {
57
- adapter;
58
- sink;
59
- binding;
60
- capabilities;
61
- history = [];
62
- disposeCount = 0;
63
- state;
64
- constructor(adapter, params, sink) {
65
- this.adapter = adapter;
66
- this.sink = sink;
67
- this.binding = { ...params.binding, nativeRef: { provider: "scripted", value: `native:${params.binding.sessionId}` } };
68
- this.capabilities = adapter.capabilities;
69
- this.state = { binding: this.binding, capabilities: this.capabilities, status: "ready", config: params.config ?? {}, capabilityRevision: 1 };
70
- }
71
- async emit(payload, turnId) {
72
- await this.sink.emit(payload, { turnId });
73
- }
74
- async execute(method, params) {
75
- const call = { method, params };
76
- this.adapter.calls.push(call);
77
- const custom = await this.adapter.options.execute?.(call, this.sink);
78
- if (custom !== undefined)
79
- return custom;
80
- let result;
81
- switch (method) {
82
- case "turn/start": {
83
- const start = params;
84
- await this.emit({ type: "turn/started", payload: { turnId: start.turnId } });
85
- const item = { id: "native-reused", type: "agentMessage", text: start.input.filter((part) => part.type === "text").map((part) => part.text).join(`
86
- `), phase: "final" };
87
- await this.emit({ type: "item/started", payload: { item: { ...item, text: "" } } }, start.turnId);
88
- await this.emit({ type: "item/delta", payload: { itemId: item.id, kind: "text", text: "preview " } }, start.turnId);
89
- await this.emit({ type: "item/completed", payload: { item } }, start.turnId);
90
- this.history.push({ turnId: start.turnId, item });
91
- await this.emit({ type: "turn/completed", payload: { turnId: start.turnId, status: "completed" } });
92
- result = { turnId: start.turnId, accepted: true };
93
- break;
94
- }
95
- case "turn/steer":
96
- result = { turnId: params.expectedTurnId, accepted: true };
97
- break;
98
- case "turn/interrupt": {
99
- const id = params.turnId;
100
- await this.emit({ type: "turn/completed", payload: { turnId: id, status: "interrupted" } });
101
- result = { accepted: true, turnId: id };
102
- break;
103
- }
104
- case "session/config/set":
105
- this.state = { ...this.state, config: { ...this.state.config, ...params.patch }, capabilityRevision: this.state.capabilityRevision + 1 };
106
- result = this.state;
107
- break;
108
- case "session/revert": {
109
- const request = params;
110
- if (request.scope !== "files") {
111
- const index = this.history.findIndex((entry) => entry.turnId === request.boundary.turnId);
112
- if (index < 0)
113
- throw new HarnessSdkError("TURN_NOT_FOUND");
114
- this.history.splice(request.boundary.relation === "before" ? index : index + 1);
115
- }
116
- result = { appliedScope: request.scope, boundary: request.boundary };
117
- break;
118
- }
119
- case "history/read":
120
- result = { items: this.history, snapshotToken: "fake-snapshot", source: this.binding.nativeRef };
121
- break;
122
- case "goal/set":
123
- result = { goal: params.goal, status: "active" };
124
- break;
125
- case "goal/clear":
126
- result = { goal: null, status: "cleared" };
127
- break;
128
- case "compact/start":
129
- result = { maintenanceId: "fake-maintenance", status: "completed" };
130
- break;
131
- case "review/start": {
132
- const id = params.turnId;
133
- await this.emit({ type: "turn/started", payload: { turnId: id } });
134
- await this.emit({ type: "turn/completed", payload: { turnId: id, status: "completed" } });
135
- result = { accepted: true, turnId: id };
136
- break;
137
- }
138
- case "session/close":
139
- result = { closed: true };
140
- break;
141
- case "interaction/respond":
142
- case "interaction/cancel":
143
- throw new Error("Interactions must be handled by the SDK registry");
144
- }
145
- return result;
146
- }
147
- async dispose() {
148
- this.disposeCount++;
149
- }
150
- }
151
- function createScriptedAdapter(options) {
152
- return new ScriptedHarnessAdapter(options);
153
- }
154
- function createScriptedConformanceFactory(capabilities) {
155
- return (scenario) => {
156
- const adapter = createScriptedAdapter({ capabilities, execute: async (call, sink) => {
157
- if (call.method !== "turn/start")
158
- return;
159
- const { turnId } = call.params;
160
- if (["interrupt", "steer", "disconnect", "backpressure"].includes(scenario)) {
161
- await sink.emit({ type: "turn/started", payload: { turnId } });
162
- if (scenario === "backpressure")
163
- sink.tryEmit({ type: "notice", payload: { severity: "info", code: "FLOOD", message: "Conformance overflow" } });
164
- return { accepted: true, turnId };
165
- }
166
- const interaction = {
167
- approval: { interactionId: "conformance-approval", kind: "approval", turnId, options: [{ id: "deny", label: "Deny", semantic: "deny" }] },
168
- userInput: { interactionId: "conformance-input", kind: "userInput", turnId, questions: [{ id: "q", prompt: "Value" }] },
169
- elicitation: { interactionId: "conformance-elicitation", kind: "elicitation", turnId, mode: "form" },
170
- dynamicTool: { interactionId: "conformance-tool", kind: "dynamicTool", turnId, toolName: "conformance", arguments: {} }
171
- };
172
- if (interaction[scenario] || scenario === "unknown-item") {
173
- await sink.emit({ type: "turn/started", payload: { turnId } });
174
- if (interaction[scenario])
175
- await sink.requestInteraction(interaction[scenario], async () => {});
176
- else
177
- await sink.emit({ type: "item/completed", payload: { item: { id: "unknown", type: "toolCall", kind: "other", title: "Unknown", nativeType: "futureNativeType", raw: { safe: "value" } } } }, { turnId });
178
- await sink.emit({ type: "turn/completed", payload: { turnId, status: "completed" } });
179
- return { accepted: true, turnId };
180
- }
181
- return;
182
- } });
183
- return {
184
- adapter,
185
- openParams: { requestId: "conformance-open", mode: "new", binding: { sessionId: "conformance-session", adapterId: "scripted", runtimeConfigId: "conformance-runtime" }, cwd: "/tmp" },
186
- ...scenario === "backpressure" ? { runtimeOptions: { queue: { maxEvents: 2 } } } : {},
187
- ...scenario === "disconnect" ? { afterStart: async () => {
188
- await adapter.sinks.get("conformance-session").fail(new HarnessSdkError("CONNECTION_CLOSED", "Conformance disconnection", false, true));
189
- } } : {}
190
- };
191
- };
192
- }
193
- export {
194
- FAKE_CAPABILITIES,
195
- ScriptedBackendSession,
196
- ScriptedHarnessAdapter,
197
- createScriptedAdapter,
198
- createScriptedConformanceFactory
199
- };
@@ -1,21 +0,0 @@
1
- import type { AhpEventPayload, AhpMethod, AhpRequestMap, AhpResultMap } from "@anureo/protocol";
2
- export type WireFlavor = "codex" | "claude" | "acp" | "ahp";
3
- export interface NativeFixtureFrame {
4
- waitForMethod?: string;
5
- emit: ReadonlyArray<Record<string, unknown>>;
6
- expectedResponse?: {
7
- id: string | number;
8
- result: unknown;
9
- };
10
- }
11
- export interface ConformanceFixture {
12
- name: string;
13
- flavor: WireFlavor;
14
- script: readonly NativeFixtureFrame[];
15
- expectedEvents: readonly AhpEventPayload[];
16
- }
17
- export type MethodFixture<M extends AhpMethod> = {
18
- method: M;
19
- request: AhpRequestMap[M];
20
- result: AhpResultMap[M];
21
- };
package/dist/index.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from "./fake.js";
2
- export * from "./suite.js";
3
- export * from "./fixtures.js";
4
- export * from "./fake-claude.js";