@agent-surface/testing 0.1.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 Wiseair S.r.l.
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,46 @@
1
+ # @agent-surface/testing
2
+
3
+ Deterministic testing toolkit for [agent-surface](https://github.com/Wiseair-srl/agent-surface). The surface is a typed contract; contracts are tested deterministically — **no test in this ecosystem requires an LLM**.
4
+
5
+ Docs: https://agent-surface-docs.vercel.app
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add -D @agent-surface/testing
11
+ ```
12
+
13
+ ## Use
14
+
15
+ ```tsx
16
+ import { renderAgentSurface } from "@agent-surface/testing/react";
17
+ import { matchers } from "@agent-surface/testing/matchers";
18
+ expect.extend(matchers);
19
+
20
+ it("exposes the documented surface", async () => {
21
+ const s = await renderAgentSurface(<DevicesPage />);
22
+ expect(s).toExpose("view:devices.table.selectRows");
23
+ expect(s).toExposeUnavailable("domain:devices.disable", {
24
+ reason: "Select at least one device first",
25
+ });
26
+ expect(s).toMatchSurfaceSnapshot(); // reviewable "what agents can see" artifact
27
+ });
28
+
29
+ it("runs the full disable flow", async () => {
30
+ const s = await renderAgentSurface(<DevicesPage />);
31
+ await s.invoke("view:devices.table.selectRows", { ids: ["d1"] });
32
+ let r = await s.invoke("domain:devices.disable", {});
33
+ expect(r).toFailWith("CONFIRMATION_REQUIRED");
34
+ s.confirmations.approve();
35
+ r = await s.invoke("domain:devices.disable", {}, {
36
+ confirmationId: r.error.details!.confirmationId as string,
37
+ });
38
+ expect(r).toBeOk();
39
+ });
40
+ ```
41
+
42
+ `createTestSurface` (framework-free) drives a bare registry the same way. Semantic snapshots normalize volatility (`registrationId` → `<reg#N>`), so they survive Strict Mode and remounts. Matchers distinguish *hidden* from *visible-disabled* — that distinction is the security model.
43
+
44
+ Full specification: [docs/08](https://github.com/Wiseair-srl/agent-surface/blob/main/docs/08-testing.md).
45
+
46
+ MIT © Wiseair S.r.l.
@@ -0,0 +1,173 @@
1
+ // src/matchers.ts
2
+ import { jsonDeepEqual } from "@agent-surface/core";
3
+
4
+ // src/serialize.ts
5
+ function serializeSurfaceSnapshot(snapshot, options) {
6
+ const regIdMap = /* @__PURE__ */ new Map();
7
+ const normalizeRegId = (id) => {
8
+ let placeholder = regIdMap.get(id);
9
+ if (!placeholder) {
10
+ placeholder = `<reg#${regIdMap.size + 1}>`;
11
+ regIdMap.set(id, placeholder);
12
+ }
13
+ return placeholder;
14
+ };
15
+ return {
16
+ ...options?.includeVersion ? { surfaceVersion: snapshot.surfaceVersion } : {},
17
+ ...snapshot.route ? { route: snapshot.route } : {},
18
+ components: snapshot.components.map((component) => ({
19
+ ...component,
20
+ registrationId: normalizeRegId(component.registrationId)
21
+ })),
22
+ procedures: snapshot.procedures.map((procedure) => ({
23
+ ...procedure,
24
+ registrationId: normalizeRegId(procedure.registrationId)
25
+ })),
26
+ ...snapshot.truncated ? { truncated: snapshot.truncated } : {}
27
+ };
28
+ }
29
+
30
+ // src/matchers.ts
31
+ function isTestSurface(value) {
32
+ return typeof value === "object" && value !== null && typeof value.snapshot === "function" && typeof value.invoke === "function";
33
+ }
34
+ function collectCapabilities(snapshot) {
35
+ const entries = [];
36
+ for (const component of snapshot.components) {
37
+ for (const cap of [...component.observations, ...component.actions]) {
38
+ entries.push({
39
+ capabilityId: cap.capabilityId,
40
+ available: cap.available,
41
+ unavailableReason: cap.unavailableReason,
42
+ instanceId: component.instanceId
43
+ });
44
+ }
45
+ }
46
+ for (const proc of snapshot.procedures) {
47
+ entries.push({
48
+ capabilityId: proc.procedureId,
49
+ available: proc.available,
50
+ unavailableReason: proc.unavailableReason,
51
+ ...proc.context ? { instanceId: proc.context.instanceId } : {}
52
+ });
53
+ }
54
+ return entries;
55
+ }
56
+ function findEntries(surface, capabilityId, opts) {
57
+ const snapshot = surface.snapshot();
58
+ return collectCapabilities(snapshot).filter(
59
+ (e) => e.capabilityId === capabilityId && (opts?.instanceId === void 0 || e.instanceId === opts.instanceId)
60
+ );
61
+ }
62
+ function toExpose(received, capabilityId, opts) {
63
+ if (!isTestSurface(received)) {
64
+ return { pass: false, message: () => "toExpose expects a TestSurface" };
65
+ }
66
+ const entries = findEntries(received, capabilityId, opts);
67
+ const pass = entries.some((e) => e.available);
68
+ return {
69
+ pass,
70
+ message: () => pass ? `expected surface not to expose ${capabilityId}, but it is exposed and available` : entries.length > 0 ? `expected ${capabilityId} to be available, but it is visible-disabled (${entries[0]?.unavailableReason ?? "no reason"})` : `expected surface to expose ${capabilityId}, but it is absent (hidden or unregistered)`
71
+ };
72
+ }
73
+ function toExposeUnavailable(received, capabilityId, opts) {
74
+ if (!isTestSurface(received)) {
75
+ return { pass: false, message: () => "toExposeUnavailable expects a TestSurface" };
76
+ }
77
+ const entries = findEntries(received, capabilityId, opts);
78
+ const disabled = entries.filter((e) => !e.available);
79
+ const pass = disabled.length > 0 && (opts?.reason === void 0 || disabled.some((e) => e.unavailableReason === opts.reason));
80
+ return {
81
+ pass,
82
+ message: () => {
83
+ if (entries.length === 0) {
84
+ return `expected ${capabilityId} to be visible-disabled, but it is absent from the snapshot (hidden)`;
85
+ }
86
+ if (disabled.length === 0) {
87
+ return `expected ${capabilityId} to be visible-disabled, but it is available`;
88
+ }
89
+ return `expected ${capabilityId} unavailableReason ${JSON.stringify(opts?.reason)}, got ${JSON.stringify(disabled[0]?.unavailableReason)}`;
90
+ }
91
+ };
92
+ }
93
+ function toBeOk(received) {
94
+ const result = received;
95
+ const pass = typeof result === "object" && result !== null && result.status === "ok";
96
+ return {
97
+ pass,
98
+ message: () => pass ? "expected invocation result not to be ok" : `expected ok result, got ${JSON.stringify(
99
+ result?.error ?? result
100
+ )}`
101
+ };
102
+ }
103
+ function toFailWith(received, code, detailsSubset) {
104
+ const result = received;
105
+ if (typeof result !== "object" || result === null || result.status !== "error") {
106
+ return {
107
+ pass: false,
108
+ message: () => `expected an error result with code ${code}, got ${JSON.stringify(result)}`
109
+ };
110
+ }
111
+ if (result.error.code !== code) {
112
+ return {
113
+ pass: false,
114
+ message: () => `expected error code ${code}, got ${result.error.code} (${result.error.message})`
115
+ };
116
+ }
117
+ if (detailsSubset) {
118
+ for (const [key, expected] of Object.entries(detailsSubset)) {
119
+ const actual = result.error.details?.[key];
120
+ if (!jsonDeepEqual(actual, expected)) {
121
+ return {
122
+ pass: false,
123
+ message: () => `expected details.${key} = ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
124
+ };
125
+ }
126
+ }
127
+ }
128
+ return { pass: true, message: () => `expected result not to fail with ${code}` };
129
+ }
130
+ function toMatchSurfaceSnapshot(received) {
131
+ if (!isTestSurface(received)) {
132
+ return { pass: false, message: () => "toMatchSurfaceSnapshot expects a TestSurface" };
133
+ }
134
+ const normalized = serializeSurfaceSnapshot(received.snapshot());
135
+ const expectFn = globalThis.expect;
136
+ if (!expectFn) {
137
+ return {
138
+ pass: false,
139
+ message: () => "toMatchSurfaceSnapshot requires a global expect with snapshot support (vitest globals or jest)"
140
+ };
141
+ }
142
+ try {
143
+ expectFn(normalized).toMatchSnapshot();
144
+ return { pass: true, message: () => "surface snapshot matched" };
145
+ } catch (err) {
146
+ return {
147
+ pass: false,
148
+ message: () => err instanceof Error ? err.message : String(err)
149
+ };
150
+ }
151
+ }
152
+ var matchers = {
153
+ toExpose,
154
+ toExposeUnavailable,
155
+ toBeOk,
156
+ toFailWith,
157
+ toMatchSurfaceSnapshot
158
+ };
159
+ var globalExpect = globalThis.expect;
160
+ if (globalExpect?.extend) {
161
+ globalExpect.extend(matchers);
162
+ }
163
+
164
+ export {
165
+ serializeSurfaceSnapshot,
166
+ toExpose,
167
+ toExposeUnavailable,
168
+ toBeOk,
169
+ toFailWith,
170
+ toMatchSurfaceSnapshot,
171
+ matchers
172
+ };
173
+ //# sourceMappingURL=chunk-2E5UCTGQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/matchers.ts","../src/serialize.ts"],"sourcesContent":["import type {\n AgentCapabilityErrorCode,\n AgentInvocationResult,\n AgentSurfaceSnapshot,\n JsonValue,\n} from \"@agent-surface/core\";\nimport { jsonDeepEqual } from \"@agent-surface/core\";\nimport type { TestSurface } from \"./harness.js\";\nimport { serializeSurfaceSnapshot } from \"./serialize.js\";\n\ninterface MatcherResult {\n pass: boolean;\n message: () => string;\n}\n\ninterface CapabilityEntry {\n capabilityId: string;\n available: boolean;\n unavailableReason?: string;\n instanceId?: string;\n}\n\nfunction isTestSurface(value: unknown): value is TestSurface {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as TestSurface).snapshot === \"function\" &&\n typeof (value as TestSurface).invoke === \"function\"\n );\n}\n\nfunction collectCapabilities(snapshot: AgentSurfaceSnapshot): CapabilityEntry[] {\n const entries: CapabilityEntry[] = [];\n for (const component of snapshot.components) {\n for (const cap of [...component.observations, ...component.actions]) {\n entries.push({\n capabilityId: cap.capabilityId,\n available: cap.available,\n unavailableReason: cap.unavailableReason,\n instanceId: component.instanceId,\n });\n }\n }\n for (const proc of snapshot.procedures) {\n entries.push({\n capabilityId: proc.procedureId,\n available: proc.available,\n unavailableReason: proc.unavailableReason,\n ...(proc.context ? { instanceId: proc.context.instanceId } : {}),\n });\n }\n return entries;\n}\n\nfunction findEntries(\n surface: TestSurface,\n capabilityId: string,\n opts?: { instanceId?: string },\n): CapabilityEntry[] {\n const snapshot = surface.snapshot();\n return collectCapabilities(snapshot).filter(\n (e) =>\n e.capabilityId === capabilityId &&\n (opts?.instanceId === undefined || e.instanceId === opts.instanceId),\n );\n}\n\n/**\n * `toExpose` = present AND available for the harness consumer.\n * Hidden ≠ disabled: that distinction is the security model (docs/06).\n */\nexport function toExpose(\n received: unknown,\n capabilityId: string,\n opts?: { instanceId?: string },\n): MatcherResult {\n if (!isTestSurface(received)) {\n return { pass: false, message: () => \"toExpose expects a TestSurface\" };\n }\n const entries = findEntries(received, capabilityId, opts);\n const pass = entries.some((e) => e.available);\n return {\n pass,\n message: () =>\n pass\n ? `expected surface not to expose ${capabilityId}, but it is exposed and available`\n : entries.length > 0\n ? `expected ${capabilityId} to be available, but it is visible-disabled (${entries[0]?.unavailableReason ?? \"no reason\"})`\n : `expected surface to expose ${capabilityId}, but it is absent (hidden or unregistered)`,\n };\n}\n\n/** `toExposeUnavailable` = present with available: false (+ optional reason). */\nexport function toExposeUnavailable(\n received: unknown,\n capabilityId: string,\n opts?: { instanceId?: string; reason?: string },\n): MatcherResult {\n if (!isTestSurface(received)) {\n return { pass: false, message: () => \"toExposeUnavailable expects a TestSurface\" };\n }\n const entries = findEntries(received, capabilityId, opts);\n const disabled = entries.filter((e) => !e.available);\n const pass =\n disabled.length > 0 &&\n (opts?.reason === undefined || disabled.some((e) => e.unavailableReason === opts.reason));\n return {\n pass,\n message: () => {\n if (entries.length === 0) {\n return `expected ${capabilityId} to be visible-disabled, but it is absent from the snapshot (hidden)`;\n }\n if (disabled.length === 0) {\n return `expected ${capabilityId} to be visible-disabled, but it is available`;\n }\n return `expected ${capabilityId} unavailableReason ${JSON.stringify(opts?.reason)}, got ${JSON.stringify(disabled[0]?.unavailableReason)}`;\n },\n };\n}\n\nexport function toBeOk(received: unknown): MatcherResult {\n const result = received as AgentInvocationResult;\n const pass =\n typeof result === \"object\" && result !== null && (result as { status?: string }).status === \"ok\";\n return {\n pass,\n message: () =>\n pass\n ? \"expected invocation result not to be ok\"\n : `expected ok result, got ${JSON.stringify(\n (result as { error?: unknown })?.error ?? result,\n )}`,\n };\n}\n\nexport function toFailWith(\n received: unknown,\n code: AgentCapabilityErrorCode,\n detailsSubset?: Record<string, JsonValue>,\n): MatcherResult {\n const result = received as AgentInvocationResult;\n if (typeof result !== \"object\" || result === null || result.status !== \"error\") {\n return {\n pass: false,\n message: () => `expected an error result with code ${code}, got ${JSON.stringify(result)}`,\n };\n }\n if (result.error.code !== code) {\n return {\n pass: false,\n message: () =>\n `expected error code ${code}, got ${result.error.code} (${result.error.message})`,\n };\n }\n if (detailsSubset) {\n for (const [key, expected] of Object.entries(detailsSubset)) {\n const actual = result.error.details?.[key];\n if (!jsonDeepEqual(actual, expected)) {\n return {\n pass: false,\n message: () =>\n `expected details.${key} = ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n };\n }\n }\n }\n return { pass: true, message: () => `expected result not to fail with ${code}` };\n}\n\n/**\n * Semantic surface snapshot matcher. Delegates to the runner's own\n * toMatchSnapshot on the normalized form; `serializeSurfaceSnapshot` is the\n * underlying primitive if you prefer explicit snapshots.\n */\nexport function toMatchSurfaceSnapshot(received: unknown): MatcherResult {\n if (!isTestSurface(received)) {\n return { pass: false, message: () => \"toMatchSurfaceSnapshot expects a TestSurface\" };\n }\n const normalized = serializeSurfaceSnapshot(received.snapshot());\n const expectFn = (globalThis as { expect?: (v: unknown) => { toMatchSnapshot(): void } }).expect;\n if (!expectFn) {\n return {\n pass: false,\n message: () =>\n \"toMatchSurfaceSnapshot requires a global expect with snapshot support (vitest globals or jest)\",\n };\n }\n try {\n expectFn(normalized).toMatchSnapshot();\n return { pass: true, message: () => \"surface snapshot matched\" };\n } catch (err) {\n return {\n pass: false,\n message: () => (err instanceof Error ? err.message : String(err)),\n };\n }\n}\n\nexport const matchers = {\n toExpose,\n toExposeUnavailable,\n toBeOk,\n toFailWith,\n toMatchSurfaceSnapshot,\n};\n\n/**\n * Matcher surface for `expect.extend(matchers)`. Augment your test runner's\n * Assertion interface with this shape, e.g. for vitest:\n *\n * declare module \"vitest\" {\n * interface Assertion<T = any> extends AgentSurfaceMatchers<T> {}\n * }\n */\nexport interface AgentSurfaceMatchers<R = unknown> {\n toExpose(capabilityId: string, opts?: { instanceId?: string }): R;\n toExposeUnavailable(\n capabilityId: string,\n opts?: { instanceId?: string; reason?: string },\n ): R;\n toBeOk(): R;\n toFailWith(code: AgentCapabilityErrorCode, detailsSubset?: Record<string, JsonValue>): R;\n toMatchSurfaceSnapshot(): R;\n}\n\n// Auto-extend when a global expect (vitest globals / jest) is present.\nconst globalExpect = (globalThis as { expect?: { extend?: (m: object) => void } }).expect;\nif (globalExpect?.extend) {\n globalExpect.extend(matchers);\n}\n","import type { AgentSurfaceSnapshot } from \"@agent-surface/core\";\n\nexport interface SerializeSurfaceOptions {\n /** Include surfaceVersion in the output. Default false (volatile). */\n includeVersion?: boolean;\n}\n\n/**\n * Semantic snapshot serialization (docs/08): registrationIds become stable\n * placeholders in first-appearance order (`<reg#1>`, `<reg#2>` …);\n * surfaceId/capturedAt are dropped; surfaceVersion dropped by default;\n * components/capabilities keep the canonical registry ordering; schemas are\n * included verbatim — schema drift is exactly what these snapshots catch.\n */\nexport function serializeSurfaceSnapshot(\n snapshot: AgentSurfaceSnapshot,\n options?: SerializeSurfaceOptions,\n): Record<string, unknown> {\n const regIdMap = new Map<string, string>();\n const normalizeRegId = (id: string): string => {\n let placeholder = regIdMap.get(id);\n if (!placeholder) {\n placeholder = `<reg#${regIdMap.size + 1}>`;\n regIdMap.set(id, placeholder);\n }\n return placeholder;\n };\n\n return {\n ...(options?.includeVersion ? { surfaceVersion: snapshot.surfaceVersion } : {}),\n ...(snapshot.route ? { route: snapshot.route } : {}),\n components: snapshot.components.map((component) => ({\n ...component,\n registrationId: normalizeRegId(component.registrationId),\n })),\n procedures: snapshot.procedures.map((procedure) => ({\n ...procedure,\n registrationId: normalizeRegId(procedure.registrationId),\n })),\n ...(snapshot.truncated ? { truncated: snapshot.truncated } : {}),\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,127 @@
1
+ // src/harness.ts
2
+ import {
3
+ createAgentSurfaceRegistry,
4
+ memoryAuditSink
5
+ } from "@agent-surface/core";
6
+ function createTestSurface(options) {
7
+ const hostRef = { current: options?.host ?? {} };
8
+ const consumer = options?.consumer ?? { id: "test", kind: "test" };
9
+ const auditSink = memoryAuditSink({ capacity: 5e3 });
10
+ const ownsRegistry = options?.registry === void 0;
11
+ const registry = options?.registry ?? createAgentSurfaceRegistry({
12
+ environment: "test",
13
+ context: () => hostRef.current,
14
+ audit: auditSink
15
+ });
16
+ const events = [];
17
+ const unsubscribe = registry.subscribe((event) => {
18
+ events.push(event);
19
+ });
20
+ function resolveRegistrationId(capabilityId, instanceId) {
21
+ const snapshot = registry.snapshot({ consumer });
22
+ const matches = [];
23
+ for (const component of snapshot.components) {
24
+ if (instanceId !== void 0 && component.instanceId !== instanceId) continue;
25
+ const caps = [...component.observations, ...component.actions];
26
+ if (caps.some((c) => c.capabilityId === capabilityId)) {
27
+ matches.push(component.registrationId);
28
+ }
29
+ }
30
+ for (const proc of snapshot.procedures) {
31
+ if (proc.procedureId === capabilityId) matches.push(proc.registrationId);
32
+ }
33
+ return matches.length === 1 ? matches[0] : void 0;
34
+ }
35
+ function pickPending(confirmationId) {
36
+ if (confirmationId !== void 0) return confirmationId;
37
+ const pending = registry.confirmations.pending();
38
+ if (pending.length === 1) return pending[0].confirmationId;
39
+ throw new Error(
40
+ pending.length === 0 ? "no pending confirmation to resolve" : "multiple pending confirmations \u2014 pass an explicit confirmationId"
41
+ );
42
+ }
43
+ const surface = {
44
+ registry,
45
+ snapshot(ctx) {
46
+ return registry.snapshot({ consumer, ...ctx });
47
+ },
48
+ invoke(capabilityId, input, opts) {
49
+ const registrationId = opts?.registrationId !== void 0 ? opts.registrationId : resolveRegistrationId(capabilityId, opts?.instanceId);
50
+ return registry.invoke(
51
+ {
52
+ capabilityId,
53
+ ...input !== void 0 ? { input } : {},
54
+ ...opts?.instanceId !== void 0 ? { instanceId: opts.instanceId } : {},
55
+ ...registrationId !== void 0 ? { registrationId } : {},
56
+ ...opts?.surfaceVersion !== void 0 ? { surfaceVersion: opts.surfaceVersion } : {},
57
+ ...opts?.confirmationId !== void 0 ? { confirmationId: opts.confirmationId } : {}
58
+ },
59
+ { consumer: opts?.consumer ?? consumer }
60
+ );
61
+ },
62
+ async observe(capabilityId, opts) {
63
+ const versionBefore = registry.getVersion();
64
+ const result = await surface.invoke(capabilityId, void 0, opts);
65
+ if (result.status === "error") {
66
+ throw new Error(
67
+ `observe(${capabilityId}) failed: ${result.error.code} \u2014 ${result.error.message}`
68
+ );
69
+ }
70
+ const versionAfter = registry.getVersion();
71
+ if (versionAfter !== versionBefore) {
72
+ throw new Error(
73
+ `observe(${capabilityId}) mutated the surface (version ${versionBefore} \u2192 ${versionAfter}); observations MUST be side-effect free (docs/01)`
74
+ );
75
+ }
76
+ return result.output;
77
+ },
78
+ captureRef(capabilityId, instanceId) {
79
+ const registrationId = resolveRegistrationId(capabilityId, instanceId);
80
+ if (registrationId === void 0) {
81
+ throw new Error(`captureRef: no unique live registration exposes ${capabilityId}`);
82
+ }
83
+ return { registrationId, surfaceVersion: registry.getVersion() };
84
+ },
85
+ as(host) {
86
+ if (!ownsRegistry) {
87
+ throw new Error(
88
+ "as() requires a harness-created registry \u2014 pass `host` through your own RegistryOptions.context instead"
89
+ );
90
+ }
91
+ hostRef.current = host;
92
+ },
93
+ confirmations: {
94
+ pending() {
95
+ return registry.confirmations.pending();
96
+ },
97
+ approve(confirmationId) {
98
+ registry.confirmations.resolve(pickPending(confirmationId), { approved: true });
99
+ },
100
+ deny(confirmationId, reason) {
101
+ registry.confirmations.resolve(pickPending(confirmationId), {
102
+ approved: false,
103
+ ...reason !== void 0 ? { reason } : {}
104
+ });
105
+ },
106
+ expire(confirmationId) {
107
+ registry.confirmations.forceExpire(pickPending(confirmationId));
108
+ }
109
+ },
110
+ events() {
111
+ return [...events];
112
+ },
113
+ auditLog() {
114
+ return auditSink.events();
115
+ },
116
+ dispose() {
117
+ unsubscribe();
118
+ if (ownsRegistry) registry.dispose();
119
+ }
120
+ };
121
+ return surface;
122
+ }
123
+
124
+ export {
125
+ createTestSurface
126
+ };
127
+ //# sourceMappingURL=chunk-QV7I5CCY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/harness.ts"],"sourcesContent":["import {\n createAgentSurfaceRegistry,\n memoryAuditSink,\n type AgentConsumer,\n type AgentInvocationResult,\n type AgentSurfaceEvent,\n type AgentSurfaceRegistry,\n type AgentSurfaceSnapshot,\n type AuditEvent,\n type JsonValue,\n type PendingConfirmation,\n type SnapshotContext,\n} from \"@agent-surface/core\";\n\nexport interface TestSurfaceOptions {\n registry?: AgentSurfaceRegistry; // default: fresh registry, environment \"test\"\n consumer?: AgentConsumer; // default {id:\"test\", kind:\"test\"}\n host?: Record<string, unknown>; // overrides RegistryOptions.context()\n}\n\nexport interface TestSurface {\n registry: AgentSurfaceRegistry;\n snapshot(ctx?: Partial<SnapshotContext>): AgentSurfaceSnapshot;\n\n /** Invoke with test conveniences: auto invocationId, auto registrationId\n resolution from the latest snapshot, typed input. */\n invoke(\n capabilityId: string,\n input?: JsonValue,\n opts?: {\n instanceId?: string;\n registrationId?: string; // pass a captured one to simulate staleness\n surfaceVersion?: string;\n confirmationId?: string;\n consumer?: AgentConsumer;\n },\n ): Promise<AgentInvocationResult>;\n\n /** Sugar: invoke an observation and return its parsed output (throws on error). */\n observe<T = JsonValue>(capabilityId: string, opts?: { instanceId?: string }): Promise<T>;\n\n /** Capture current resolution tokens for later stale-invocation tests. */\n captureRef(\n capabilityId: string,\n instanceId?: string,\n ): { registrationId: string; surfaceVersion: string };\n\n /** Swap host context mid-test (auth changes): as({user: admin}). */\n as(host: Record<string, unknown>): void;\n\n confirmations: {\n pending(): PendingConfirmation[];\n approve(confirmationId?: string): void; // default: the only pending one\n deny(confirmationId?: string, reason?: string): void;\n expire(confirmationId?: string): void; // force-expires that record\n };\n\n /** All registry events recorded since creation, in order. */\n events(): AgentSurfaceEvent[];\n auditLog(): AuditEvent[];\n\n dispose(): void;\n}\n\nexport function createTestSurface(options?: TestSurfaceOptions): TestSurface {\n const hostRef: { current: Record<string, unknown> } = { current: options?.host ?? {} };\n const consumer: AgentConsumer = options?.consumer ?? { id: \"test\", kind: \"test\" };\n const auditSink = memoryAuditSink({ capacity: 5000 });\n const ownsRegistry = options?.registry === undefined;\n\n const registry =\n options?.registry ??\n createAgentSurfaceRegistry({\n environment: \"test\",\n context: () => hostRef.current,\n audit: auditSink,\n });\n\n const events: AgentSurfaceEvent[] = [];\n const unsubscribe = registry.subscribe((event) => {\n events.push(event);\n });\n\n function resolveRegistrationId(\n capabilityId: string,\n instanceId: string | undefined,\n ): string | undefined {\n const snapshot = registry.snapshot({ consumer });\n const matches: string[] = [];\n for (const component of snapshot.components) {\n if (instanceId !== undefined && component.instanceId !== instanceId) continue;\n const caps = [...component.observations, ...component.actions];\n if (caps.some((c) => c.capabilityId === capabilityId)) {\n matches.push(component.registrationId);\n }\n }\n for (const proc of snapshot.procedures) {\n if (proc.procedureId === capabilityId) matches.push(proc.registrationId);\n }\n // Auto-attach only when unambiguous; otherwise let the registry produce\n // AMBIGUOUS_INSTANCE / CAPABILITY_NOT_FOUND / COMPONENT_UNMOUNTED.\n return matches.length === 1 ? matches[0] : undefined;\n }\n\n function pickPending(confirmationId: string | undefined): string {\n if (confirmationId !== undefined) return confirmationId;\n const pending = registry.confirmations.pending();\n if (pending.length === 1) return pending[0]!.confirmationId;\n throw new Error(\n pending.length === 0\n ? \"no pending confirmation to resolve\"\n : \"multiple pending confirmations — pass an explicit confirmationId\",\n );\n }\n\n const surface: TestSurface = {\n registry,\n\n snapshot(ctx) {\n return registry.snapshot({ consumer, ...ctx });\n },\n\n invoke(capabilityId, input, opts) {\n const registrationId =\n opts?.registrationId !== undefined\n ? opts.registrationId\n : resolveRegistrationId(capabilityId, opts?.instanceId);\n return registry.invoke(\n {\n capabilityId,\n ...(input !== undefined ? { input } : {}),\n ...(opts?.instanceId !== undefined ? { instanceId: opts.instanceId } : {}),\n ...(registrationId !== undefined ? { registrationId } : {}),\n ...(opts?.surfaceVersion !== undefined ? { surfaceVersion: opts.surfaceVersion } : {}),\n ...(opts?.confirmationId !== undefined ? { confirmationId: opts.confirmationId } : {}),\n },\n { consumer: opts?.consumer ?? consumer },\n );\n },\n\n async observe<T = JsonValue>(capabilityId: string, opts?: { instanceId?: string }): Promise<T> {\n const versionBefore = registry.getVersion();\n const result = await surface.invoke(capabilityId, undefined, opts);\n if (result.status === \"error\") {\n throw new Error(\n `observe(${capabilityId}) failed: ${result.error.code} — ${result.error.message}`,\n );\n }\n const versionAfter = registry.getVersion();\n if (versionAfter !== versionBefore) {\n throw new Error(\n `observe(${capabilityId}) mutated the surface (version ${versionBefore} → ${versionAfter}); observations MUST be side-effect free (docs/01)`,\n );\n }\n return result.output as T;\n },\n\n captureRef(capabilityId, instanceId) {\n const registrationId = resolveRegistrationId(capabilityId, instanceId);\n if (registrationId === undefined) {\n throw new Error(`captureRef: no unique live registration exposes ${capabilityId}`);\n }\n return { registrationId, surfaceVersion: registry.getVersion() };\n },\n\n as(host) {\n if (!ownsRegistry) {\n throw new Error(\n \"as() requires a harness-created registry — pass `host` through your own RegistryOptions.context instead\",\n );\n }\n hostRef.current = host;\n },\n\n confirmations: {\n pending() {\n return registry.confirmations.pending();\n },\n approve(confirmationId) {\n registry.confirmations.resolve(pickPending(confirmationId), { approved: true });\n },\n deny(confirmationId, reason) {\n registry.confirmations.resolve(pickPending(confirmationId), {\n approved: false,\n ...(reason !== undefined ? { reason } : {}),\n });\n },\n expire(confirmationId) {\n registry.confirmations.forceExpire(pickPending(confirmationId));\n },\n },\n\n events() {\n return [...events];\n },\n\n auditLog() {\n return auditSink.events();\n },\n\n dispose() {\n unsubscribe();\n if (ownsRegistry) registry.dispose();\n },\n };\n\n return surface;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,44 @@
1
+ import { AgentSurfaceRegistry, SnapshotContext, AgentSurfaceSnapshot, JsonValue, AgentConsumer, AgentInvocationResult, PendingConfirmation, AgentSurfaceEvent, AuditEvent } from '@agent-surface/core';
2
+
3
+ interface TestSurfaceOptions {
4
+ registry?: AgentSurfaceRegistry;
5
+ consumer?: AgentConsumer;
6
+ host?: Record<string, unknown>;
7
+ }
8
+ interface TestSurface {
9
+ registry: AgentSurfaceRegistry;
10
+ snapshot(ctx?: Partial<SnapshotContext>): AgentSurfaceSnapshot;
11
+ /** Invoke with test conveniences: auto invocationId, auto registrationId
12
+ resolution from the latest snapshot, typed input. */
13
+ invoke(capabilityId: string, input?: JsonValue, opts?: {
14
+ instanceId?: string;
15
+ registrationId?: string;
16
+ surfaceVersion?: string;
17
+ confirmationId?: string;
18
+ consumer?: AgentConsumer;
19
+ }): Promise<AgentInvocationResult>;
20
+ /** Sugar: invoke an observation and return its parsed output (throws on error). */
21
+ observe<T = JsonValue>(capabilityId: string, opts?: {
22
+ instanceId?: string;
23
+ }): Promise<T>;
24
+ /** Capture current resolution tokens for later stale-invocation tests. */
25
+ captureRef(capabilityId: string, instanceId?: string): {
26
+ registrationId: string;
27
+ surfaceVersion: string;
28
+ };
29
+ /** Swap host context mid-test (auth changes): as({user: admin}). */
30
+ as(host: Record<string, unknown>): void;
31
+ confirmations: {
32
+ pending(): PendingConfirmation[];
33
+ approve(confirmationId?: string): void;
34
+ deny(confirmationId?: string, reason?: string): void;
35
+ expire(confirmationId?: string): void;
36
+ };
37
+ /** All registry events recorded since creation, in order. */
38
+ events(): AgentSurfaceEvent[];
39
+ auditLog(): AuditEvent[];
40
+ dispose(): void;
41
+ }
42
+ declare function createTestSurface(options?: TestSurfaceOptions): TestSurface;
43
+
44
+ export { type TestSurface as T, type TestSurfaceOptions as a, createTestSurface as c };
@@ -0,0 +1,18 @@
1
+ export { T as TestSurface, a as TestSurfaceOptions, c as createTestSurface } from './harness-B_t1q70R.js';
2
+ import { AgentSurfaceSnapshot } from '@agent-surface/core';
3
+ export { AgentSurfaceMatchers, matchers, toBeOk, toExpose, toExposeUnavailable, toFailWith, toMatchSurfaceSnapshot } from './matchers.js';
4
+
5
+ interface SerializeSurfaceOptions {
6
+ /** Include surfaceVersion in the output. Default false (volatile). */
7
+ includeVersion?: boolean;
8
+ }
9
+ /**
10
+ * Semantic snapshot serialization (docs/08): registrationIds become stable
11
+ * placeholders in first-appearance order (`<reg#1>`, `<reg#2>` …);
12
+ * surfaceId/capturedAt are dropped; surfaceVersion dropped by default;
13
+ * components/capabilities keep the canonical registry ordering; schemas are
14
+ * included verbatim — schema drift is exactly what these snapshots catch.
15
+ */
16
+ declare function serializeSurfaceSnapshot(snapshot: AgentSurfaceSnapshot, options?: SerializeSurfaceOptions): Record<string, unknown>;
17
+
18
+ export { type SerializeSurfaceOptions, serializeSurfaceSnapshot };
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ matchers,
3
+ serializeSurfaceSnapshot,
4
+ toBeOk,
5
+ toExpose,
6
+ toExposeUnavailable,
7
+ toFailWith,
8
+ toMatchSurfaceSnapshot
9
+ } from "./chunk-2E5UCTGQ.js";
10
+ import {
11
+ createTestSurface
12
+ } from "./chunk-QV7I5CCY.js";
13
+ export {
14
+ createTestSurface,
15
+ matchers,
16
+ serializeSurfaceSnapshot,
17
+ toBeOk,
18
+ toExpose,
19
+ toExposeUnavailable,
20
+ toFailWith,
21
+ toMatchSurfaceSnapshot
22
+ };
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,55 @@
1
+ import { AgentCapabilityErrorCode, JsonValue } from '@agent-surface/core';
2
+
3
+ interface MatcherResult {
4
+ pass: boolean;
5
+ message: () => string;
6
+ }
7
+ /**
8
+ * `toExpose` = present AND available for the harness consumer.
9
+ * Hidden ≠ disabled: that distinction is the security model (docs/06).
10
+ */
11
+ declare function toExpose(received: unknown, capabilityId: string, opts?: {
12
+ instanceId?: string;
13
+ }): MatcherResult;
14
+ /** `toExposeUnavailable` = present with available: false (+ optional reason). */
15
+ declare function toExposeUnavailable(received: unknown, capabilityId: string, opts?: {
16
+ instanceId?: string;
17
+ reason?: string;
18
+ }): MatcherResult;
19
+ declare function toBeOk(received: unknown): MatcherResult;
20
+ declare function toFailWith(received: unknown, code: AgentCapabilityErrorCode, detailsSubset?: Record<string, JsonValue>): MatcherResult;
21
+ /**
22
+ * Semantic surface snapshot matcher. Delegates to the runner's own
23
+ * toMatchSnapshot on the normalized form; `serializeSurfaceSnapshot` is the
24
+ * underlying primitive if you prefer explicit snapshots.
25
+ */
26
+ declare function toMatchSurfaceSnapshot(received: unknown): MatcherResult;
27
+ declare const matchers: {
28
+ toExpose: typeof toExpose;
29
+ toExposeUnavailable: typeof toExposeUnavailable;
30
+ toBeOk: typeof toBeOk;
31
+ toFailWith: typeof toFailWith;
32
+ toMatchSurfaceSnapshot: typeof toMatchSurfaceSnapshot;
33
+ };
34
+ /**
35
+ * Matcher surface for `expect.extend(matchers)`. Augment your test runner's
36
+ * Assertion interface with this shape, e.g. for vitest:
37
+ *
38
+ * declare module "vitest" {
39
+ * interface Assertion<T = any> extends AgentSurfaceMatchers<T> {}
40
+ * }
41
+ */
42
+ interface AgentSurfaceMatchers<R = unknown> {
43
+ toExpose(capabilityId: string, opts?: {
44
+ instanceId?: string;
45
+ }): R;
46
+ toExposeUnavailable(capabilityId: string, opts?: {
47
+ instanceId?: string;
48
+ reason?: string;
49
+ }): R;
50
+ toBeOk(): R;
51
+ toFailWith(code: AgentCapabilityErrorCode, detailsSubset?: Record<string, JsonValue>): R;
52
+ toMatchSurfaceSnapshot(): R;
53
+ }
54
+
55
+ export { type AgentSurfaceMatchers, matchers, toBeOk, toExpose, toExposeUnavailable, toFailWith, toMatchSurfaceSnapshot };
@@ -0,0 +1,17 @@
1
+ import {
2
+ matchers,
3
+ toBeOk,
4
+ toExpose,
5
+ toExposeUnavailable,
6
+ toFailWith,
7
+ toMatchSurfaceSnapshot
8
+ } from "./chunk-2E5UCTGQ.js";
9
+ export {
10
+ matchers,
11
+ toBeOk,
12
+ toExpose,
13
+ toExposeUnavailable,
14
+ toFailWith,
15
+ toMatchSurfaceSnapshot
16
+ };
17
+ //# sourceMappingURL=matchers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,24 @@
1
+ import { ComponentType, ReactNode, ReactElement } from 'react';
2
+ import { RenderResult } from '@testing-library/react';
3
+ import { a as TestSurfaceOptions, T as TestSurface } from './harness-B_t1q70R.js';
4
+ import '@agent-surface/core';
5
+
6
+ interface RenderAgentSurfaceOptions extends TestSurfaceOptions {
7
+ wrapper?: ComponentType<{
8
+ children: ReactNode;
9
+ }>;
10
+ }
11
+ interface RenderedAgentSurface extends TestSurface {
12
+ /** RTL render result (rerender/unmount/container…). */
13
+ view: RenderResult;
14
+ rerender(ui: ReactElement): void;
15
+ unmount(): void;
16
+ }
17
+ /**
18
+ * Renders `ui` inside an AgentSurfaceProvider bound to a harness registry and
19
+ * resolves after mount effects flush, so registrations are live (docs/08).
20
+ * Runs fine under React Strict Mode — keep it on to prove cleanup symmetry.
21
+ */
22
+ declare function renderAgentSurface(ui: ReactElement, options?: RenderAgentSurfaceOptions): Promise<RenderedAgentSurface>;
23
+
24
+ export { type RenderAgentSurfaceOptions, type RenderedAgentSurface, renderAgentSurface };
package/dist/react.js ADDED
@@ -0,0 +1,91 @@
1
+ import {
2
+ createTestSurface
3
+ } from "./chunk-QV7I5CCY.js";
4
+
5
+ // src/react.ts
6
+ import { createElement } from "react";
7
+ import { act, render } from "@testing-library/react";
8
+ import { AgentSurfaceProvider } from "@agent-surface/react";
9
+ async function renderAgentSurface(ui, options) {
10
+ const surface = createTestSurface(options);
11
+ const Wrapper = options?.wrapper;
12
+ const wrap = (element) => {
13
+ const provided = createElement(AgentSurfaceProvider, {
14
+ registry: surface.registry,
15
+ children: element
16
+ });
17
+ return Wrapper ? createElement(Wrapper, null, provided) : provided;
18
+ };
19
+ let view;
20
+ await act(async () => {
21
+ view = render(wrap(ui));
22
+ });
23
+ const actInvoke = async (...args) => {
24
+ let result;
25
+ await act(async () => {
26
+ result = await surface.invoke(...args);
27
+ });
28
+ return result;
29
+ };
30
+ return {
31
+ ...surface,
32
+ invoke: actInvoke,
33
+ async observe(capabilityId, opts) {
34
+ const versionBefore = surface.registry.getVersion();
35
+ const result = await actInvoke(capabilityId, void 0, opts);
36
+ if (result.status === "error") {
37
+ throw new Error(
38
+ `observe(${capabilityId}) failed: ${result.error.code} \u2014 ${result.error.message}`
39
+ );
40
+ }
41
+ if (surface.registry.getVersion() !== versionBefore) {
42
+ throw new Error(
43
+ `observe(${capabilityId}) mutated the surface; observations MUST be side-effect free (docs/01)`
44
+ );
45
+ }
46
+ return result.output;
47
+ },
48
+ confirmations: {
49
+ pending: () => surface.confirmations.pending(),
50
+ approve(confirmationId) {
51
+ act(() => {
52
+ surface.confirmations.approve(confirmationId);
53
+ });
54
+ },
55
+ deny(confirmationId, reason) {
56
+ act(() => {
57
+ surface.confirmations.deny(confirmationId, reason);
58
+ });
59
+ },
60
+ expire(confirmationId) {
61
+ act(() => {
62
+ surface.confirmations.expire(confirmationId);
63
+ });
64
+ }
65
+ },
66
+ view,
67
+ rerender(next) {
68
+ act(() => {
69
+ view.rerender(wrap(next));
70
+ });
71
+ },
72
+ unmount() {
73
+ act(() => {
74
+ view.unmount();
75
+ });
76
+ },
77
+ dispose() {
78
+ try {
79
+ act(() => {
80
+ view.unmount();
81
+ });
82
+ } catch {
83
+ }
84
+ surface.dispose();
85
+ }
86
+ };
87
+ }
88
+ export {
89
+ renderAgentSurface
90
+ };
91
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react.ts"],"sourcesContent":["import { createElement, type ComponentType, type ReactElement, type ReactNode } from \"react\";\nimport { act, render, type RenderResult } from \"@testing-library/react\";\nimport { AgentSurfaceProvider } from \"@agent-surface/react\";\nimport { createTestSurface, type TestSurface, type TestSurfaceOptions } from \"./harness.js\";\n\nexport interface RenderAgentSurfaceOptions extends TestSurfaceOptions {\n wrapper?: ComponentType<{ children: ReactNode }>; // routers, query clients…\n}\n\nexport interface RenderedAgentSurface extends TestSurface {\n /** RTL render result (rerender/unmount/container…). */\n view: RenderResult;\n rerender(ui: ReactElement): void;\n unmount(): void;\n}\n\n/**\n * Renders `ui` inside an AgentSurfaceProvider bound to a harness registry and\n * resolves after mount effects flush, so registrations are live (docs/08).\n * Runs fine under React Strict Mode — keep it on to prove cleanup symmetry.\n */\nexport async function renderAgentSurface(\n ui: ReactElement,\n options?: RenderAgentSurfaceOptions,\n): Promise<RenderedAgentSurface> {\n const surface = createTestSurface(options);\n const Wrapper = options?.wrapper;\n\n const wrap = (element: ReactElement): ReactElement => {\n const provided = createElement(AgentSurfaceProvider, {\n registry: surface.registry,\n children: element,\n });\n return Wrapper ? createElement(Wrapper, null, provided) : provided;\n };\n\n let view!: RenderResult;\n await act(async () => {\n view = render(wrap(ui));\n });\n\n // Invocations run app handlers that call setState: wrap them in act() so\n // React commits deterministically and tests stay warning-free.\n const actInvoke: TestSurface[\"invoke\"] = async (...args) => {\n let result!: Awaited<ReturnType<TestSurface[\"invoke\"]>>;\n await act(async () => {\n result = await surface.invoke(...args);\n });\n return result;\n };\n\n return {\n ...surface,\n invoke: actInvoke,\n async observe(capabilityId, opts) {\n const versionBefore = surface.registry.getVersion();\n const result = await actInvoke(capabilityId, undefined, opts);\n if (result.status === \"error\") {\n throw new Error(\n `observe(${capabilityId}) failed: ${result.error.code} — ${result.error.message}`,\n );\n }\n if (surface.registry.getVersion() !== versionBefore) {\n throw new Error(\n `observe(${capabilityId}) mutated the surface; observations MUST be side-effect free (docs/01)`,\n );\n }\n return result.output as never;\n },\n confirmations: {\n pending: () => surface.confirmations.pending(),\n approve(confirmationId) {\n act(() => {\n surface.confirmations.approve(confirmationId);\n });\n },\n deny(confirmationId, reason) {\n act(() => {\n surface.confirmations.deny(confirmationId, reason);\n });\n },\n expire(confirmationId) {\n act(() => {\n surface.confirmations.expire(confirmationId);\n });\n },\n },\n view,\n rerender(next: ReactElement) {\n act(() => {\n view.rerender(wrap(next));\n });\n },\n unmount() {\n act(() => {\n view.unmount();\n });\n },\n dispose() {\n try {\n act(() => {\n view.unmount();\n });\n } catch {\n /* already unmounted */\n }\n surface.dispose();\n },\n };\n}\n"],"mappings":";;;;;AAAA,SAAS,qBAA4E;AACrF,SAAS,KAAK,cAAiC;AAC/C,SAAS,4BAA4B;AAmBrC,eAAsB,mBACpB,IACA,SAC+B;AAC/B,QAAM,UAAU,kBAAkB,OAAO;AACzC,QAAM,UAAU,SAAS;AAEzB,QAAM,OAAO,CAAC,YAAwC;AACpD,UAAM,WAAW,cAAc,sBAAsB;AAAA,MACnD,UAAU,QAAQ;AAAA,MAClB,UAAU;AAAA,IACZ,CAAC;AACD,WAAO,UAAU,cAAc,SAAS,MAAM,QAAQ,IAAI;AAAA,EAC5D;AAEA,MAAI;AACJ,QAAM,IAAI,YAAY;AACpB,WAAO,OAAO,KAAK,EAAE,CAAC;AAAA,EACxB,CAAC;AAID,QAAM,YAAmC,UAAU,SAAS;AAC1D,QAAI;AACJ,UAAM,IAAI,YAAY;AACpB,eAAS,MAAM,QAAQ,OAAO,GAAG,IAAI;AAAA,IACvC,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,QAAQ;AAAA,IACR,MAAM,QAAQ,cAAc,MAAM;AAChC,YAAM,gBAAgB,QAAQ,SAAS,WAAW;AAClD,YAAM,SAAS,MAAM,UAAU,cAAc,QAAW,IAAI;AAC5D,UAAI,OAAO,WAAW,SAAS;AAC7B,cAAM,IAAI;AAAA,UACR,WAAW,YAAY,aAAa,OAAO,MAAM,IAAI,WAAM,OAAO,MAAM,OAAO;AAAA,QACjF;AAAA,MACF;AACA,UAAI,QAAQ,SAAS,WAAW,MAAM,eAAe;AACnD,cAAM,IAAI;AAAA,UACR,WAAW,YAAY;AAAA,QACzB;AAAA,MACF;AACA,aAAO,OAAO;AAAA,IAChB;AAAA,IACA,eAAe;AAAA,MACb,SAAS,MAAM,QAAQ,cAAc,QAAQ;AAAA,MAC7C,QAAQ,gBAAgB;AACtB,YAAI,MAAM;AACR,kBAAQ,cAAc,QAAQ,cAAc;AAAA,QAC9C,CAAC;AAAA,MACH;AAAA,MACA,KAAK,gBAAgB,QAAQ;AAC3B,YAAI,MAAM;AACR,kBAAQ,cAAc,KAAK,gBAAgB,MAAM;AAAA,QACnD,CAAC;AAAA,MACH;AAAA,MACA,OAAO,gBAAgB;AACrB,YAAI,MAAM;AACR,kBAAQ,cAAc,OAAO,cAAc;AAAA,QAC7C,CAAC;AAAA,MACH;AAAA,IACF;AAAA,IACA;AAAA,IACA,SAAS,MAAoB;AAC3B,UAAI,MAAM;AACR,aAAK,SAAS,KAAK,IAAI,CAAC;AAAA,MAC1B,CAAC;AAAA,IACH;AAAA,IACA,UAAU;AACR,UAAI,MAAM;AACR,aAAK,QAAQ;AAAA,MACf,CAAC;AAAA,IACH;AAAA,IACA,UAAU;AACR,UAAI;AACF,YAAI,MAAM;AACR,eAAK,QAAQ;AAAA,QACf,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AACA,cAAQ,QAAQ;AAAA,IAClB;AAAA,EACF;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@agent-surface/testing",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic test harness, matchers and semantic snapshots for agent surfaces — no LLM required",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./react": {
16
+ "types": "./dist/react.d.ts",
17
+ "import": "./dist/react.js"
18
+ },
19
+ "./matchers": {
20
+ "types": "./dist/matchers.d.ts",
21
+ "import": "./dist/matchers.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "LICENSE"
27
+ ],
28
+ "dependencies": {
29
+ "@agent-surface/core": "^0.1.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@testing-library/react": ">=14",
33
+ "react": ">=18.2",
34
+ "@agent-surface/react": "^0.1.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "@agent-surface/react": {
38
+ "optional": true
39
+ },
40
+ "@testing-library/react": {
41
+ "optional": true
42
+ },
43
+ "react": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@testing-library/react": "^16.3.0",
49
+ "react": "^19.1.1",
50
+ "react-dom": "^19.1.1",
51
+ "@types/react": "^19.1.9",
52
+ "vitest": "^3.2.4",
53
+ "zod": "^4.1.5",
54
+ "@agent-surface/react": "0.1.0"
55
+ },
56
+ "author": "Paolo Barbato",
57
+ "engines": {
58
+ "node": ">=20.19.0"
59
+ },
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/Wiseair-srl/agent-surface.git",
63
+ "directory": "packages/testing"
64
+ },
65
+ "homepage": "https://agent-surface-docs.vercel.app",
66
+ "bugs": {
67
+ "url": "https://github.com/Wiseair-srl/agent-surface/issues"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ },
72
+ "keywords": [
73
+ "agent-surface",
74
+ "agent",
75
+ "ai",
76
+ "llm",
77
+ "frontend",
78
+ "capabilities",
79
+ "typescript",
80
+ "testing",
81
+ "vitest",
82
+ "jest"
83
+ ],
84
+ "size-limit": [
85
+ {
86
+ "path": "dist/index.js",
87
+ "limit": "8 kB",
88
+ "ignore": [
89
+ "@agent-surface/core",
90
+ "@agent-surface/react",
91
+ "react",
92
+ "@testing-library/react"
93
+ ]
94
+ }
95
+ ],
96
+ "scripts": {
97
+ "build": "tsup",
98
+ "typecheck": "tsc --noEmit",
99
+ "size": "size-limit"
100
+ }
101
+ }