@yolk-sdk/conformance 0.1.0-canary.96
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 +21 -0
- package/README.md +316 -0
- package/dist/case.d.mts +89 -0
- package/dist/case.d.mts.map +1 -0
- package/dist/case.mjs +101 -0
- package/dist/case.mjs.map +1 -0
- package/dist/fixture.d.mts +432 -0
- package/dist/fixture.d.mts.map +1 -0
- package/dist/fixture.mjs +391 -0
- package/dist/fixture.mjs.map +1 -0
- package/dist/record.d.mts +96 -0
- package/dist/record.d.mts.map +1 -0
- package/dist/record.mjs +212 -0
- package/dist/record.mjs.map +1 -0
- package/dist/replay.d.mts +316 -0
- package/dist/replay.d.mts.map +1 -0
- package/dist/replay.mjs +272 -0
- package/dist/replay.mjs.map +1 -0
- package/dist/runner.d.mts +163 -0
- package/dist/runner.d.mts.map +1 -0
- package/dist/runner.mjs +264 -0
- package/dist/runner.mjs.map +1 -0
- package/dist/wire-internal.d.mts +90 -0
- package/dist/wire-internal.d.mts.map +1 -0
- package/dist/wire-internal.mjs +212 -0
- package/dist/wire-internal.mjs.map +1 -0
- package/package.json +75 -0
- package/src/case.ts +153 -0
- package/src/fixture.ts +625 -0
- package/src/record.ts +377 -0
- package/src/replay.ts +617 -0
- package/src/runner.ts +559 -0
- package/src/wire-internal.ts +382 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yolk SDK contributors
|
|
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,316 @@
|
|
|
1
|
+
# @yolk-sdk/conformance
|
|
2
|
+
|
|
3
|
+
> **EXPERIMENTAL.** This package is new and its API may change in any canary release, beyond the
|
|
4
|
+
> usual canary instability.
|
|
5
|
+
|
|
6
|
+
A small Effect-only toolkit for recording real HTTP exchanges with outside services, replaying them
|
|
7
|
+
in tests (offline, fail closed), and injecting wire faults such as mid-stream drops, truncation,
|
|
8
|
+
and rate-limit responses. It also defines conformance cases (small Effect programs that each prove
|
|
9
|
+
one claim about how a service really behaves on the wire) and a runner that decides which cases may
|
|
10
|
+
run where and reports the results.
|
|
11
|
+
|
|
12
|
+
Canary APIs are unstable. Keep all `@yolk-sdk/*` packages on the same version.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add -D @yolk-sdk/conformance@canary effect@4.0.0-rc.115
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Subpaths
|
|
21
|
+
|
|
22
|
+
There is no root export. Import an explicit subpath:
|
|
23
|
+
|
|
24
|
+
| Subpath | Purpose |
|
|
25
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
26
|
+
| `@yolk-sdk/conformance/fixture` | `WireFixture` / `PortFixture` schemas and types, decoders, staleness helpers, secret scans, `redactPortPayload`, `syntheticPortCredentialParams` |
|
|
27
|
+
| `@yolk-sdk/conformance/replay` | `ReplayHttpClient.layer`, `makeReplayHttpClient`, `ReplayLedger`, `WireFault` |
|
|
28
|
+
| `@yolk-sdk/conformance/record` | `WireRecorder.layer`, `makeRecordingHttpClient`, `makeWireFixture` |
|
|
29
|
+
| `@yolk-sdk/conformance/case` | `defineConformanceCase`, `ConformanceCase`, `ConformanceSafety`, `expectConformance`, `expectEqual`, `ConformanceMismatch` |
|
|
30
|
+
| `@yolk-sdk/conformance/runner` | `runConformance`, `ConformanceTarget`, `ConformanceReport`, `conformanceSkipReason`, `formatConformanceReport` |
|
|
31
|
+
|
|
32
|
+
## Fixtures
|
|
33
|
+
|
|
34
|
+
A `WireFixture` is plain data: an id, a `caseId`, `evidence` (`verified` for a live recording,
|
|
35
|
+
`unverified` for a synthetic placeholder), a `recordedAt` date (`YYYY-MM-DD`), a synthetic
|
|
36
|
+
`account` label, the `endpoint`, optional `model` / `note`, and a non-empty list of exchanges.
|
|
37
|
+
Each exchange has a request (method, absolute URL, allowlisted headers, JSON or text body) and a
|
|
38
|
+
response recorded losslessly in exactly one of three shapes:
|
|
39
|
+
|
|
40
|
+
- `body`: the whole body as a string, when it is valid UTF-8.
|
|
41
|
+
- `bodyBase64`: the whole body's exact bytes in base64, for anything else (for example a PDF).
|
|
42
|
+
- `chunks`: streamed network chunks with their original boundaries. Each entry is a string when
|
|
43
|
+
that chunk is valid UTF-8 on its own (empty chunks are `""`), or `{ base64 }` holding its exact
|
|
44
|
+
bytes (for example half of a multi-byte character). Plain strings stay the common, readable case.
|
|
45
|
+
|
|
46
|
+
**Fixtures must contain synthetic or scrubbed data only.** Never commit credentials, cookies,
|
|
47
|
+
customer names, or real account identifiers. Run `scanFixtureForSecrets` (or build fixtures with
|
|
48
|
+
`makeWireFixture`, which fails on any finding) before committing. The scan flags credential
|
|
49
|
+
headers (including `x-*-token` / `x-*-key` style names), bearer tokens, common API-key prefixes,
|
|
50
|
+
JWTs, private keys, credential query and form parameters, and non-empty string credential JSON
|
|
51
|
+
fields (`access_token`, `client_secret`, `password`, `api_key`, `token`, ...; numeric usage
|
|
52
|
+
counters such as `max_tokens` are not flagged). It covers metadata strings, URLs, headers, request
|
|
53
|
+
bodies, response bodies (including decodable `bodyBase64` text), each chunk, and the reassembled
|
|
54
|
+
stream, so a secret split across chunks is still found; JSON bodies and SSE `data:` payloads get
|
|
55
|
+
the credential-field scan. SSE framing with CRLF, LF, or bare CR line endings is recognized. It
|
|
56
|
+
reports locations, never the secret itself. The scan errs toward flagging: a field named exactly
|
|
57
|
+
`token` (for example an OpenAI `logprobs` entry) is flagged, so avoid recording logprobs. The scan
|
|
58
|
+
is a safety net, not a guarantee: review fixtures before publishing them.
|
|
59
|
+
|
|
60
|
+
`fixtureAgeDays(fixture, now)` and `isFixtureStale(fixture, now, maxAgeDays = 30)` help hosts
|
|
61
|
+
decide when a recording should be refreshed.
|
|
62
|
+
|
|
63
|
+
### Port fixtures
|
|
64
|
+
|
|
65
|
+
Some outside services are reached through a host-provided port rather than HTTP (for example the
|
|
66
|
+
generic `EmailClient` port of `@yolk-sdk/connectors/email`, where the host speaks IMAP and SMTP).
|
|
67
|
+
A `PortFixture` records one such call as plain JSON:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import type { PortFixture } from '@yolk-sdk/conformance/fixture'
|
|
71
|
+
|
|
72
|
+
const listed: PortFixture = {
|
|
73
|
+
id: 'example.list.synthetic',
|
|
74
|
+
port: 'ExampleClient',
|
|
75
|
+
method: 'listItems',
|
|
76
|
+
request: { folder: 'INBOX', limit: 50 },
|
|
77
|
+
response: { items: [] }
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
It carries exactly one of `response` (any JSON value) or `failure` (`{ kind: 'expected' | 'error',
|
|
82
|
+
code, message, status? }`: `expected` is the port's value-level failure, `error` its typed error
|
|
83
|
+
channel). Without `observed` it is a synthetic placeholder (`unverified`); `observed: { account,
|
|
84
|
+
date }` records the live observation and makes it `verified` as of that date
|
|
85
|
+
(`conformanceFixtureEvidence`). Record requests through `redactPortPayload`, which drops
|
|
86
|
+
`credential(s)` and every credential field name at any depth (AWS-style `accessKeyId`,
|
|
87
|
+
`secretAccessKey`, and `sessionToken`, camelCase or snake_case, included), and run
|
|
88
|
+
`scanPortFixtureForSecrets` before committing: it applies the same token patterns and
|
|
89
|
+
credential-field scan as `scanFixtureForSecrets`, flags any non-null `credential(s)` field, and
|
|
90
|
+
flags credential query or form parameters inside every JSON string value, the note, and the failure
|
|
91
|
+
message (a signed URL such as an S3 presigned URL). Every `name=` is found on its own, so no later
|
|
92
|
+
parameter is skipped. The port scan's only exemption: the raw value, up to a structural boundary
|
|
93
|
+
(`&`, `#`, whitespace, `"`, `<`, `>`, or the end; a raw `?` or `'` and encoded delimiters such as
|
|
94
|
+
`%3F`, `%26`, `%23`, `%20` stay inside), percent-decoded, exactly equals that parameter's entry in
|
|
95
|
+
the frozen `syntheticPortCredentialParams` (the SigV4 `x-amz-signature` and `x-amz-credential`
|
|
96
|
+
placeholders). Anything else inside the value, another parameter name, or another scope is flagged;
|
|
97
|
+
`scanFixtureForSecrets` exempts nothing. A placeholder written by hand in prose and followed by `.`,
|
|
98
|
+
`,`, `)`, or `'` (a URL quoted in single quotes) is flagged too (fail-closed). Parameters with
|
|
99
|
+
escaped names (`&` before them, percent-encoded names) are not found: port owners that can meet
|
|
100
|
+
them check them themselves. Replaying port fixtures is up to the port's owner (the email bridge
|
|
101
|
+
ships `makeEmailReplayBackend`); the runner accepts `PortFixture`s next to `WireFixture`s for its
|
|
102
|
+
fixture warnings.
|
|
103
|
+
|
|
104
|
+
## Replay
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { Effect } from 'effect'
|
|
108
|
+
import { HttpClient } from 'effect/unstable/http'
|
|
109
|
+
import { ReplayHttpClient, ReplayLedger, WireFault } from '@yolk-sdk/conformance/replay'
|
|
110
|
+
|
|
111
|
+
const program = Effect.gen(function* () {
|
|
112
|
+
const client = yield* HttpClient.HttpClient
|
|
113
|
+
// ... run the code under test against `client` ...
|
|
114
|
+
const entries = yield* (yield* ReplayLedger).entries
|
|
115
|
+
return entries
|
|
116
|
+
}).pipe(
|
|
117
|
+
Effect.provide(
|
|
118
|
+
ReplayHttpClient.layer([myFixture], {
|
|
119
|
+
faults: [WireFault.StatusOnAttempt({ attempt: 1, status: 500 })]
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- Requests match by method and normalized absolute URL (hash removed, query parameters sorted).
|
|
126
|
+
- Each recorded exchange is consumed once, in recorded order among exchanges with the same method
|
|
127
|
+
and URL, so create → update → read-back flows and repeated requests replay deterministically.
|
|
128
|
+
- A request with no remaining match fails closed with an `HttpClientError` (`TransportError`)
|
|
129
|
+
whose message names the method and URL, never headers or body. It is still written to the ledger
|
|
130
|
+
as `unmatched`. Faults never answer such requests.
|
|
131
|
+
- Replay emits exactly the recorded bytes: one `Uint8Array` per recorded chunk (including empty
|
|
132
|
+
ones), each produced only when the consumer pulls it; `bodyBase64` replays as the original bytes.
|
|
133
|
+
- `ReplayLedger.entries` records every request: method, URL, headers with credentials redacted,
|
|
134
|
+
body text and parsed JSON, attempt number per method + URL, how it was answered (`matched`,
|
|
135
|
+
`injected`, `unmatched`, or `invalid`), and the tag of any fault that applied.
|
|
136
|
+
`ReplayLedger.remaining` lists recorded exchanges not consumed yet.
|
|
137
|
+
|
|
138
|
+
Faults (`WireFault.*`) accept an optional `match: { method?, url? }`; `url` matches exactly after
|
|
139
|
+
normalization, or as a prefix when it ends with `*`:
|
|
140
|
+
|
|
141
|
+
| Fault | Effect |
|
|
142
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
|
+
| `StatusOnAttempt` | Answer attempt N with a status/headers/body instead of consuming the recording (500-then-success, 429 + retry). Fires only when an unconsumed recorded exchange exists for the method + URL. |
|
|
144
|
+
| `FailAfterChunks` | Emit N chunks, then fail the body stream like a dropped connection under `FetchHttpClient`. |
|
|
145
|
+
| `TruncateAfterChunks` | Emit N chunks, then end the body cleanly (for example without a final event). |
|
|
146
|
+
| `HoldAfterChunks` | Emit N chunks, run `release`, then continue (observe progressive delivery). |
|
|
147
|
+
|
|
148
|
+
Chunk faults take an optional 1-based `attempt`. A chunk fault that cannot take effect fails the
|
|
149
|
+
request with an `HttpClientError` whose cause is `WireReplayInvalid` (ledger outcome `invalid`, no
|
|
150
|
+
fault tag, exchange left unconsumed) instead of silently doing nothing: any chunk fault matched
|
|
151
|
+
against a whole-body response, `TruncateAfterChunks` / `HoldAfterChunks` with `chunks` at or beyond
|
|
152
|
+
the recorded chunk count, or `FailAfterChunks` with `chunks` beyond it. The error reason is a
|
|
153
|
+
`TransportError`, which generic transient-retry policies retry; when a test runs through a retrying
|
|
154
|
+
client, also assert the ledger has no `invalid` outcome so a misconfigured fault cannot be retried
|
|
155
|
+
into a silent success.
|
|
156
|
+
|
|
157
|
+
## Record
|
|
158
|
+
|
|
159
|
+
This package **never performs network I/O itself**. For live recording, the host supplies the real
|
|
160
|
+
`HttpClient` and the recorder wraps it:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { Effect, Layer } from 'effect'
|
|
164
|
+
import { FetchHttpClient } from 'effect/unstable/http'
|
|
165
|
+
import { makeWireFixture, WireRecorder } from '@yolk-sdk/conformance/record'
|
|
166
|
+
|
|
167
|
+
const recording = WireRecorder.layer().pipe(Layer.provide(FetchHttpClient.layer))
|
|
168
|
+
|
|
169
|
+
const fixture = Effect.gen(function* () {
|
|
170
|
+
// ... run the real client code once ...
|
|
171
|
+
const exchanges = yield* (yield* WireRecorder).drain
|
|
172
|
+
return yield* makeWireFixture({
|
|
173
|
+
id: 'example.case',
|
|
174
|
+
caseId: 'example.case',
|
|
175
|
+
evidence: 'verified',
|
|
176
|
+
recordedAt: '2026-01-01',
|
|
177
|
+
account: 'synthetic',
|
|
178
|
+
endpoint: 'https://api.example.test/v1/chat',
|
|
179
|
+
exchanges
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The recorder keeps only allowlisted headers (request: `content-type`, `accept`; response:
|
|
185
|
+
`content-type`, `retry-after`, `retry-after-ms`, and rate-limit headers). Credential headers such as
|
|
186
|
+
`authorization`, `cookie`, `set-cookie`, and token/key-bearing names such as `x-auth-token`,
|
|
187
|
+
`private-token`, or `x-*-key` are always dropped. `text/event-stream` responses are teed chunk by
|
|
188
|
+
chunk without changing what the caller receives; each network chunk is recorded standalone (text
|
|
189
|
+
when valid UTF-8, otherwise `{ base64 }`). Other bodies are recorded as `body` or `bodyBase64`.
|
|
190
|
+
`drain` fails with `WireRecordingIncomplete` if a request failed or a body was not read to the end.
|
|
191
|
+
A request still pending at `drain` time is reported by that drain and then dropped: if it finishes
|
|
192
|
+
later, it never appears in, or overwrites an entry of, a later drain.
|
|
193
|
+
|
|
194
|
+
Only record against accounts and data you are allowed to publish, and keep live recording out of CI.
|
|
195
|
+
|
|
196
|
+
## Cases
|
|
197
|
+
|
|
198
|
+
A conformance case is a small, named, pure Effect program that proves one claim about how an outside
|
|
199
|
+
service really behaves on the wire, next to what its docs say. It states only what it needs (its
|
|
200
|
+
`R`), so the same case runs unchanged against replayed fixtures, an in-process emulator, a local
|
|
201
|
+
emulator process, or a real practice account: only the layers change.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { Effect } from 'effect'
|
|
205
|
+
import { HttpClient, HttpClientRequest } from 'effect/unstable/http'
|
|
206
|
+
import { defineConformanceCase, expectEqual } from '@yolk-sdk/conformance/case'
|
|
207
|
+
|
|
208
|
+
export const missingItemCase = defineConformanceCase({
|
|
209
|
+
id: 'example.items.missing',
|
|
210
|
+
safety: 'read',
|
|
211
|
+
docs: 'The docs say a missing item returns 404.',
|
|
212
|
+
wire: 'A missing item returns 404 with an empty JSON object.',
|
|
213
|
+
fixtures: ['example.items.missing.synthetic'],
|
|
214
|
+
run: Effect.gen(function* () {
|
|
215
|
+
const client = yield* HttpClient.HttpClient
|
|
216
|
+
const response = yield* client.execute(
|
|
217
|
+
HttpClientRequest.get('https://api.example.test/items/0')
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
yield* expectEqual(response.status, 404, 'expected 404 for a missing item')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- `id` is dotted lower-case (`a-z`, `0-9`, inner hyphens; two or more segments).
|
|
226
|
+
- `safety` is `read` (only reads), `write-reversible` (writes but leaves the account as it found
|
|
227
|
+
it: it creates its own records and cleans up, or the write is rejected and changes nothing), or
|
|
228
|
+
`write-irreversible` (for example sending an email; never automated against a live account).
|
|
229
|
+
- `docs` is what the documentation claims; `wire` is what the wire actually does.
|
|
230
|
+
- `observed` (`{ account, date }`, a synthetic account label and `YYYY-MM-DD`) records the last time
|
|
231
|
+
a person watched the claim hold against the real service. Absent means unverified.
|
|
232
|
+
- `fixtures` lists the `WireFixture` ids that back replay of the case.
|
|
233
|
+
- `defineConformanceCase` validates this metadata and **throws** `ConformanceCaseInvalid` for an
|
|
234
|
+
invalid definition. Cases are module-level constants, so a bad one fails when its module loads
|
|
235
|
+
rather than mid-run.
|
|
236
|
+
|
|
237
|
+
`expectConformance(condition, message, details?)` and `expectEqual(actual, expected, message)`
|
|
238
|
+
fail with a `ConformanceMismatch` (`message`, optional JSON `expected` / `actual`). `expectEqual`
|
|
239
|
+
compares JSON values structurally (effect `Equal.equals`: array order matters, object key order
|
|
240
|
+
does not). Cases may also fail with their own errors or with the errors of the ports they use.
|
|
241
|
+
|
|
242
|
+
## Runner
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { Effect } from 'effect'
|
|
246
|
+
import { ReplayHttpClient } from '@yolk-sdk/conformance/replay'
|
|
247
|
+
import { formatConformanceReport, runConformance } from '@yolk-sdk/conformance/runner'
|
|
248
|
+
|
|
249
|
+
const text = await Effect.runPromise(
|
|
250
|
+
runConformance([missingItemCase], {
|
|
251
|
+
target: { kind: 'replay' },
|
|
252
|
+
fixtures: hostFixtures,
|
|
253
|
+
layer: testCase =>
|
|
254
|
+
ReplayHttpClient.layer(hostFixtures.filter(fixture => testCase.fixtures.includes(fixture.id)))
|
|
255
|
+
}).pipe(Effect.map(formatConformanceReport))
|
|
256
|
+
)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`hostFixtures` is a placeholder for your own fixtures.
|
|
260
|
+
|
|
261
|
+
Safety policy (`conformanceSkipReason`):
|
|
262
|
+
|
|
263
|
+
| Target | `read` | `write-reversible` | `write-irreversible` |
|
|
264
|
+
| ---------------------------------- | ------ | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
265
|
+
| `replay`, `in-process`, `emulated` | runs | runs | runs |
|
|
266
|
+
| `live` (`account` required) | runs | runs only with `allowWrites: 'reversible'`, else `writes-not-allowed` | runs only if its exact id is in `allowIrreversible`, else `manual-only` |
|
|
267
|
+
|
|
268
|
+
`allowWrites` defaults to `'none'`. `allowIrreversible` lists the exact case ids a person
|
|
269
|
+
explicitly started and applies regardless of `allowWrites`. Nothing real happens on the other
|
|
270
|
+
targets, so every case runs there.
|
|
271
|
+
|
|
272
|
+
- `layer(testCase)` is called once per case that runs (never for skipped cases) and must provide
|
|
273
|
+
what that case needs. It is called inside the case's failure boundary and built with a fresh
|
|
274
|
+
memo map in its own scope, so state the layer allocates when it is built (replay consumption,
|
|
275
|
+
ledgers, emulator state) never leaks between cases, even if the factory returns the same `Layer`
|
|
276
|
+
value. Isolation therefore needs layers that allocate mutable state when built: a service
|
|
277
|
+
captured in a shared value (one `Layer.succeed(service, instance)` reused across cases) or
|
|
278
|
+
provided by the caller's environment is not rebuilt and stays shared. Whatever the layers still
|
|
279
|
+
need (for example a host's live `HttpClient`) becomes the requirement of the run.
|
|
280
|
+
- Cases with different error and requirement types can share a run; the case type is inferred as
|
|
281
|
+
their union.
|
|
282
|
+
- Each case runs under `Effect.exit`: typed failures, layer build failures, a throwing `layer`
|
|
283
|
+
factory, and defects become `failed` results and the run continues. Interruption is not captured:
|
|
284
|
+
interrupting the run, or a case interrupting itself, interrupts the whole run.
|
|
285
|
+
- `concurrency` defaults to 1 (live accounts are shared); results keep case order.
|
|
286
|
+
- `now` defaults to the Effect `Clock`; `maxFixtureAgeDays` defaults to 30.
|
|
287
|
+
|
|
288
|
+
A `ConformanceReport` has the `target` (kind, plus `account` for live), `startedAt` (ISO), one
|
|
289
|
+
result per case (`id`, `safety`, `status` `passed` / `failed` / `skipped`, `skipReason`, `failure`,
|
|
290
|
+
`durationMs`, `warnings`), and a `summary` count. `failure` is `{ kind, tag?, message }` with `kind`
|
|
291
|
+
`failure` or `defect`. `tag` is the error's `_tag` only when it is identifier-like
|
|
292
|
+
(`^[A-Za-z][A-Za-z0-9_]*$`) and not credential-shaped. A `ConformanceMismatch` keeps its
|
|
293
|
+
case-authored message with credential patterns redacted; every other failure, layer failure, and
|
|
294
|
+
defect message goes through `sanitizeConformanceMessage`, a best-effort sanitizer that redacts the
|
|
295
|
+
credential patterns shared with the fixture secret scan (bearer tokens, API-key prefixes, JWTs,
|
|
296
|
+
credential query/form parameters and field pairs, and credential header lines such as `Cookie:`,
|
|
297
|
+
`X-Api-Key:`, or `Proxy-Authorization:` to the end of the line), replaces JSON-looking spans
|
|
298
|
+
(balanced `{...}` / `[...]`) with `[json]`, collapses whitespace, and caps the length at 300
|
|
299
|
+
characters. Request bodies, headers, and mismatch `expected` / `actual` details are never copied
|
|
300
|
+
into the report; still keep secrets out of error messages.
|
|
301
|
+
|
|
302
|
+
Warnings are non-fatal and listed per case:
|
|
303
|
+
|
|
304
|
+
| Warning | When |
|
|
305
|
+
| -------------------- | ------------------------------------------------------------------------------------ |
|
|
306
|
+
| `unverified-case` | The case has no `observed` |
|
|
307
|
+
| `stale-observation` | `observed.date` is older than `maxFixtureAgeDays` (`ageDays`) |
|
|
308
|
+
| `unverified-fixture` | A referenced fixture has `evidence: 'unverified'` |
|
|
309
|
+
| `stale-fixture` | A referenced fixture is older than `maxFixtureAgeDays` (`ageDays`) |
|
|
310
|
+
| `missing-fixture` | A referenced fixture id is not in `fixtures` (only checked when `fixtures` is given) |
|
|
311
|
+
|
|
312
|
+
Live targets omit all fixture-level warnings (`unverified-fixture`, `stale-fixture`, and
|
|
313
|
+
`missing-fixture`) because fixtures are not used live; elsewhere fixture warnings need `fixtures`.
|
|
314
|
+
Case-level warnings always apply. `formatConformanceReport` prints one plain-text line per
|
|
315
|
+
case (status, id, safety, skip reason or failure, warnings) and a summary line, without colors.
|
|
316
|
+
`conformanceReportFailed` is true when any case failed; skipped cases never fail a report.
|
package/dist/case.d.mts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
|
|
4
|
+
//#region src/case.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What a case does to the account it runs against:
|
|
7
|
+
*
|
|
8
|
+
* - `read`: only reads.
|
|
9
|
+
* - `write-reversible`: writes but leaves the account as it found it (it
|
|
10
|
+
* creates its own records and cleans them up, or the write is rejected and
|
|
11
|
+
* changes nothing).
|
|
12
|
+
* - `write-irreversible`: a write that cannot be undone (for example sending
|
|
13
|
+
* an email). Never automated against a live account.
|
|
14
|
+
*/
|
|
15
|
+
declare const ConformanceSafety: Schema.Literals<readonly ["read", "write-reversible", "write-irreversible"]>;
|
|
16
|
+
type ConformanceSafety = typeof ConformanceSafety.Type;
|
|
17
|
+
/**
|
|
18
|
+
* Dotted lower-case case id: two or more segments of `a-z`, `0-9`, and inner
|
|
19
|
+
* hyphens, for example `vendor.stream.plain-text`.
|
|
20
|
+
*/
|
|
21
|
+
declare const ConformanceCaseId: Schema.String;
|
|
22
|
+
/** When and where a person last watched the claim hold against the real service. */
|
|
23
|
+
declare const ConformanceObservation: Schema.Struct<{
|
|
24
|
+
/** Synthetic account label, for example `synthetic`; never a real account name. */readonly account: Schema.NonEmptyString; /** Calendar date of the observation (`YYYY-MM-DD`, UTC). */
|
|
25
|
+
readonly date: Schema.String;
|
|
26
|
+
}>;
|
|
27
|
+
type ConformanceObservation = typeof ConformanceObservation.Type;
|
|
28
|
+
/**
|
|
29
|
+
* One wire claim. `run` succeeds when the claim holds and fails (with a
|
|
30
|
+
* `ConformanceMismatch`, its own error, or a port's error) when it does not.
|
|
31
|
+
*/
|
|
32
|
+
type ConformanceCase<E = never, R = never> = {
|
|
33
|
+
/** Dotted lower-case id (see `ConformanceCaseId`). */readonly id: string;
|
|
34
|
+
readonly title?: string;
|
|
35
|
+
readonly safety: ConformanceSafety; /** What the service's documentation claims. */
|
|
36
|
+
readonly docs: string; /** What the wire actually does (the claim this case proves). */
|
|
37
|
+
readonly wire: string; /** Last live observation. Absent means the claim is unverified against the real service. */
|
|
38
|
+
readonly observed?: ConformanceObservation; /** Ids of the fixtures (`WireFixture`s or `PortFixture`s) that back replay of this case. */
|
|
39
|
+
readonly fixtures: ReadonlyArray<string>;
|
|
40
|
+
readonly run: Effect.Effect<void, E, R>;
|
|
41
|
+
};
|
|
42
|
+
declare const ConformanceCaseInvalid_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
43
|
+
readonly _tag: "ConformanceCaseInvalid";
|
|
44
|
+
} & Readonly<A>;
|
|
45
|
+
/**
|
|
46
|
+
* Thrown by `defineConformanceCase` for an invalid definition. A programmer
|
|
47
|
+
* error, surfaced when the defining module loads (like `makeTool`).
|
|
48
|
+
*/
|
|
49
|
+
declare class ConformanceCaseInvalid extends ConformanceCaseInvalid_base<{
|
|
50
|
+
readonly caseId: string;
|
|
51
|
+
readonly reason: string;
|
|
52
|
+
}> {
|
|
53
|
+
get message(): string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Define a conformance case. The metadata (id format, safety, non-empty
|
|
57
|
+
* `docs`/`wire`, observation date, fixture ids) is validated here and an
|
|
58
|
+
* invalid definition throws `ConformanceCaseInvalid`: definitions are
|
|
59
|
+
* module-level constants, so a bad one fails fast when the module loads
|
|
60
|
+
* instead of surfacing mid-run.
|
|
61
|
+
*/
|
|
62
|
+
declare const defineConformanceCase: <E = never, R = never>(spec: ConformanceCase<E, R>) => ConformanceCase<E, R>;
|
|
63
|
+
declare const ConformanceMismatch_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
64
|
+
readonly _tag: "ConformanceMismatch";
|
|
65
|
+
} & Readonly<A>;
|
|
66
|
+
/**
|
|
67
|
+
* A wire claim did not hold. Model-free: `expected` / `actual` are optional
|
|
68
|
+
* JSON values chosen by the case author; keep them small and synthetic.
|
|
69
|
+
*/
|
|
70
|
+
declare class ConformanceMismatch extends ConformanceMismatch_base<{
|
|
71
|
+
readonly message: string;
|
|
72
|
+
readonly expected?: Schema.Json;
|
|
73
|
+
readonly actual?: Schema.Json;
|
|
74
|
+
}> {}
|
|
75
|
+
type ConformanceMismatchDetails = {
|
|
76
|
+
readonly expected?: Schema.Json;
|
|
77
|
+
readonly actual?: Schema.Json;
|
|
78
|
+
};
|
|
79
|
+
/** Succeed when `condition` holds; otherwise fail with a `ConformanceMismatch`. */
|
|
80
|
+
declare const expectConformance: (condition: boolean, message: string, details?: ConformanceMismatchDetails) => Effect.Effect<void, ConformanceMismatch>;
|
|
81
|
+
/**
|
|
82
|
+
* Succeed when `actual` and `expected` are structurally equal JSON values
|
|
83
|
+
* (effect `Equal.equals`: same primitives, same array order, same object keys
|
|
84
|
+
* and values); otherwise fail with a `ConformanceMismatch` carrying both.
|
|
85
|
+
*/
|
|
86
|
+
declare const expectEqual: (actual: Schema.Json, expected: Schema.Json, message: string) => Effect.Effect<void, ConformanceMismatch>;
|
|
87
|
+
//#endregion
|
|
88
|
+
export { ConformanceCase, ConformanceCaseId, ConformanceCaseInvalid, ConformanceMismatch, ConformanceMismatchDetails, ConformanceObservation, ConformanceSafety, defineConformanceCase, expectConformance, expectEqual };
|
|
89
|
+
//# sourceMappingURL=case.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"case.d.mts","names":[],"sources":["../src/case.ts"],"mappings":";;;;AA0B6D;AAM7D;;;;AAEC;AAKD;;;;AAb6D,cAFhD,iBAAA,EAAiB,MAAA,CAAA,QAAA;AAAA,KAElB,iBAAA,UAA2B,iBAAA,CAAkB,IAAI;;;;;cAMhD,iBAAA,EAAiB,MAAA,CAAA,MAE7B;;cAKY,sBAAA,EAAsB,MAAA,CAAA,MAAA;;;;KAOvB,sBAAA,UAAgC,sBAAA,CAAuB,IAAI;;;;AAAA;KAM3D,eAAA;EAAe,+DAEhB,EAAA;EAAA,SACA,KAAA;EAAA,SACA,MAAA,EAAQ,iBAAA,EAQE;EAAA,SANV,IAAA,UAO4B;EAAA,SAL5B,IAAA,UAKkB;EAAA,SAHlB,QAAA,GAAW,sBAAA,EAVM;EAAA,SAYjB,QAAA,EAAU,aAAA;EAAA,SACV,GAAA,EAAK,MAAA,CAAO,MAAA,OAAa,CAAA,EAAG,CAAA;AAAA;AAAA,cACtC,2BAAA;;;;;;;cAkBY,sBAAA,SAA+B,2BAAA;EAAA,SACjC,MAAA;EAAA,SACA,MAAA;AAAA;EAAA,IAEI,OAAA,CAAA;AAAA;;AAvByB;AACvC;;;;;cAkCY,qBAAA,yBACX,IAAA,EAAM,eAAA,CAAgB,CAAA,EAAG,CAAA,MACxB,eAAA,CAAgB,CAAA,EAAG,CAAA;AAAA,cAYrB,wBAAA;;;;;;;cAMY,mBAAA,SAA4B,wBAAA;EAAA,SAC9B,OAAA;EAAA,SACA,QAAA,GAAW,MAAA,CAAO,IAAA;EAAA,SAClB,MAAA,GAAS,MAAA,CAAO,IAAA;AAAA;AAAA,KAGf,0BAAA;EAAA,SACD,QAAA,GAAW,MAAA,CAAO,IAAA;EAAA,SAClB,MAAA,GAAS,MAAA,CAAO,IAAI;AAAA;;cAOlB,iBAAA,GACX,SAAA,WACA,OAAA,UACA,OAAA,GAAU,0BAAA,KACT,MAAA,CAAO,MAAA,OAAa,mBAAA;;;;;AAvDvB;cA+Da,WAAA,GACX,MAAA,EAAQ,MAAA,CAAO,IAAA,EACf,QAAA,EAAU,MAAA,CAAO,IAAA,EACjB,OAAA,aACC,MAAA,CAAO,MAAA,OAAa,mBAAA"}
|
package/dist/case.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Data, Effect, Equal, Result } from "effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
//#region src/case.ts
|
|
4
|
+
/**
|
|
5
|
+
* Conformance cases: small, named, pure Effect programs that each prove one
|
|
6
|
+
* claim about how an outside service really behaves on the wire.
|
|
7
|
+
*
|
|
8
|
+
* A case only states what it needs (its `R`); the same case runs unchanged
|
|
9
|
+
* against replayed fixtures, an in-process emulator, a local emulator
|
|
10
|
+
* process, or a real practice account. Only the layers a host provides
|
|
11
|
+
* change. See `@yolk-sdk/conformance/runner` for the safety policy.
|
|
12
|
+
*
|
|
13
|
+
* @experimental
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* What a case does to the account it runs against:
|
|
17
|
+
*
|
|
18
|
+
* - `read`: only reads.
|
|
19
|
+
* - `write-reversible`: writes but leaves the account as it found it (it
|
|
20
|
+
* creates its own records and cleans them up, or the write is rejected and
|
|
21
|
+
* changes nothing).
|
|
22
|
+
* - `write-irreversible`: a write that cannot be undone (for example sending
|
|
23
|
+
* an email). Never automated against a live account.
|
|
24
|
+
*/
|
|
25
|
+
const ConformanceSafety = Schema.Literals([
|
|
26
|
+
"read",
|
|
27
|
+
"write-reversible",
|
|
28
|
+
"write-irreversible"
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Dotted lower-case case id: two or more segments of `a-z`, `0-9`, and inner
|
|
32
|
+
* hyphens, for example `vendor.stream.plain-text`.
|
|
33
|
+
*/
|
|
34
|
+
const ConformanceCaseId = Schema.String.check(Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)+$/));
|
|
35
|
+
const CalendarDate = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/));
|
|
36
|
+
/** When and where a person last watched the claim hold against the real service. */
|
|
37
|
+
const ConformanceObservation = Schema.Struct({
|
|
38
|
+
/** Synthetic account label, for example `synthetic`; never a real account name. */
|
|
39
|
+
account: Schema.NonEmptyString,
|
|
40
|
+
/** Calendar date of the observation (`YYYY-MM-DD`, UTC). */
|
|
41
|
+
date: CalendarDate
|
|
42
|
+
});
|
|
43
|
+
const ConformanceCaseMetadata = Schema.Struct({
|
|
44
|
+
id: ConformanceCaseId,
|
|
45
|
+
title: Schema.optionalKey(Schema.NonEmptyString),
|
|
46
|
+
safety: ConformanceSafety,
|
|
47
|
+
docs: Schema.NonEmptyString,
|
|
48
|
+
wire: Schema.NonEmptyString,
|
|
49
|
+
observed: Schema.optionalKey(ConformanceObservation),
|
|
50
|
+
fixtures: Schema.Array(Schema.NonEmptyString)
|
|
51
|
+
});
|
|
52
|
+
const validateMetadata = Schema.decodeUnknownResult(ConformanceCaseMetadata);
|
|
53
|
+
/**
|
|
54
|
+
* Thrown by `defineConformanceCase` for an invalid definition. A programmer
|
|
55
|
+
* error, surfaced when the defining module loads (like `makeTool`).
|
|
56
|
+
*/
|
|
57
|
+
var ConformanceCaseInvalid = class extends Data.TaggedError("ConformanceCaseInvalid") {
|
|
58
|
+
get message() {
|
|
59
|
+
return `Invalid conformance case ${JSON.stringify(this.caseId)}: ${this.reason}`;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Define a conformance case. The metadata (id format, safety, non-empty
|
|
64
|
+
* `docs`/`wire`, observation date, fixture ids) is validated here and an
|
|
65
|
+
* invalid definition throws `ConformanceCaseInvalid`: definitions are
|
|
66
|
+
* module-level constants, so a bad one fails fast when the module loads
|
|
67
|
+
* instead of surfacing mid-run.
|
|
68
|
+
*/
|
|
69
|
+
const defineConformanceCase = (spec) => {
|
|
70
|
+
const { run: _run, ...metadata } = spec;
|
|
71
|
+
const result = validateMetadata(metadata);
|
|
72
|
+
if (Result.isFailure(result)) throw new ConformanceCaseInvalid({
|
|
73
|
+
caseId: spec.id,
|
|
74
|
+
reason: new Schema.SchemaError(result.failure.issue).message
|
|
75
|
+
});
|
|
76
|
+
return spec;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* A wire claim did not hold. Model-free: `expected` / `actual` are optional
|
|
80
|
+
* JSON values chosen by the case author; keep them small and synthetic.
|
|
81
|
+
*/
|
|
82
|
+
var ConformanceMismatch = class extends Data.TaggedError("ConformanceMismatch") {};
|
|
83
|
+
const mismatch = (message, details = {}) => new ConformanceMismatch({
|
|
84
|
+
message,
|
|
85
|
+
...details
|
|
86
|
+
});
|
|
87
|
+
/** Succeed when `condition` holds; otherwise fail with a `ConformanceMismatch`. */
|
|
88
|
+
const expectConformance = (condition, message, details) => condition ? Effect.void : Effect.fail(mismatch(message, details));
|
|
89
|
+
/**
|
|
90
|
+
* Succeed when `actual` and `expected` are structurally equal JSON values
|
|
91
|
+
* (effect `Equal.equals`: same primitives, same array order, same object keys
|
|
92
|
+
* and values); otherwise fail with a `ConformanceMismatch` carrying both.
|
|
93
|
+
*/
|
|
94
|
+
const expectEqual = (actual, expected, message) => expectConformance(Equal.equals(actual, expected), message, {
|
|
95
|
+
expected,
|
|
96
|
+
actual
|
|
97
|
+
});
|
|
98
|
+
//#endregion
|
|
99
|
+
export { ConformanceCaseId, ConformanceCaseInvalid, ConformanceMismatch, ConformanceObservation, ConformanceSafety, defineConformanceCase, expectConformance, expectEqual };
|
|
100
|
+
|
|
101
|
+
//# sourceMappingURL=case.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"case.mjs","names":[],"sources":["../src/case.ts"],"sourcesContent":["/**\n * Conformance cases: small, named, pure Effect programs that each prove one\n * claim about how an outside service really behaves on the wire.\n *\n * A case only states what it needs (its `R`); the same case runs unchanged\n * against replayed fixtures, an in-process emulator, a local emulator\n * process, or a real practice account. Only the layers a host provides\n * change. See `@yolk-sdk/conformance/runner` for the safety policy.\n *\n * @experimental\n */\nimport { Data, Effect, Equal, Result } from 'effect'\nimport * as Schema from 'effect/Schema'\n\n/**\n * What a case does to the account it runs against:\n *\n * - `read`: only reads.\n * - `write-reversible`: writes but leaves the account as it found it (it\n * creates its own records and cleans them up, or the write is rejected and\n * changes nothing).\n * - `write-irreversible`: a write that cannot be undone (for example sending\n * an email). Never automated against a live account.\n */\nexport const ConformanceSafety = Schema.Literals(['read', 'write-reversible', 'write-irreversible'])\n\nexport type ConformanceSafety = typeof ConformanceSafety.Type\n\n/**\n * Dotted lower-case case id: two or more segments of `a-z`, `0-9`, and inner\n * hyphens, for example `vendor.stream.plain-text`.\n */\nexport const ConformanceCaseId = Schema.String.check(\n Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*(?:\\.[a-z0-9]+(?:-[a-z0-9]+)*)+$/)\n)\n\nconst CalendarDate = Schema.String.check(Schema.isPattern(/^\\d{4}-\\d{2}-\\d{2}$/))\n\n/** When and where a person last watched the claim hold against the real service. */\nexport const ConformanceObservation = Schema.Struct({\n /** Synthetic account label, for example `synthetic`; never a real account name. */\n account: Schema.NonEmptyString,\n /** Calendar date of the observation (`YYYY-MM-DD`, UTC). */\n date: CalendarDate\n})\n\nexport type ConformanceObservation = typeof ConformanceObservation.Type\n\n/**\n * One wire claim. `run` succeeds when the claim holds and fails (with a\n * `ConformanceMismatch`, its own error, or a port's error) when it does not.\n */\nexport type ConformanceCase<E = never, R = never> = {\n /** Dotted lower-case id (see `ConformanceCaseId`). */\n readonly id: string\n readonly title?: string\n readonly safety: ConformanceSafety\n /** What the service's documentation claims. */\n readonly docs: string\n /** What the wire actually does (the claim this case proves). */\n readonly wire: string\n /** Last live observation. Absent means the claim is unverified against the real service. */\n readonly observed?: ConformanceObservation\n /** Ids of the fixtures (`WireFixture`s or `PortFixture`s) that back replay of this case. */\n readonly fixtures: ReadonlyArray<string>\n readonly run: Effect.Effect<void, E, R>\n}\n\nconst ConformanceCaseMetadata = Schema.Struct({\n id: ConformanceCaseId,\n title: Schema.optionalKey(Schema.NonEmptyString),\n safety: ConformanceSafety,\n docs: Schema.NonEmptyString,\n wire: Schema.NonEmptyString,\n observed: Schema.optionalKey(ConformanceObservation),\n fixtures: Schema.Array(Schema.NonEmptyString)\n})\n\nconst validateMetadata = Schema.decodeUnknownResult(ConformanceCaseMetadata)\n\n/**\n * Thrown by `defineConformanceCase` for an invalid definition. A programmer\n * error, surfaced when the defining module loads (like `makeTool`).\n */\nexport class ConformanceCaseInvalid extends Data.TaggedError('ConformanceCaseInvalid')<{\n readonly caseId: string\n readonly reason: string\n}> {\n override get message(): string {\n return `Invalid conformance case ${JSON.stringify(this.caseId)}: ${this.reason}`\n }\n}\n\n/**\n * Define a conformance case. The metadata (id format, safety, non-empty\n * `docs`/`wire`, observation date, fixture ids) is validated here and an\n * invalid definition throws `ConformanceCaseInvalid`: definitions are\n * module-level constants, so a bad one fails fast when the module loads\n * instead of surfacing mid-run.\n */\nexport const defineConformanceCase = <E = never, R = never>(\n spec: ConformanceCase<E, R>\n): ConformanceCase<E, R> => {\n const { run: _run, ...metadata } = spec\n const result = validateMetadata(metadata)\n\n if (Result.isFailure(result)) {\n throw new ConformanceCaseInvalid({\n caseId: spec.id,\n reason: new Schema.SchemaError(result.failure.issue).message\n })\n }\n\n return spec\n}\n\n/**\n * A wire claim did not hold. Model-free: `expected` / `actual` are optional\n * JSON values chosen by the case author; keep them small and synthetic.\n */\nexport class ConformanceMismatch extends Data.TaggedError('ConformanceMismatch')<{\n readonly message: string\n readonly expected?: Schema.Json\n readonly actual?: Schema.Json\n}> {}\n\nexport type ConformanceMismatchDetails = {\n readonly expected?: Schema.Json\n readonly actual?: Schema.Json\n}\n\nconst mismatch = (message: string, details: ConformanceMismatchDetails = {}) =>\n new ConformanceMismatch({ message, ...details })\n\n/** Succeed when `condition` holds; otherwise fail with a `ConformanceMismatch`. */\nexport const expectConformance = (\n condition: boolean,\n message: string,\n details?: ConformanceMismatchDetails\n): Effect.Effect<void, ConformanceMismatch> =>\n condition ? Effect.void : Effect.fail(mismatch(message, details))\n\n/**\n * Succeed when `actual` and `expected` are structurally equal JSON values\n * (effect `Equal.equals`: same primitives, same array order, same object keys\n * and values); otherwise fail with a `ConformanceMismatch` carrying both.\n */\nexport const expectEqual = (\n actual: Schema.Json,\n expected: Schema.Json,\n message: string\n): Effect.Effect<void, ConformanceMismatch> =>\n expectConformance(Equal.equals(actual, expected), message, { expected, actual })\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,oBAAoB,OAAO,SAAS;CAAC;CAAQ;CAAoB;AAAoB,CAAC;;;;;AAQnG,MAAa,oBAAoB,OAAO,OAAO,MAC7C,OAAO,UAAU,2DAA2D,CAC9E;AAEA,MAAM,eAAe,OAAO,OAAO,MAAM,OAAO,UAAU,qBAAqB,CAAC;;AAGhF,MAAa,yBAAyB,OAAO,OAAO;;CAElD,SAAS,OAAO;;CAEhB,MAAM;AACR,CAAC;AAwBD,MAAM,0BAA0B,OAAO,OAAO;CAC5C,IAAI;CACJ,OAAO,OAAO,YAAY,OAAO,cAAc;CAC/C,QAAQ;CACR,MAAM,OAAO;CACb,MAAM,OAAO;CACb,UAAU,OAAO,YAAY,sBAAsB;CACnD,UAAU,OAAO,MAAM,OAAO,cAAc;AAC9C,CAAC;AAED,MAAM,mBAAmB,OAAO,oBAAoB,uBAAuB;;;;;AAM3E,IAAa,yBAAb,cAA4C,KAAK,YAAY,wBAAwB,EAGlF;CACD,IAAa,UAAkB;EAC7B,OAAO,4BAA4B,KAAK,UAAU,KAAK,MAAM,EAAE,IAAI,KAAK;CAC1E;AACF;;;;;;;;AASA,MAAa,yBACX,SAC0B;CAC1B,MAAM,EAAE,KAAK,MAAM,GAAG,aAAa;CACnC,MAAM,SAAS,iBAAiB,QAAQ;CAExC,IAAI,OAAO,UAAU,MAAM,GACzB,MAAM,IAAI,uBAAuB;EAC/B,QAAQ,KAAK;EACb,QAAQ,IAAI,OAAO,YAAY,OAAO,QAAQ,KAAK,EAAE;CACvD,CAAC;CAGH,OAAO;AACT;;;;;AAMA,IAAa,sBAAb,cAAyC,KAAK,YAAY,qBAAqB,EAI5E,CAAC;AAOJ,MAAM,YAAY,SAAiB,UAAsC,CAAC,MACxE,IAAI,oBAAoB;CAAE;CAAS,GAAG;AAAQ,CAAC;;AAGjD,MAAa,qBACX,WACA,SACA,YAEA,YAAY,OAAO,OAAO,OAAO,KAAK,SAAS,SAAS,OAAO,CAAC;;;;;;AAOlE,MAAa,eACX,QACA,UACA,YAEA,kBAAkB,MAAM,OAAO,QAAQ,QAAQ,GAAG,SAAS;CAAE;CAAU;AAAO,CAAC"}
|