@moltzap/protocol 2026.408.0 → 2026.425.1
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/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/rpc-registry.d.ts +446 -0
- package/dist/rpc-registry.d.ts.map +1 -0
- package/dist/rpc-registry.js +67 -0
- package/dist/rpc-registry.js.map +1 -0
- package/dist/rpc.d.ts +42 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +29 -0
- package/dist/rpc.js.map +1 -0
- package/dist/schema/apps.d.ts +86 -0
- package/dist/schema/apps.d.ts.map +1 -0
- package/dist/schema/apps.js +77 -0
- package/dist/schema/apps.js.map +1 -0
- package/dist/schema/contacts.d.ts +8 -18
- package/dist/schema/contacts.d.ts.map +1 -1
- package/dist/schema/contacts.js +9 -13
- package/dist/schema/contacts.js.map +1 -1
- package/dist/schema/conversations.d.ts +15 -8
- package/dist/schema/conversations.d.ts.map +1 -1
- package/dist/schema/conversations.js +18 -7
- package/dist/schema/conversations.js.map +1 -1
- package/dist/schema/delivery.d.ts +1 -4
- package/dist/schema/delivery.d.ts.map +1 -1
- package/dist/schema/delivery.js +2 -3
- package/dist/schema/delivery.js.map +1 -1
- package/dist/schema/errors.d.ts +13 -0
- package/dist/schema/errors.d.ts.map +1 -1
- package/dist/schema/errors.js +14 -0
- package/dist/schema/errors.js.map +1 -1
- package/dist/schema/events.d.ts +112 -93
- package/dist/schema/events.d.ts.map +1 -1
- package/dist/schema/events.js +84 -25
- package/dist/schema/events.js.map +1 -1
- package/dist/schema/identity.d.ts +19 -17
- package/dist/schema/identity.d.ts.map +1 -1
- package/dist/schema/identity.js +7 -14
- package/dist/schema/identity.js.map +1 -1
- package/dist/schema/index.d.ts +4 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -2
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/messages.d.ts +3 -7
- package/dist/schema/messages.d.ts.map +1 -1
- package/dist/schema/messages.js +4 -6
- package/dist/schema/messages.js.map +1 -1
- package/dist/schema/methods/apps.d.ts +123 -0
- package/dist/schema/methods/apps.d.ts.map +1 -0
- package/dist/schema/methods/apps.js +93 -0
- package/dist/schema/methods/apps.js.map +1 -0
- package/dist/schema/methods/auth.d.ts +102 -90
- package/dist/schema/methods/auth.d.ts.map +1 -1
- package/dist/schema/methods/auth.js +86 -74
- package/dist/schema/methods/auth.js.map +1 -1
- package/dist/schema/methods/contacts.d.ts +39 -74
- package/dist/schema/methods/contacts.d.ts.map +1 -1
- package/dist/schema/methods/contacts.js +35 -29
- package/dist/schema/methods/contacts.js.map +1 -1
- package/dist/schema/methods/conversations.d.ts +48 -52
- package/dist/schema/methods/conversations.d.ts.map +1 -1
- package/dist/schema/methods/conversations.js +89 -38
- package/dist/schema/methods/conversations.js.map +1 -1
- package/dist/schema/methods/invites.d.ts +1 -3
- package/dist/schema/methods/invites.d.ts.map +1 -1
- package/dist/schema/methods/invites.js +8 -1
- package/dist/schema/methods/invites.js.map +1 -1
- package/dist/schema/methods/messages.d.ts +12 -44
- package/dist/schema/methods/messages.d.ts.map +1 -1
- package/dist/schema/methods/messages.js +22 -28
- package/dist/schema/methods/messages.js.map +1 -1
- package/dist/schema/methods/presence.d.ts +7 -22
- package/dist/schema/methods/presence.d.ts.map +1 -1
- package/dist/schema/methods/presence.js +12 -6
- package/dist/schema/methods/presence.js.map +1 -1
- package/dist/schema/methods/push.d.ts +8 -6
- package/dist/schema/methods/push.d.ts.map +1 -1
- package/dist/schema/methods/push.js +20 -7
- package/dist/schema/methods/push.js.map +1 -1
- package/dist/schema/methods/system.d.ts +4 -0
- package/dist/schema/methods/system.d.ts.map +1 -0
- package/dist/schema/methods/system.js +11 -0
- package/dist/schema/methods/system.js.map +1 -0
- package/dist/schema/presence.d.ts +1 -12
- package/dist/schema/presence.d.ts.map +1 -1
- package/dist/schema/presence.js +2 -7
- package/dist/schema/presence.js.map +1 -1
- package/dist/schema/surfaces.d.ts +31 -25
- package/dist/schema/surfaces.d.ts.map +1 -1
- package/dist/schema/surfaces.js +40 -21
- package/dist/schema/surfaces.js.map +1 -1
- package/dist/testing/agent-registration.d.ts +49 -0
- package/dist/testing/agent-registration.d.ts.map +1 -0
- package/dist/testing/agent-registration.js +77 -0
- package/dist/testing/agent-registration.js.map +1 -0
- package/dist/testing/arbitraries/frames.d.ts +24 -0
- package/dist/testing/arbitraries/frames.d.ts.map +1 -0
- package/dist/testing/arbitraries/frames.js +36 -0
- package/dist/testing/arbitraries/frames.js.map +1 -0
- package/dist/testing/arbitraries/from-typebox.d.ts +37 -0
- package/dist/testing/arbitraries/from-typebox.d.ts.map +1 -0
- package/dist/testing/arbitraries/from-typebox.js +131 -0
- package/dist/testing/arbitraries/from-typebox.js.map +1 -0
- package/dist/testing/arbitraries/index.d.ts +4 -0
- package/dist/testing/arbitraries/index.d.ts.map +1 -0
- package/dist/testing/arbitraries/index.js +4 -0
- package/dist/testing/arbitraries/index.js.map +1 -0
- package/dist/testing/arbitraries/rpc.d.ts +66 -0
- package/dist/testing/arbitraries/rpc.d.ts.map +1 -0
- package/dist/testing/arbitraries/rpc.js +98 -0
- package/dist/testing/arbitraries/rpc.js.map +1 -0
- package/dist/testing/canonicalize.d.ts +38 -0
- package/dist/testing/canonicalize.d.ts.map +1 -0
- package/dist/testing/canonicalize.js +55 -0
- package/dist/testing/canonicalize.js.map +1 -0
- package/dist/testing/captures.d.ts +61 -0
- package/dist/testing/captures.d.ts.map +1 -0
- package/dist/testing/captures.js +99 -0
- package/dist/testing/captures.js.map +1 -0
- package/dist/testing/codec.d.ts +44 -0
- package/dist/testing/codec.d.ts.map +1 -0
- package/dist/testing/codec.js +170 -0
- package/dist/testing/codec.js.map +1 -0
- package/dist/testing/conformance/__divergence_proofs__/executable-proof-helpers.d.ts +6 -0
- package/dist/testing/conformance/__divergence_proofs__/executable-proof-helpers.d.ts.map +1 -0
- package/dist/testing/conformance/__divergence_proofs__/executable-proof-helpers.js +33 -0
- package/dist/testing/conformance/__divergence_proofs__/executable-proof-helpers.js.map +1 -0
- package/dist/testing/conformance/adversity.d.ts +36 -0
- package/dist/testing/conformance/adversity.d.ts.map +1 -0
- package/dist/testing/conformance/adversity.js +360 -0
- package/dist/testing/conformance/adversity.js.map +1 -0
- package/dist/testing/conformance/boundary.d.ts +56 -0
- package/dist/testing/conformance/boundary.d.ts.map +1 -0
- package/dist/testing/conformance/boundary.js +129 -0
- package/dist/testing/conformance/boundary.js.map +1 -0
- package/dist/testing/conformance/client/_fixtures.d.ts +71 -0
- package/dist/testing/conformance/client/_fixtures.d.ts.map +1 -0
- package/dist/testing/conformance/client/_fixtures.js +102 -0
- package/dist/testing/conformance/client/_fixtures.js.map +1 -0
- package/dist/testing/conformance/client/adversity.d.ts +39 -0
- package/dist/testing/conformance/client/adversity.d.ts.map +1 -0
- package/dist/testing/conformance/client/adversity.js +162 -0
- package/dist/testing/conformance/client/adversity.js.map +1 -0
- package/dist/testing/conformance/client/boundary.d.ts +12 -0
- package/dist/testing/conformance/client/boundary.d.ts.map +1 -0
- package/dist/testing/conformance/client/boundary.js +68 -0
- package/dist/testing/conformance/client/boundary.js.map +1 -0
- package/dist/testing/conformance/client/delivery.d.ts +38 -0
- package/dist/testing/conformance/client/delivery.d.ts.map +1 -0
- package/dist/testing/conformance/client/delivery.js +202 -0
- package/dist/testing/conformance/client/delivery.js.map +1 -0
- package/dist/testing/conformance/client/index.d.ts +16 -0
- package/dist/testing/conformance/client/index.d.ts.map +1 -0
- package/dist/testing/conformance/client/index.js +16 -0
- package/dist/testing/conformance/client/index.js.map +1 -0
- package/dist/testing/conformance/client/rpc-semantics.d.ts +26 -0
- package/dist/testing/conformance/client/rpc-semantics.d.ts.map +1 -0
- package/dist/testing/conformance/client/rpc-semantics.js +145 -0
- package/dist/testing/conformance/client/rpc-semantics.js.map +1 -0
- package/dist/testing/conformance/client/runner.d.ts +258 -0
- package/dist/testing/conformance/client/runner.d.ts.map +1 -0
- package/dist/testing/conformance/client/runner.js +228 -0
- package/dist/testing/conformance/client/runner.js.map +1 -0
- package/dist/testing/conformance/client/schema-conformance.d.ts +25 -0
- package/dist/testing/conformance/client/schema-conformance.d.ts.map +1 -0
- package/dist/testing/conformance/client/schema-conformance.js +123 -0
- package/dist/testing/conformance/client/schema-conformance.js.map +1 -0
- package/dist/testing/conformance/client/suite.d.ts +90 -0
- package/dist/testing/conformance/client/suite.d.ts.map +1 -0
- package/dist/testing/conformance/client/suite.js +209 -0
- package/dist/testing/conformance/client/suite.js.map +1 -0
- package/dist/testing/conformance/coverage-policy.d.ts +8 -0
- package/dist/testing/conformance/coverage-policy.d.ts.map +1 -0
- package/dist/testing/conformance/coverage-policy.js +10 -0
- package/dist/testing/conformance/coverage-policy.js.map +1 -0
- package/dist/testing/conformance/delivery.d.ts +40 -0
- package/dist/testing/conformance/delivery.d.ts.map +1 -0
- package/dist/testing/conformance/delivery.js +231 -0
- package/dist/testing/conformance/delivery.js.map +1 -0
- package/dist/testing/conformance/env.d.ts +3 -0
- package/dist/testing/conformance/env.d.ts.map +1 -0
- package/dist/testing/conformance/env.js +14 -0
- package/dist/testing/conformance/env.js.map +1 -0
- package/dist/testing/conformance/index.d.ts +10 -0
- package/dist/testing/conformance/index.d.ts.map +1 -0
- package/dist/testing/conformance/index.js +10 -0
- package/dist/testing/conformance/index.js.map +1 -0
- package/dist/testing/conformance/registry.d.ts +93 -0
- package/dist/testing/conformance/registry.d.ts.map +1 -0
- package/dist/testing/conformance/registry.js +62 -0
- package/dist/testing/conformance/registry.js.map +1 -0
- package/dist/testing/conformance/rpc-semantics.d.ts +67 -0
- package/dist/testing/conformance/rpc-semantics.d.ts.map +1 -0
- package/dist/testing/conformance/rpc-semantics.js +394 -0
- package/dist/testing/conformance/rpc-semantics.js.map +1 -0
- package/dist/testing/conformance/runner.d.ts +78 -0
- package/dist/testing/conformance/runner.d.ts.map +1 -0
- package/dist/testing/conformance/runner.js +65 -0
- package/dist/testing/conformance/runner.js.map +1 -0
- package/dist/testing/conformance/schema-conformance.d.ts +30 -0
- package/dist/testing/conformance/schema-conformance.d.ts.map +1 -0
- package/dist/testing/conformance/schema-conformance.js +229 -0
- package/dist/testing/conformance/schema-conformance.js.map +1 -0
- package/dist/testing/conformance/suite.d.ts +92 -0
- package/dist/testing/conformance/suite.d.ts.map +1 -0
- package/dist/testing/conformance/suite.js +233 -0
- package/dist/testing/conformance/suite.js.map +1 -0
- package/dist/testing/errors.d.ts +78 -0
- package/dist/testing/errors.d.ts.map +1 -0
- package/dist/testing/errors.js +34 -0
- package/dist/testing/errors.js.map +1 -0
- package/dist/testing/index.d.ts +25 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +37 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/models/dispatch.d.ts +61 -0
- package/dist/testing/models/dispatch.d.ts.map +1 -0
- package/dist/testing/models/dispatch.js +197 -0
- package/dist/testing/models/dispatch.js.map +1 -0
- package/dist/testing/models/index.d.ts +3 -0
- package/dist/testing/models/index.d.ts.map +1 -0
- package/dist/testing/models/index.js +3 -0
- package/dist/testing/models/index.js.map +1 -0
- package/dist/testing/models/state.d.ts +37 -0
- package/dist/testing/models/state.d.ts.map +1 -0
- package/dist/testing/models/state.js +14 -0
- package/dist/testing/models/state.js.map +1 -0
- package/dist/testing/test-client.d.ts +70 -0
- package/dist/testing/test-client.d.ts.map +1 -0
- package/dist/testing/test-client.js +266 -0
- package/dist/testing/test-client.js.map +1 -0
- package/dist/testing/test-server.d.ts +62 -0
- package/dist/testing/test-server.d.ts.map +1 -0
- package/dist/testing/test-server.js +134 -0
- package/dist/testing/test-server.js.map +1 -0
- package/dist/testing/toxics/client.d.ts +52 -0
- package/dist/testing/toxics/client.d.ts.map +1 -0
- package/dist/testing/toxics/client.js +120 -0
- package/dist/testing/toxics/client.js.map +1 -0
- package/dist/testing/toxics/defaults.d.ts +43 -0
- package/dist/testing/toxics/defaults.d.ts.map +1 -0
- package/dist/testing/toxics/defaults.js +9 -0
- package/dist/testing/toxics/defaults.js.map +1 -0
- package/dist/testing/toxics/index.d.ts +4 -0
- package/dist/testing/toxics/index.d.ts.map +1 -0
- package/dist/testing/toxics/index.js +4 -0
- package/dist/testing/toxics/index.js.map +1 -0
- package/dist/testing/toxics/profile.d.ts +69 -0
- package/dist/testing/toxics/profile.d.ts.map +1 -0
- package/dist/testing/toxics/profile.js +57 -0
- package/dist/testing/toxics/profile.js.map +1 -0
- package/dist/types.d.ts +7 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/dist/validators.d.ts +49 -177
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +77 -48
- package/dist/validators.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +12 -34
- package/dist/optional/contact-events.d.ts +0 -3
- package/dist/optional/contact-events.d.ts.map +0 -1
- package/dist/optional/contact-events.js +0 -5
- package/dist/optional/contact-events.js.map +0 -1
- package/dist/optional/contact-methods.d.ts +0 -5
- package/dist/optional/contact-methods.d.ts.map +0 -1
- package/dist/optional/contact-methods.js +0 -5
- package/dist/optional/contact-methods.js.map +0 -1
- package/dist/phone-hash.d.ts +0 -10
- package/dist/phone-hash.d.ts.map +0 -1
- package/dist/phone-hash.js +0 -17
- package/dist/phone-hash.js.map +0 -1
- package/dist/schema/methods/phone-contacts.d.ts +0 -30
- package/dist/schema/methods/phone-contacts.d.ts.map +0 -1
- package/dist/schema/methods/phone-contacts.js +0 -10
- package/dist/schema/methods/phone-contacts.js.map +0 -1
- package/dist/test-client.d.ts +0 -34
- package/dist/test-client.d.ts.map +0 -1
- package/dist/test-client.js +0 -176
- package/dist/test-client.js.map +0 -1
- package/dist/test-fixtures/phone-hashes.d.ts +0 -18
- package/dist/test-fixtures/phone-hashes.d.ts.map +0 -1
- package/dist/test-fixtures/phone-hashes.js +0 -24
- package/dist/test-fixtures/phone-hashes.js.map +0 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side conformance runner — acquires a TestServer substrate plus the
|
|
3
|
+
* consumer-provided real-client factory under a single Scope, pinned to an
|
|
4
|
+
* FC seed.
|
|
5
|
+
*
|
|
6
|
+
* Parallel to `conformance/runner.ts` (server-side: real server + TestClient).
|
|
7
|
+
* This module's input is `realClient: () => Effect<RealClientHandle, E,
|
|
8
|
+
* Scope>`; scope teardown closes the real client, drains the handshake-noise
|
|
9
|
+
* guard (see `ClientHandshakeWindow` below), and releases the TestServer.
|
|
10
|
+
*
|
|
11
|
+
* Architect O5 decision: the factory returns an `Effect` that owns the real
|
|
12
|
+
* client's lifetime via `Scope`. Consumers that already ship an Effect-native
|
|
13
|
+
* construction path (`packages/client/MoltZapWsClient` via its internal
|
|
14
|
+
* `ManagedRuntime`) wrap it in `Effect.acquireRelease`. Channel packages
|
|
15
|
+
* (`openclaw-channel`, `nanoclaw-channel`) add a narrow test-support subpath
|
|
16
|
+
* export (see §4 O5 resolution in the design doc) that returns the same
|
|
17
|
+
* factory shape.
|
|
18
|
+
*/
|
|
19
|
+
import { Context, Effect, Ref, type Scope } from "effect";
|
|
20
|
+
import type { EventFrame, ResponseFrame } from "../../../schema/frames.js";
|
|
21
|
+
import { type TestServer, type TestServerConnection } from "../../test-server.js";
|
|
22
|
+
import { type ToxiproxyClient } from "../../toxics/client.js";
|
|
23
|
+
import { RealServerAcquireError, TransportIoError, type ToxicControlError } from "../../errors.js";
|
|
24
|
+
import type { ConformanceArtifact } from "../runner.js";
|
|
25
|
+
/**
|
|
26
|
+
* Opaque handle to a live real MoltZap client connected to `TestServer`.
|
|
27
|
+
* The consumer's factory returns this under a `Scope`; scope release runs
|
|
28
|
+
* `close()`.
|
|
29
|
+
*
|
|
30
|
+
* Invariant I9: every field below is a **public** observable surface on
|
|
31
|
+
* the real client — no private reads, no monkey-patching, no log
|
|
32
|
+
* scraping. When a channel package's client is private, the consumer
|
|
33
|
+
* exposes it via a test-support subpath export (O5 resolution).
|
|
34
|
+
*/
|
|
35
|
+
export interface RealClientHandle {
|
|
36
|
+
/**
|
|
37
|
+
* Stable identifier emitted in the connect frame's `agentId` field.
|
|
38
|
+
* Used to correlate TestServer-observed inbound frames to this client.
|
|
39
|
+
*/
|
|
40
|
+
readonly agentId: string;
|
|
41
|
+
/**
|
|
42
|
+
* Fully-connected promise — resolves after the handshake completes and
|
|
43
|
+
* the client is ready to receive events. Property bodies await this
|
|
44
|
+
* before scripting TestServer emissions so the handshake-noise guard
|
|
45
|
+
* window is closed (see `ClientHandshakeWindow`).
|
|
46
|
+
*/
|
|
47
|
+
readonly ready: Effect.Effect<void, RealClientLifecycleError>;
|
|
48
|
+
/**
|
|
49
|
+
* Real client's public event-subscriber surface. Every captured event
|
|
50
|
+
* is tagged with the property-authored `emissionId` when the property
|
|
51
|
+
* uses `ClientHandshakeWindow.emitTaggedEvent`; predicates filter by
|
|
52
|
+
* that tag to exclude handshake-noise frames.
|
|
53
|
+
*/
|
|
54
|
+
readonly events: RealClientEventSubscriber;
|
|
55
|
+
/**
|
|
56
|
+
* Real client's documented RPC caller. B1 / B4 / D5 predicates invoke
|
|
57
|
+
* this and assert on the returned promise's resolution / rejection.
|
|
58
|
+
*/
|
|
59
|
+
readonly call: RealClientRpcCaller;
|
|
60
|
+
/**
|
|
61
|
+
* Real client's documented close / disconnect lifecycle signal. D6
|
|
62
|
+
* predicate awaits this on slow-close and asserts it resolves within
|
|
63
|
+
* the reap deadline.
|
|
64
|
+
*/
|
|
65
|
+
readonly closeSignal: Effect.Effect<RealClientCloseEvent>;
|
|
66
|
+
/**
|
|
67
|
+
* Scope-release hook. The runner's Scope calls this on teardown; a
|
|
68
|
+
* close that throws surfaces as `RealClientLifecycleError`.
|
|
69
|
+
*/
|
|
70
|
+
readonly close: Effect.Effect<void, RealClientLifecycleError>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Real client's public event-subscriber surface. Property bodies `subscribe`
|
|
74
|
+
* once per fixture and drain via `snapshot`. Concrete shape is per-consumer
|
|
75
|
+
* (packages/client's `waitForEvent` + `onEvent`, channel packages' native
|
|
76
|
+
* event pipe); the wrapper adapts it to this interface.
|
|
77
|
+
*/
|
|
78
|
+
export interface RealClientEventSubscriber {
|
|
79
|
+
readonly subscribe: (filter: RealClientEventFilter) => Effect.Effect<RealClientSubscription, RealClientLifecycleError>;
|
|
80
|
+
readonly snapshot: Effect.Effect<ReadonlyArray<ObservedEvent>>;
|
|
81
|
+
}
|
|
82
|
+
export interface RealClientSubscription {
|
|
83
|
+
readonly id: string;
|
|
84
|
+
readonly unsubscribe: Effect.Effect<void>;
|
|
85
|
+
}
|
|
86
|
+
export interface RealClientEventFilter {
|
|
87
|
+
/**
|
|
88
|
+
* Property-authored emission tag. The real client surfaces only
|
|
89
|
+
* events whose payload carries this tag, excluding handshake-noise.
|
|
90
|
+
* Implementations match on the event payload's `__emissionId` field
|
|
91
|
+
* (set by `ClientHandshakeWindow.emitTaggedEvent`).
|
|
92
|
+
*/
|
|
93
|
+
readonly emissionTag?: string;
|
|
94
|
+
/** Restrict to a specific conversation / task. */
|
|
95
|
+
readonly conversationId?: string;
|
|
96
|
+
/** Restrict to a specific event-name family. */
|
|
97
|
+
readonly eventNamePrefix?: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Observed event after the real client has surfaced it on its public
|
|
101
|
+
* subscriber API. `rawBytes` carries the payload byte-for-byte (C3);
|
|
102
|
+
* `decoded` is the schema-decoded frame (A2 validation target).
|
|
103
|
+
*/
|
|
104
|
+
export interface ObservedEvent {
|
|
105
|
+
readonly emissionTag: string | null;
|
|
106
|
+
readonly decoded: EventFrame;
|
|
107
|
+
readonly rawBytes: Uint8Array;
|
|
108
|
+
readonly observedAtMs: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Real client's RPC caller. Takes the raw JSON-RPC method + params;
|
|
112
|
+
* returns the decoded response or a typed error. Contract: the real
|
|
113
|
+
* client itself generates request IDs — the property does not mint
|
|
114
|
+
* them — and records the outbound ID via `outboundIdFeed` so the
|
|
115
|
+
* property can assert ID-set equality (B4, O7 idempotence).
|
|
116
|
+
*/
|
|
117
|
+
export interface RealClientRpcCaller {
|
|
118
|
+
readonly call: (method: string, params: unknown) => Effect.Effect<ResponseFrame, RealClientRpcError>;
|
|
119
|
+
/** Stream of outbound request IDs the real client has minted. */
|
|
120
|
+
readonly outboundIdFeed: Effect.Effect<ReadonlyArray<string>>;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Real-client lifecycle error tag. All three cover the Principle 3 error
|
|
124
|
+
* channel; no raw throws escape the factory's Scope.
|
|
125
|
+
*/
|
|
126
|
+
export declare class RealClientLifecycleError {
|
|
127
|
+
readonly cause: unknown;
|
|
128
|
+
readonly _tag = "RealClientLifecycleError";
|
|
129
|
+
constructor(cause: unknown);
|
|
130
|
+
}
|
|
131
|
+
/** Typed error surface for real-client RPC calls (D5 predicate target). */
|
|
132
|
+
export declare class RealClientRpcError {
|
|
133
|
+
readonly kind: "timeout" | "server-error" | "malformed-response" | "disconnected";
|
|
134
|
+
readonly method: string;
|
|
135
|
+
readonly documentedErrorTag: string | null;
|
|
136
|
+
readonly cause: unknown;
|
|
137
|
+
readonly _tag = "RealClientRpcError";
|
|
138
|
+
constructor(kind: "timeout" | "server-error" | "malformed-response" | "disconnected", method: string, documentedErrorTag: string | null, cause: unknown);
|
|
139
|
+
}
|
|
140
|
+
/** Close-event shape surfaced by `RealClientHandle.closeSignal`. */
|
|
141
|
+
export interface RealClientCloseEvent {
|
|
142
|
+
readonly code: number;
|
|
143
|
+
readonly reason: string;
|
|
144
|
+
readonly observedAtMs: number;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Handshake-noise guard window (O7 resolution).
|
|
148
|
+
*
|
|
149
|
+
* When a real client connects to TestServer, `packages/client` and the
|
|
150
|
+
* channel packages emit hello + subscribe + presence frames **before**
|
|
151
|
+
* the property's first scripted emission. Those frames must not be
|
|
152
|
+
* accepted as satisfying a later sampled predicate.
|
|
153
|
+
*
|
|
154
|
+
* Every client-side property that observes frames requests a
|
|
155
|
+
* `ClientHandshakeWindow` on its fixture and emits via
|
|
156
|
+
* `emitTaggedEvent` / `emitTaggedResponse`. The window stamps each
|
|
157
|
+
* emission with a property-authored `emissionTag`; the
|
|
158
|
+
* `RealClientEventSubscriber` filter drops untagged events.
|
|
159
|
+
*
|
|
160
|
+
* D6 is the only client-side property exempt (observes lifecycle
|
|
161
|
+
* signals, not frames).
|
|
162
|
+
*/
|
|
163
|
+
export interface ClientHandshakeWindow {
|
|
164
|
+
readonly freshEmissionTag: Effect.Effect<string>;
|
|
165
|
+
readonly emitTaggedEvent: (opts: {
|
|
166
|
+
readonly connection: TestServerConnection;
|
|
167
|
+
readonly base: EventFrame;
|
|
168
|
+
readonly emissionTag: string;
|
|
169
|
+
}) => Effect.Effect<string>;
|
|
170
|
+
readonly emitTaggedResponse: (opts: {
|
|
171
|
+
readonly connection: TestServerConnection;
|
|
172
|
+
readonly base: ResponseFrame;
|
|
173
|
+
readonly emissionTag: string;
|
|
174
|
+
}) => Effect.Effect<string>;
|
|
175
|
+
readonly awaitHandshakeComplete: Effect.Effect<void, RealClientLifecycleError>;
|
|
176
|
+
}
|
|
177
|
+
export declare const ClientHandshakeWindow: Context.Tag<ClientHandshakeWindow, ClientHandshakeWindow>;
|
|
178
|
+
/**
|
|
179
|
+
* Context a client-side property receives. Parallel to server-side
|
|
180
|
+
* `ConformanceRunContext` — same `seed`, `toxiproxy`, `artifacts`
|
|
181
|
+
* plumbing; different factory pair.
|
|
182
|
+
*/
|
|
183
|
+
/**
|
|
184
|
+
* Factory arguments the suite passes to every `realClient()` invocation.
|
|
185
|
+
* The factory uses `testServerUrl` to point its WS client at the bound
|
|
186
|
+
* TestServer substrate.
|
|
187
|
+
*/
|
|
188
|
+
export interface RealClientFactoryArgs {
|
|
189
|
+
readonly testServerUrl: string;
|
|
190
|
+
}
|
|
191
|
+
export interface ClientConformanceRunContext {
|
|
192
|
+
readonly testServer: TestServer;
|
|
193
|
+
readonly realClientFactory: (args: RealClientFactoryArgs) => Effect.Effect<RealClientHandle, RealClientLifecycleError, Scope.Scope>;
|
|
194
|
+
readonly handshakeWindow: ClientHandshakeWindow;
|
|
195
|
+
readonly toxiproxy: ToxiproxyClient | null;
|
|
196
|
+
readonly opts: ClientConformanceRunOptions;
|
|
197
|
+
readonly seed: number;
|
|
198
|
+
readonly artifacts: Ref.Ref<ReadonlyArray<ConformanceArtifact>>;
|
|
199
|
+
}
|
|
200
|
+
export interface ClientConformanceRunOptions {
|
|
201
|
+
readonly tiers: ReadonlyArray<"A" | "B" | "C" | "D" | "E">;
|
|
202
|
+
readonly realClient: (args: RealClientFactoryArgs) => Effect.Effect<RealClientHandle, RealClientLifecycleError, Scope.Scope>;
|
|
203
|
+
readonly replaySeed?: number;
|
|
204
|
+
readonly numRuns?: number;
|
|
205
|
+
readonly manageToxiproxy?: boolean;
|
|
206
|
+
readonly toxiproxyUrl?: string;
|
|
207
|
+
readonly artifactDir?: string;
|
|
208
|
+
/**
|
|
209
|
+
* If `true`, TestServer binds behind a Toxiproxy upstream matching the
|
|
210
|
+
* adversity-tier `downstream` port; otherwise a direct bind. Default:
|
|
211
|
+
* `true` when `tiers` includes `"D"`.
|
|
212
|
+
*/
|
|
213
|
+
readonly bindThroughToxiproxy?: boolean;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Acquire the full client-side context under one Scope. Returns a
|
|
217
|
+
* live TestServer, a real-client factory ready to call, and a
|
|
218
|
+
* handshake-noise guard window.
|
|
219
|
+
*
|
|
220
|
+
* The TestServer binds on an ephemeral port. Optional Toxiproxy is
|
|
221
|
+
* acquired when `manageToxiproxy` is set or `toxiproxyUrl` is provided
|
|
222
|
+
* alongside tier "D".
|
|
223
|
+
*
|
|
224
|
+
* Errors are typed; no raw throws.
|
|
225
|
+
*/
|
|
226
|
+
export declare function acquireClientRunContext(opts: ClientConformanceRunOptions): Effect.Effect<ClientConformanceRunContext, ToxicControlError | RealServerAcquireError | RealClientLifecycleError, Scope.Scope>;
|
|
227
|
+
/**
|
|
228
|
+
* Build a `ClientHandshakeWindow` from a real-client handle. Returns a
|
|
229
|
+
* window whose `awaitHandshakeComplete` resolves when `handle.ready`
|
|
230
|
+
* does; emissions are passed through to the connection the property
|
|
231
|
+
* body chooses (TestServer may have multiple connections).
|
|
232
|
+
*/
|
|
233
|
+
export declare function makeClientHandshakeWindow(handle: RealClientHandle): Effect.Effect<ClientHandshakeWindow, never, Scope.Scope>;
|
|
234
|
+
/**
|
|
235
|
+
* Auto-handshake responder. Spawned as a background fiber by property
|
|
236
|
+
* bodies; watches a TestServer connection's inbound capture buffer for
|
|
237
|
+
* `auth/connect` RPC requests and responds with a minimal valid
|
|
238
|
+
* `HelloOkSchema`. Required because `MoltZapWsClient.connect()` blocks
|
|
239
|
+
* on the auth/connect response before `ready` resolves.
|
|
240
|
+
*
|
|
241
|
+
* Exposed as a helper so each property body can choose whether to run
|
|
242
|
+
* the auto-responder or assert directly against the raw inbound stream
|
|
243
|
+
* (e.g., B4 spurious-id test wants to observe the inbound ids).
|
|
244
|
+
*/
|
|
245
|
+
export declare function runAutoHandshakeResponder(connection: TestServerConnection, agentId: string): Effect.Effect<void, never, Scope.Scope>;
|
|
246
|
+
/**
|
|
247
|
+
* Utility: resolve a fresh tag from a handshake window in synchronous-
|
|
248
|
+
* friendly Effect code. Property bodies call this at the top of each
|
|
249
|
+
* fast-check iteration.
|
|
250
|
+
*/
|
|
251
|
+
export declare function freshTag(window: ClientHandshakeWindow): Effect.Effect<string>;
|
|
252
|
+
/**
|
|
253
|
+
* Fiber-safe helper to await a TestServer connection. Times out so a
|
|
254
|
+
* never-connecting real client doesn't block the property body
|
|
255
|
+
* indefinitely.
|
|
256
|
+
*/
|
|
257
|
+
export declare function awaitConnection(testServer: TestServer, timeoutMs?: number): Effect.Effect<TestServerConnection, TransportIoError>;
|
|
258
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../../src/testing/conformance/client/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGxD;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,wBAAwB,CAAC,CAAC;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,yBAAyB,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAC1D;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,wBAAwB,CAAC,CAAC;CAC/D;AAED;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,SAAS,EAAE,CAClB,MAAM,EAAE,qBAAqB,KAC1B,MAAM,CAAC,MAAM,CAAC,sBAAsB,EAAE,wBAAwB,CAAC,CAAC;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,gDAAgD;IAChD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,CACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,KACZ,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC;IACtD,iEAAiE;IACjE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;CAC/D;AAED;;;GAGG;AACH,qBAAa,wBAAwB;IAEvB,QAAQ,CAAC,KAAK,EAAE,OAAO;IADnC,QAAQ,CAAC,IAAI,8BAA8B;gBACtB,KAAK,EAAE,OAAO;CACpC;AAED,2EAA2E;AAC3E,qBAAa,kBAAkB;IAG3B,QAAQ,CAAC,IAAI,EACT,SAAS,GACT,cAAc,GACd,oBAAoB,GACpB,cAAc;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,IAAI;IAC1C,QAAQ,CAAC,KAAK,EAAE,OAAO;IATzB,QAAQ,CAAC,IAAI,wBAAwB;gBAE1B,IAAI,EACT,SAAS,GACT,cAAc,GACd,oBAAoB,GACpB,cAAc,EACT,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,MAAM,GAAG,IAAI,EACjC,KAAK,EAAE,OAAO;CAE1B;AAED,oEAAoE;AACpE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACjD,QAAQ,CAAC,eAAe,EAAE,CAAC,IAAI,EAAE;QAC/B,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAC;QAC1C,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;QAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;KAC9B,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5B,QAAQ,CAAC,kBAAkB,EAAE,CAAC,IAAI,EAAE;QAClC,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAC;QAC1C,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;QAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;KAC9B,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5B,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC,MAAM,CAC5C,IAAI,EACJ,wBAAwB,CACzB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,2DAEjC,CAAC;AAEF;;;;GAIG;AACH;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,iBAAiB,EAAE,CAC1B,IAAI,EAAE,qBAAqB,KACxB,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,wBAAwB,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5E,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC;IAChD,QAAQ,CAAC,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;IAC3D,QAAQ,CAAC,UAAU,EAAE,CACnB,IAAI,EAAE,qBAAqB,KACxB,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,wBAAwB,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CACzC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,2BAA2B,GAChC,MAAM,CAAC,MAAM,CACd,2BAA2B,EAC3B,iBAAiB,GAAG,sBAAsB,GAAG,wBAAwB,EACrE,KAAK,CAAC,KAAK,CACZ,CAgEA;AA0CD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,gBAAgB,GACvB,MAAM,CAAC,MAAM,CAAC,qBAAqB,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAc1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,oBAAoB,EAChC,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CA8CzC;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAE7E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,UAAU,EACtB,SAAS,SAAO,GACf,MAAM,CAAC,MAAM,CAAC,oBAAoB,EAAE,gBAAgB,CAAC,CAavD"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side conformance runner — acquires a TestServer substrate plus the
|
|
3
|
+
* consumer-provided real-client factory under a single Scope, pinned to an
|
|
4
|
+
* FC seed.
|
|
5
|
+
*
|
|
6
|
+
* Parallel to `conformance/runner.ts` (server-side: real server + TestClient).
|
|
7
|
+
* This module's input is `realClient: () => Effect<RealClientHandle, E,
|
|
8
|
+
* Scope>`; scope teardown closes the real client, drains the handshake-noise
|
|
9
|
+
* guard (see `ClientHandshakeWindow` below), and releases the TestServer.
|
|
10
|
+
*
|
|
11
|
+
* Architect O5 decision: the factory returns an `Effect` that owns the real
|
|
12
|
+
* client's lifetime via `Scope`. Consumers that already ship an Effect-native
|
|
13
|
+
* construction path (`packages/client/MoltZapWsClient` via its internal
|
|
14
|
+
* `ManagedRuntime`) wrap it in `Effect.acquireRelease`. Channel packages
|
|
15
|
+
* (`openclaw-channel`, `nanoclaw-channel`) add a narrow test-support subpath
|
|
16
|
+
* export (see §4 O5 resolution in the design doc) that returns the same
|
|
17
|
+
* factory shape.
|
|
18
|
+
*/
|
|
19
|
+
import { Context, Effect, Ref } from "effect";
|
|
20
|
+
import { makeTestServer, } from "../../test-server.js";
|
|
21
|
+
import { makeToxiproxyClient, } from "../../toxics/client.js";
|
|
22
|
+
import { RealServerAcquireError, TransportIoError, } from "../../errors.js";
|
|
23
|
+
import { conformanceNumRunsFromEnv } from "../env.js";
|
|
24
|
+
import { PROTOCOL_VERSION } from "../../../version.js";
|
|
25
|
+
/**
|
|
26
|
+
* Real-client lifecycle error tag. All three cover the Principle 3 error
|
|
27
|
+
* channel; no raw throws escape the factory's Scope.
|
|
28
|
+
*/
|
|
29
|
+
export class RealClientLifecycleError {
|
|
30
|
+
cause;
|
|
31
|
+
_tag = "RealClientLifecycleError";
|
|
32
|
+
constructor(cause) {
|
|
33
|
+
this.cause = cause;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Typed error surface for real-client RPC calls (D5 predicate target). */
|
|
37
|
+
export class RealClientRpcError {
|
|
38
|
+
kind;
|
|
39
|
+
method;
|
|
40
|
+
documentedErrorTag;
|
|
41
|
+
cause;
|
|
42
|
+
_tag = "RealClientRpcError";
|
|
43
|
+
constructor(kind, method, documentedErrorTag, cause) {
|
|
44
|
+
this.kind = kind;
|
|
45
|
+
this.method = method;
|
|
46
|
+
this.documentedErrorTag = documentedErrorTag;
|
|
47
|
+
this.cause = cause;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export const ClientHandshakeWindow = Context.GenericTag("@moltzap/protocol/testing/ClientHandshakeWindow");
|
|
51
|
+
/**
|
|
52
|
+
* Acquire the full client-side context under one Scope. Returns a
|
|
53
|
+
* live TestServer, a real-client factory ready to call, and a
|
|
54
|
+
* handshake-noise guard window.
|
|
55
|
+
*
|
|
56
|
+
* The TestServer binds on an ephemeral port. Optional Toxiproxy is
|
|
57
|
+
* acquired when `manageToxiproxy` is set or `toxiproxyUrl` is provided
|
|
58
|
+
* alongside tier "D".
|
|
59
|
+
*
|
|
60
|
+
* Errors are typed; no raw throws.
|
|
61
|
+
*/
|
|
62
|
+
export function acquireClientRunContext(opts) {
|
|
63
|
+
return Effect.gen(function* () {
|
|
64
|
+
const effectiveOpts = {
|
|
65
|
+
...opts,
|
|
66
|
+
numRuns: opts.numRuns ?? conformanceNumRunsFromEnv(),
|
|
67
|
+
};
|
|
68
|
+
const seed = effectiveOpts.replaySeed ??
|
|
69
|
+
Number(process.env.FC_SEED ?? Date.now() & 0x7fffffff);
|
|
70
|
+
const artifacts = yield* Ref.make([]);
|
|
71
|
+
// Bind the TestServer under the ambient Scope. Server-close on teardown.
|
|
72
|
+
const testServer = yield* makeTestServer({
|
|
73
|
+
port: 0,
|
|
74
|
+
host: "127.0.0.1",
|
|
75
|
+
captureCapacity: 256,
|
|
76
|
+
}).pipe(Effect.mapError((err) => new RealServerAcquireError({
|
|
77
|
+
cause: new Error(`TestServer bind failed: ${String(err)}`),
|
|
78
|
+
})));
|
|
79
|
+
// Optional Toxiproxy acquisition — matches the server-side runner's
|
|
80
|
+
// contract (only allocate when tier "D" is present).
|
|
81
|
+
let toxiproxy = null;
|
|
82
|
+
if (effectiveOpts.tiers.includes("D") &&
|
|
83
|
+
effectiveOpts.toxiproxyUrl !== undefined) {
|
|
84
|
+
const tp = yield* makeToxiproxyClient({
|
|
85
|
+
apiUrl: effectiveOpts.toxiproxyUrl,
|
|
86
|
+
});
|
|
87
|
+
yield* tp.ping.pipe(Effect.orElseSucceed(() => undefined));
|
|
88
|
+
toxiproxy = tp;
|
|
89
|
+
}
|
|
90
|
+
// Build a placeholder handshake window; property bodies overwrite it
|
|
91
|
+
// via `makeClientHandshakeWindow(handle)` once they have a handle.
|
|
92
|
+
// The context carries the initial no-op shape so type-system contracts
|
|
93
|
+
// hold; each property body still binds a per-handle window.
|
|
94
|
+
const handshakeWindow = {
|
|
95
|
+
freshEmissionTag: Effect.sync(() => `tag-${Math.random().toString(36).slice(2, 10)}`),
|
|
96
|
+
emitTaggedEvent: ({ connection, base, emissionTag }) => emitTaggedEventDefault(connection, base, emissionTag),
|
|
97
|
+
emitTaggedResponse: ({ connection, base, emissionTag }) => emitTaggedResponseDefault(connection, base, emissionTag),
|
|
98
|
+
awaitHandshakeComplete: Effect.void,
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
testServer,
|
|
102
|
+
realClientFactory: effectiveOpts.realClient,
|
|
103
|
+
handshakeWindow,
|
|
104
|
+
toxiproxy,
|
|
105
|
+
opts: effectiveOpts,
|
|
106
|
+
seed,
|
|
107
|
+
artifacts,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Default tagged-event emission: stamp the event payload with the
|
|
113
|
+
* caller's `emissionTag` under the reserved `__emissionTag` key, then
|
|
114
|
+
* forward to the connection's real `emitEvent`. Returns the tag so the
|
|
115
|
+
* caller can filter subscriber observations by the same string.
|
|
116
|
+
*
|
|
117
|
+
* `EventFrame.data` is `Type.Optional(Type.Unknown())`; injecting an
|
|
118
|
+
* object field is schema-valid. The real clients under test are
|
|
119
|
+
* payload-opaque (C3 predicate), so the extra field round-trips cleanly.
|
|
120
|
+
*/
|
|
121
|
+
function emitTaggedEventDefault(connection, base, emissionTag) {
|
|
122
|
+
const base_data = (base.data ?? {}); // #ignore-sloppy-code[record-cast]: EventFrame.data is Type.Optional(Type.Unknown()); opaque-payload merge, not a Kysely row
|
|
123
|
+
const tagged = {
|
|
124
|
+
...base,
|
|
125
|
+
data: { ...base_data, __emissionTag: emissionTag },
|
|
126
|
+
};
|
|
127
|
+
return connection.emitEvent(tagged).pipe(Effect.orElseSucceed(() => undefined), Effect.as(emissionTag));
|
|
128
|
+
}
|
|
129
|
+
function emitTaggedResponseDefault(connection, base, _emissionTag) {
|
|
130
|
+
// Response frames don't carry a free-form `data` field; responses are
|
|
131
|
+
// correlated by `id` instead — the response's `id` IS its emission tag
|
|
132
|
+
// from the property's perspective (see B1 / B4 / D5 predicates).
|
|
133
|
+
return connection.emitResponse(base).pipe(Effect.orElseSucceed(() => undefined), Effect.as(base.id));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build a `ClientHandshakeWindow` from a real-client handle. Returns a
|
|
137
|
+
* window whose `awaitHandshakeComplete` resolves when `handle.ready`
|
|
138
|
+
* does; emissions are passed through to the connection the property
|
|
139
|
+
* body chooses (TestServer may have multiple connections).
|
|
140
|
+
*/
|
|
141
|
+
export function makeClientHandshakeWindow(handle) {
|
|
142
|
+
return Effect.gen(function* () {
|
|
143
|
+
const tagCounter = yield* Ref.make(0);
|
|
144
|
+
return {
|
|
145
|
+
freshEmissionTag: Ref.updateAndGet(tagCounter, (n) => n + 1).pipe(Effect.map((n) => `emit-${handle.agentId}-${n}`)),
|
|
146
|
+
emitTaggedEvent: ({ connection, base, emissionTag }) => emitTaggedEventDefault(connection, base, emissionTag),
|
|
147
|
+
emitTaggedResponse: ({ connection, base, emissionTag }) => emitTaggedResponseDefault(connection, base, emissionTag),
|
|
148
|
+
awaitHandshakeComplete: handle.ready,
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Auto-handshake responder. Spawned as a background fiber by property
|
|
154
|
+
* bodies; watches a TestServer connection's inbound capture buffer for
|
|
155
|
+
* `auth/connect` RPC requests and responds with a minimal valid
|
|
156
|
+
* `HelloOkSchema`. Required because `MoltZapWsClient.connect()` blocks
|
|
157
|
+
* on the auth/connect response before `ready` resolves.
|
|
158
|
+
*
|
|
159
|
+
* Exposed as a helper so each property body can choose whether to run
|
|
160
|
+
* the auto-responder or assert directly against the raw inbound stream
|
|
161
|
+
* (e.g., B4 spurious-id test wants to observe the inbound ids).
|
|
162
|
+
*/
|
|
163
|
+
export function runAutoHandshakeResponder(connection, agentId) {
|
|
164
|
+
return Effect.forkScoped(Effect.gen(function* () {
|
|
165
|
+
let handshakeHandled = false;
|
|
166
|
+
while (!handshakeHandled) {
|
|
167
|
+
yield* Effect.sleep("10 millis");
|
|
168
|
+
const snap = yield* connection.inbound.snapshot;
|
|
169
|
+
for (const entry of snap) {
|
|
170
|
+
if (entry.kind === "inbound" &&
|
|
171
|
+
entry.frame !== null &&
|
|
172
|
+
entry.frame.type === "request" &&
|
|
173
|
+
entry.frame.method === "auth/connect") {
|
|
174
|
+
const helloOk = {
|
|
175
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
176
|
+
agentId,
|
|
177
|
+
conversations: [],
|
|
178
|
+
unreadCounts: {},
|
|
179
|
+
policy: {
|
|
180
|
+
maxMessageBytes: 65536,
|
|
181
|
+
maxPartsPerMessage: 32,
|
|
182
|
+
maxTextLength: 4096,
|
|
183
|
+
maxGroupParticipants: 64,
|
|
184
|
+
heartbeatIntervalMs: 30_000,
|
|
185
|
+
rateLimits: {
|
|
186
|
+
messagesPerMinute: 60,
|
|
187
|
+
requestsPerMinute: 300,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
yield* connection
|
|
192
|
+
.emitResponse({
|
|
193
|
+
jsonrpc: "2.0",
|
|
194
|
+
type: "response",
|
|
195
|
+
id: entry.frame.id,
|
|
196
|
+
result: helloOk,
|
|
197
|
+
})
|
|
198
|
+
.pipe(Effect.orElseSucceed(() => undefined));
|
|
199
|
+
handshakeHandled = true;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
})).pipe(Effect.asVoid);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Utility: resolve a fresh tag from a handshake window in synchronous-
|
|
208
|
+
* friendly Effect code. Property bodies call this at the top of each
|
|
209
|
+
* fast-check iteration.
|
|
210
|
+
*/
|
|
211
|
+
export function freshTag(window) {
|
|
212
|
+
return window.freshEmissionTag;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Fiber-safe helper to await a TestServer connection. Times out so a
|
|
216
|
+
* never-connecting real client doesn't block the property body
|
|
217
|
+
* indefinitely.
|
|
218
|
+
*/
|
|
219
|
+
export function awaitConnection(testServer, timeoutMs = 5000) {
|
|
220
|
+
return testServer.accept.pipe(Effect.timeoutFail({
|
|
221
|
+
duration: `${timeoutMs} millis`,
|
|
222
|
+
onTimeout: () => new TransportIoError({
|
|
223
|
+
direction: "inbound",
|
|
224
|
+
cause: new Error(`TestServer accept timeout after ${timeoutMs}ms (no real client connected)`),
|
|
225
|
+
}),
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../src/testing/conformance/client/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ClientConformanceRunContext } from "./runner.js";
|
|
2
|
+
/**
|
|
3
|
+
* A2 client-side — TestServer emits a property-sampled valid
|
|
4
|
+
* `EventFrame` with a property-authored `emissionTag`; real client's
|
|
5
|
+
* subscriber surfaces an event whose payload schema-matches within
|
|
6
|
+
* deadline.
|
|
7
|
+
*
|
|
8
|
+
* Predicate: `Value.Check(EventFrameSchema, observed.decoded)` passes
|
|
9
|
+
* AND `data.__emissionTag === emissionTag`.
|
|
10
|
+
*
|
|
11
|
+
* Discriminates: a client that strips or reorders required schema
|
|
12
|
+
* fields when surfacing events fails.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerEventWellFormednessClient(ctx: ClientConformanceRunContext): void;
|
|
15
|
+
/**
|
|
16
|
+
* A4 client half — TestServer emits a bit-flipped / truncated /
|
|
17
|
+
* oversized frame; real client drops it silently. A subsequent tagged
|
|
18
|
+
* valid event still surfaces (liveness proof, mirrors #187 round-5
|
|
19
|
+
* guard). A client that crashes on the malformed frame disconnects,
|
|
20
|
+
* preventing the liveness probe from surfacing within the deadline.
|
|
21
|
+
*
|
|
22
|
+
* Predicate: liveness — next tagged event surfaces within deadline.
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerMalformedFrameHandlingClient(ctx: ClientConformanceRunContext): void;
|
|
25
|
+
//# sourceMappingURL=schema-conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-conformance.d.ts","sourceRoot":"","sources":["../../../../src/testing/conformance/client/schema-conformance.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAY/D;;;;;;;;;;;GAWG;AACH,wBAAgB,iCAAiC,CAC/C,GAAG,EAAE,2BAA2B,GAC/B,IAAI,CAiEN;AAED;;;;;;;;GAQG;AACH,wBAAgB,oCAAoC,CAClD,GAAG,EAAE,2BAA2B,GAC/B,IAAI,CA0DN"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side schema-conformance properties.
|
|
3
|
+
*
|
|
4
|
+
* Covers spec-amendment #200 §5:
|
|
5
|
+
* A2 — event-well-formedness (client-side new)
|
|
6
|
+
* A4 — malformed-frame-handling (client half of both-sides)
|
|
7
|
+
*
|
|
8
|
+
* Predicate-authoring discipline:
|
|
9
|
+
* - P1 (#195): every predicate names a client-realistic misbehaviour.
|
|
10
|
+
* Here it's "real client surfaces a malformed or dropped event."
|
|
11
|
+
* - P2 (#195): executable divergence proofs live under
|
|
12
|
+
* `__divergence_proofs__/*-executable.proofs.test.ts`.
|
|
13
|
+
* - O7 (#200): every observation filters by property-authored
|
|
14
|
+
* `emissionTag` via `ClientHandshakeWindow.emitTaggedEvent` — auto-
|
|
15
|
+
* subscribe / hello / resume frames never satisfy a predicate.
|
|
16
|
+
* - O6 (#200): when spec names a typed error, assert exact match.
|
|
17
|
+
* A4 client half: `MalformedFrameError` is documented; the adapter
|
|
18
|
+
* exposes no typed error channel, so the predicate checks liveness
|
|
19
|
+
* only — a client that crashes on a malformed frame will disconnect,
|
|
20
|
+
* preventing the subsequent liveness probe from surfacing.
|
|
21
|
+
*/
|
|
22
|
+
import { Effect } from "effect";
|
|
23
|
+
import { Value } from "@sinclair/typebox/value";
|
|
24
|
+
import { EventFrameSchema } from "../../../schema/frames.js";
|
|
25
|
+
import { arbitraryEventFrame } from "../../arbitraries/frames.js";
|
|
26
|
+
import * as fc from "fast-check";
|
|
27
|
+
import { registerProperty } from "../registry.js";
|
|
28
|
+
import { acquireFixture, collectTagged, invariant, subscribeAll, } from "./_fixtures.js";
|
|
29
|
+
const CATEGORY = "schema-conformance";
|
|
30
|
+
const PROPERTY_BUDGET_MS = 8_000;
|
|
31
|
+
/**
|
|
32
|
+
* A2 client-side — TestServer emits a property-sampled valid
|
|
33
|
+
* `EventFrame` with a property-authored `emissionTag`; real client's
|
|
34
|
+
* subscriber surfaces an event whose payload schema-matches within
|
|
35
|
+
* deadline.
|
|
36
|
+
*
|
|
37
|
+
* Predicate: `Value.Check(EventFrameSchema, observed.decoded)` passes
|
|
38
|
+
* AND `data.__emissionTag === emissionTag`.
|
|
39
|
+
*
|
|
40
|
+
* Discriminates: a client that strips or reorders required schema
|
|
41
|
+
* fields when surfacing events fails.
|
|
42
|
+
*/
|
|
43
|
+
export function registerEventWellFormednessClient(ctx) {
|
|
44
|
+
registerProperty(ctx, CATEGORY, "event-well-formedness-client", "valid EventFrame emitted by TestServer surfaces schema-clean on real client", Effect.scoped(Effect.gen(function* () {
|
|
45
|
+
const fx = yield* acquireFixture(ctx, CATEGORY, "event-well-formedness-client");
|
|
46
|
+
yield* subscribeAll(fx.handle);
|
|
47
|
+
const sampled = fc.sample(arbitraryEventFrame(), {
|
|
48
|
+
numRuns: 1,
|
|
49
|
+
seed: ctx.seed,
|
|
50
|
+
})[0];
|
|
51
|
+
if (sampled === undefined) {
|
|
52
|
+
return yield* Effect.fail(invariant(CATEGORY, "event-well-formedness-client", "failed to sample EventFrame"));
|
|
53
|
+
}
|
|
54
|
+
const tag = yield* fx.window.freshEmissionTag;
|
|
55
|
+
yield* fx.window.emitTaggedEvent({
|
|
56
|
+
connection: fx.connection,
|
|
57
|
+
base: sampled,
|
|
58
|
+
emissionTag: tag,
|
|
59
|
+
});
|
|
60
|
+
const observed = yield* collectTagged(fx.handle, (t) => t === tag, {
|
|
61
|
+
expected: 1,
|
|
62
|
+
budgetMs: PROPERTY_BUDGET_MS,
|
|
63
|
+
});
|
|
64
|
+
if (observed.length === 0) {
|
|
65
|
+
return yield* Effect.fail(invariant(CATEGORY, "event-well-formedness-client", `tagged event ${tag} not surfaced within ${PROPERTY_BUDGET_MS}ms`));
|
|
66
|
+
}
|
|
67
|
+
// Reconstruct the expected event shape and re-check schema.
|
|
68
|
+
const reconstructed = {
|
|
69
|
+
jsonrpc: "2.0",
|
|
70
|
+
type: "event",
|
|
71
|
+
event: observed[0].eventName,
|
|
72
|
+
data: observed[0].data,
|
|
73
|
+
};
|
|
74
|
+
if (!Value.Check(EventFrameSchema, reconstructed)) {
|
|
75
|
+
return yield* Effect.fail(invariant(CATEGORY, "event-well-formedness-client", "real client surfaced event that fails EventFrameSchema"));
|
|
76
|
+
}
|
|
77
|
+
})));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A4 client half — TestServer emits a bit-flipped / truncated /
|
|
81
|
+
* oversized frame; real client drops it silently. A subsequent tagged
|
|
82
|
+
* valid event still surfaces (liveness proof, mirrors #187 round-5
|
|
83
|
+
* guard). A client that crashes on the malformed frame disconnects,
|
|
84
|
+
* preventing the liveness probe from surfacing within the deadline.
|
|
85
|
+
*
|
|
86
|
+
* Predicate: liveness — next tagged event surfaces within deadline.
|
|
87
|
+
*/
|
|
88
|
+
export function registerMalformedFrameHandlingClient(ctx) {
|
|
89
|
+
registerProperty(ctx, CATEGORY, "malformed-frame-handling-client", "malformed TestServer emission absorbed silently; liveness intact", Effect.scoped(Effect.gen(function* () {
|
|
90
|
+
const fx = yield* acquireFixture(ctx, CATEGORY, "malformed-frame-handling-client");
|
|
91
|
+
yield* subscribeAll(fx.handle);
|
|
92
|
+
const baseEvent = fc.sample(arbitraryEventFrame(), {
|
|
93
|
+
numRuns: 1,
|
|
94
|
+
seed: ctx.seed,
|
|
95
|
+
})[0];
|
|
96
|
+
if (baseEvent === undefined) {
|
|
97
|
+
return yield* Effect.fail(invariant(CATEGORY, "malformed-frame-handling-client", "failed to sample base EventFrame"));
|
|
98
|
+
}
|
|
99
|
+
// Emit a malformed frame — the real client must absorb it.
|
|
100
|
+
yield* fx.connection
|
|
101
|
+
.emitMalformed({
|
|
102
|
+
baseEvent,
|
|
103
|
+
kind: "bit-flip",
|
|
104
|
+
seed: ctx.seed,
|
|
105
|
+
})
|
|
106
|
+
.pipe(Effect.orElseSucceed(() => undefined));
|
|
107
|
+
// Liveness probe: emit a valid tagged event after the malformed one.
|
|
108
|
+
const tag = yield* fx.window.freshEmissionTag;
|
|
109
|
+
yield* fx.window.emitTaggedEvent({
|
|
110
|
+
connection: fx.connection,
|
|
111
|
+
base: baseEvent,
|
|
112
|
+
emissionTag: tag,
|
|
113
|
+
});
|
|
114
|
+
const observed = yield* collectTagged(fx.handle, (t) => t === tag, {
|
|
115
|
+
expected: 1,
|
|
116
|
+
budgetMs: PROPERTY_BUDGET_MS,
|
|
117
|
+
});
|
|
118
|
+
if (observed.length === 0) {
|
|
119
|
+
return yield* Effect.fail(invariant(CATEGORY, "malformed-frame-handling-client", "liveness failed: no tagged event after malformed emission"));
|
|
120
|
+
}
|
|
121
|
+
})));
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=schema-conformance.js.map
|