@contractspec/lib.testing 3.7.6 → 3.7.10
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/README.md +41 -30
- package/dist/index.d.ts +5 -5
- package/dist/index.js +85 -89
- package/dist/node/index.js +85 -89
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,51 +1,62 @@
|
|
|
1
1
|
# @contractspec/lib.testing
|
|
2
2
|
|
|
3
|
-
Website: https://contractspec.io
|
|
3
|
+
Website: https://contractspec.io
|
|
4
4
|
|
|
5
|
+
**Contract-aware testing utilities and runners.**
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
## What It Provides
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
- **Layer**: lib.
|
|
10
|
+
- **Consumers**: CLI, bundles.
|
|
11
|
+
- `src/adapters/` contains runtime, provider, or environment-specific adapters.
|
|
12
|
+
- Related ContractSpec packages include `@contractspec/lib.contracts-spec`, `@contractspec/lib.schema`, `@contractspec/tool.bun`, `@contractspec/tool.typescript`.
|
|
13
|
+
- `src/adapters/` contains runtime, provider, or environment-specific adapters.
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
- **GoldenTestGenerator** converts snapshots into runnable suites.
|
|
12
|
-
- **Adapters** output Vitest or Jest files and helper runners.
|
|
13
|
-
|
|
14
|
-
### Usage
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
import {
|
|
18
|
-
TrafficRecorder,
|
|
19
|
-
InMemoryTrafficStore,
|
|
20
|
-
} from '@contractspec/lib.testing/recorder';
|
|
21
|
-
|
|
22
|
-
const recorder = new TrafficRecorder({
|
|
23
|
-
store: new InMemoryTrafficStore(),
|
|
24
|
-
sampleRate: 0.01,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
await recorder.record({
|
|
28
|
-
operation: { name: 'orders.create', version: 3 },
|
|
29
|
-
input: payload,
|
|
30
|
-
output,
|
|
31
|
-
success: true,
|
|
32
|
-
timestamp: new Date(),
|
|
33
|
-
});
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
See `GoldenTestGenerator` for generating suites and CLI in `@contractspec/app.contracts-cli`.
|
|
15
|
+
## Installation
|
|
37
16
|
|
|
17
|
+
`npm install @contractspec/lib.testing`
|
|
38
18
|
|
|
19
|
+
or
|
|
39
20
|
|
|
21
|
+
`bun add @contractspec/lib.testing`
|
|
40
22
|
|
|
23
|
+
## Usage
|
|
41
24
|
|
|
25
|
+
Import the root entrypoint from `@contractspec/lib.testing`, or choose a documented subpath when you only need one part of the package surface.
|
|
42
26
|
|
|
27
|
+
## Architecture
|
|
43
28
|
|
|
29
|
+
- `src/adapters/` contains runtime, provider, or environment-specific adapters.
|
|
30
|
+
- `src/generator` is part of the package's public or composition surface.
|
|
31
|
+
- `src/index.ts` is the root public barrel and package entrypoint.
|
|
32
|
+
- `src/recorder` is part of the package's public or composition surface.
|
|
33
|
+
- `src/types.ts` is shared public type definitions.
|
|
44
34
|
|
|
35
|
+
## Public Entry Points
|
|
45
36
|
|
|
37
|
+
- Export `.` resolves through `./src/index.ts`.
|
|
46
38
|
|
|
39
|
+
## Local Commands
|
|
47
40
|
|
|
41
|
+
- `bun run dev` — contractspec-bun-build dev
|
|
42
|
+
- `bun run build` — bun run prebuild && bun run build:bundle && bun run build:types
|
|
43
|
+
- `bun run test` — bun test
|
|
44
|
+
- `bun run lint` — bun lint:fix
|
|
45
|
+
- `bun run lint:check` — biome check .
|
|
46
|
+
- `bun run lint:fix` — biome check --write --unsafe --only=nursery/useSortedClasses . && biome check --write .
|
|
47
|
+
- `bun run typecheck` — tsc --noEmit
|
|
48
|
+
- `bun run publish:pkg` — bun publish --tolerate-republish --ignore-scripts --verbose
|
|
49
|
+
- `bun run publish:pkg:canary` — bun publish:pkg --tag canary
|
|
50
|
+
- `bun run clean` — rimraf dist .turbo
|
|
51
|
+
- `bun run build:bundle` — contractspec-bun-build transpile
|
|
52
|
+
- `bun run build:types` — contractspec-bun-build types
|
|
53
|
+
- `bun run prebuild` — contractspec-bun-build prebuild
|
|
48
54
|
|
|
55
|
+
## Recent Updates
|
|
49
56
|
|
|
57
|
+
- Replace eslint+prettier by biomejs to optimize speed.
|
|
50
58
|
|
|
59
|
+
## Notes
|
|
51
60
|
|
|
61
|
+
- TrafficRecorder and GoldenTestGenerator interfaces are public API — do not break signatures.
|
|
62
|
+
- Test output format must stay compatible with Vitest and Jest runners.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export * from './types';
|
|
2
|
-
export * from './recorder/traffic-recorder';
|
|
3
|
-
export * from './generator/golden-test-generator';
|
|
4
|
-
export * from './generator/assertion-builder';
|
|
5
|
-
export * from './adapters/vitest-adapter';
|
|
6
1
|
export * from './adapters/jest-adapter';
|
|
2
|
+
export * from './adapters/vitest-adapter';
|
|
3
|
+
export * from './generator/assertion-builder';
|
|
4
|
+
export * from './generator/golden-test-generator';
|
|
5
|
+
export * from './recorder/traffic-recorder';
|
|
6
|
+
export * from './types';
|
package/dist/index.js
CHANGED
|
@@ -1,71 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/recorder/traffic-recorder.ts
|
|
3
|
-
import { randomUUID } from "crypto";
|
|
4
|
-
|
|
5
|
-
class InMemoryTrafficStore {
|
|
6
|
-
items = [];
|
|
7
|
-
async save(snapshot) {
|
|
8
|
-
this.items.push(snapshot);
|
|
9
|
-
}
|
|
10
|
-
async list(operation) {
|
|
11
|
-
if (!operation)
|
|
12
|
-
return [...this.items];
|
|
13
|
-
return this.items.filter((item) => item.operation.name === operation);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
class TrafficRecorder {
|
|
18
|
-
store;
|
|
19
|
-
sampleRate;
|
|
20
|
-
sanitize;
|
|
21
|
-
constructor(options) {
|
|
22
|
-
this.store = options.store;
|
|
23
|
-
this.sampleRate = options.sampleRate ?? 1;
|
|
24
|
-
this.sanitize = options.sanitize;
|
|
25
|
-
}
|
|
26
|
-
async record(input) {
|
|
27
|
-
if (!this.shouldSample())
|
|
28
|
-
return;
|
|
29
|
-
const snapshot = {
|
|
30
|
-
id: randomUUID(),
|
|
31
|
-
operation: input.operation,
|
|
32
|
-
input: structuredCloneSafe(input.input),
|
|
33
|
-
output: structuredCloneSafe(input.output),
|
|
34
|
-
error: input.error ? structuredCloneSafe(input.error) : undefined,
|
|
35
|
-
success: input.success,
|
|
36
|
-
timestamp: new Date,
|
|
37
|
-
durationMs: input.durationMs,
|
|
38
|
-
tenantId: input.tenantId,
|
|
39
|
-
userId: input.userId,
|
|
40
|
-
channel: input.channel,
|
|
41
|
-
metadata: input.metadata
|
|
42
|
-
};
|
|
43
|
-
const sanitized = this.sanitize ? this.sanitize(snapshot) : snapshot;
|
|
44
|
-
await this.store.save(sanitized);
|
|
45
|
-
}
|
|
46
|
-
shouldSample() {
|
|
47
|
-
if (this.sampleRate >= 1)
|
|
48
|
-
return true;
|
|
49
|
-
return Math.random() <= this.sampleRate;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function structuredCloneSafe(value) {
|
|
53
|
-
if (value == null)
|
|
54
|
-
return value ?? undefined;
|
|
55
|
-
try {
|
|
56
|
-
const clone = globalThis.structuredClone;
|
|
57
|
-
if (typeof clone === "function") {
|
|
58
|
-
return clone(value);
|
|
59
|
-
}
|
|
60
|
-
return JSON.parse(JSON.stringify(value));
|
|
61
|
-
} catch {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// src/generator/golden-test-generator.ts
|
|
66
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
67
|
-
import { performance } from "perf_hooks";
|
|
68
|
-
|
|
69
2
|
// src/generator/assertion-builder.ts
|
|
70
3
|
function buildAssertions(testCase, ctx) {
|
|
71
4
|
if (testCase.success) {
|
|
@@ -87,60 +20,60 @@ function serialize(value) {
|
|
|
87
20
|
}, 2);
|
|
88
21
|
}
|
|
89
22
|
|
|
90
|
-
// src/adapters/
|
|
91
|
-
function
|
|
23
|
+
// src/adapters/jest-adapter.ts
|
|
24
|
+
function generateJestSuite(options) {
|
|
92
25
|
const caseBlocks = options.cases.map((testCase) => {
|
|
93
26
|
const inputConst = serialize(testCase.input);
|
|
94
27
|
const metadataConst = serialize(testCase.metadata ?? {});
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
] : [
|
|
99
|
-
`await expect(${options.runnerFunction}(input${testCase.id}, metadata${testCase.id})).rejects.toMatchObject(${serialize(testCase.expectedError ?? { message: "expected failure" })});`
|
|
100
|
-
];
|
|
28
|
+
const successBlock = `const result = await ${options.runnerFunction}(input${testCase.id}, metadata${testCase.id});
|
|
29
|
+
expect(result).toEqual(${serialize(testCase.expectedOutput ?? null)});`;
|
|
30
|
+
const failureBlock = `await expect(${options.runnerFunction}(input${testCase.id}, metadata${testCase.id})).rejects.toMatchObject(${serialize(testCase.expectedError ?? { message: "expected failure" })});`;
|
|
101
31
|
return `
|
|
102
|
-
|
|
32
|
+
test('${testCase.name}', async () => {
|
|
103
33
|
const input${testCase.id} = ${inputConst};
|
|
104
34
|
const metadata${testCase.id} = ${metadataConst};
|
|
105
|
-
${
|
|
106
|
-
`)}
|
|
35
|
+
${testCase.success ? successBlock : failureBlock}
|
|
107
36
|
});`;
|
|
108
37
|
}).join(`
|
|
109
38
|
`);
|
|
110
39
|
return `
|
|
111
|
-
import { describe, it, expect } from 'bun:test';
|
|
112
40
|
import { ${options.runnerFunction} } from '${options.runnerImport}';
|
|
113
41
|
|
|
114
42
|
describe('${options.suiteName}', () => {${caseBlocks}
|
|
115
43
|
});
|
|
116
44
|
`.trim();
|
|
117
45
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
function generateJestSuite(options) {
|
|
46
|
+
// src/adapters/vitest-adapter.ts
|
|
47
|
+
function generateVitestSuite(options) {
|
|
121
48
|
const caseBlocks = options.cases.map((testCase) => {
|
|
122
49
|
const inputConst = serialize(testCase.input);
|
|
123
50
|
const metadataConst = serialize(testCase.metadata ?? {});
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
51
|
+
const assertions = testCase.success ? [
|
|
52
|
+
`const result = await ${options.runnerFunction}(input${testCase.id}, metadata${testCase.id});`,
|
|
53
|
+
`expect(result).toEqual(${serialize(testCase.expectedOutput ?? null)});`
|
|
54
|
+
] : [
|
|
55
|
+
`await expect(${options.runnerFunction}(input${testCase.id}, metadata${testCase.id})).rejects.toMatchObject(${serialize(testCase.expectedError ?? { message: "expected failure" })});`
|
|
56
|
+
];
|
|
127
57
|
return `
|
|
128
|
-
|
|
58
|
+
it('${testCase.name}', async () => {
|
|
129
59
|
const input${testCase.id} = ${inputConst};
|
|
130
60
|
const metadata${testCase.id} = ${metadataConst};
|
|
131
|
-
${
|
|
61
|
+
${assertions.join(`
|
|
62
|
+
`)}
|
|
132
63
|
});`;
|
|
133
64
|
}).join(`
|
|
134
65
|
`);
|
|
135
66
|
return `
|
|
67
|
+
import { describe, it, expect } from 'bun:test';
|
|
136
68
|
import { ${options.runnerFunction} } from '${options.runnerImport}';
|
|
137
69
|
|
|
138
70
|
describe('${options.suiteName}', () => {${caseBlocks}
|
|
139
71
|
});
|
|
140
72
|
`.trim();
|
|
141
73
|
}
|
|
142
|
-
|
|
143
74
|
// src/generator/golden-test-generator.ts
|
|
75
|
+
import { randomUUID } from "crypto";
|
|
76
|
+
import { performance } from "perf_hooks";
|
|
144
77
|
class GoldenTestGenerator {
|
|
145
78
|
serializeMetadata;
|
|
146
79
|
constructor(serializeMetadata = (snapshot) => ({
|
|
@@ -152,7 +85,7 @@ class GoldenTestGenerator {
|
|
|
152
85
|
}
|
|
153
86
|
createCases(snapshots) {
|
|
154
87
|
return snapshots.map((snapshot, index) => ({
|
|
155
|
-
id: snapshot.id ??
|
|
88
|
+
id: snapshot.id ?? randomUUID(),
|
|
156
89
|
name: snapshot.success ? `case-${index + 1}-success` : `case-${index + 1}-failure`,
|
|
157
90
|
input: snapshot.input,
|
|
158
91
|
expectedOutput: snapshot.output,
|
|
@@ -212,6 +145,69 @@ async function runGoldenTests(cases, runner) {
|
|
|
212
145
|
}
|
|
213
146
|
return results;
|
|
214
147
|
}
|
|
148
|
+
// src/recorder/traffic-recorder.ts
|
|
149
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
150
|
+
|
|
151
|
+
class InMemoryTrafficStore {
|
|
152
|
+
items = [];
|
|
153
|
+
async save(snapshot) {
|
|
154
|
+
this.items.push(snapshot);
|
|
155
|
+
}
|
|
156
|
+
async list(operation) {
|
|
157
|
+
if (!operation)
|
|
158
|
+
return [...this.items];
|
|
159
|
+
return this.items.filter((item) => item.operation.name === operation);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class TrafficRecorder {
|
|
164
|
+
store;
|
|
165
|
+
sampleRate;
|
|
166
|
+
sanitize;
|
|
167
|
+
constructor(options) {
|
|
168
|
+
this.store = options.store;
|
|
169
|
+
this.sampleRate = options.sampleRate ?? 1;
|
|
170
|
+
this.sanitize = options.sanitize;
|
|
171
|
+
}
|
|
172
|
+
async record(input) {
|
|
173
|
+
if (!this.shouldSample())
|
|
174
|
+
return;
|
|
175
|
+
const snapshot = {
|
|
176
|
+
id: randomUUID2(),
|
|
177
|
+
operation: input.operation,
|
|
178
|
+
input: structuredCloneSafe(input.input),
|
|
179
|
+
output: structuredCloneSafe(input.output),
|
|
180
|
+
error: input.error ? structuredCloneSafe(input.error) : undefined,
|
|
181
|
+
success: input.success,
|
|
182
|
+
timestamp: new Date,
|
|
183
|
+
durationMs: input.durationMs,
|
|
184
|
+
tenantId: input.tenantId,
|
|
185
|
+
userId: input.userId,
|
|
186
|
+
channel: input.channel,
|
|
187
|
+
metadata: input.metadata
|
|
188
|
+
};
|
|
189
|
+
const sanitized = this.sanitize ? this.sanitize(snapshot) : snapshot;
|
|
190
|
+
await this.store.save(sanitized);
|
|
191
|
+
}
|
|
192
|
+
shouldSample() {
|
|
193
|
+
if (this.sampleRate >= 1)
|
|
194
|
+
return true;
|
|
195
|
+
return Math.random() <= this.sampleRate;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function structuredCloneSafe(value) {
|
|
199
|
+
if (value == null)
|
|
200
|
+
return value ?? undefined;
|
|
201
|
+
try {
|
|
202
|
+
const clone = globalThis.structuredClone;
|
|
203
|
+
if (typeof clone === "function") {
|
|
204
|
+
return clone(value);
|
|
205
|
+
}
|
|
206
|
+
return JSON.parse(JSON.stringify(value));
|
|
207
|
+
} catch {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
215
211
|
export {
|
|
216
212
|
serialize,
|
|
217
213
|
runGoldenTests,
|
package/dist/node/index.js
CHANGED
|
@@ -1,70 +1,3 @@
|
|
|
1
|
-
// src/recorder/traffic-recorder.ts
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
class InMemoryTrafficStore {
|
|
5
|
-
items = [];
|
|
6
|
-
async save(snapshot) {
|
|
7
|
-
this.items.push(snapshot);
|
|
8
|
-
}
|
|
9
|
-
async list(operation) {
|
|
10
|
-
if (!operation)
|
|
11
|
-
return [...this.items];
|
|
12
|
-
return this.items.filter((item) => item.operation.name === operation);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
class TrafficRecorder {
|
|
17
|
-
store;
|
|
18
|
-
sampleRate;
|
|
19
|
-
sanitize;
|
|
20
|
-
constructor(options) {
|
|
21
|
-
this.store = options.store;
|
|
22
|
-
this.sampleRate = options.sampleRate ?? 1;
|
|
23
|
-
this.sanitize = options.sanitize;
|
|
24
|
-
}
|
|
25
|
-
async record(input) {
|
|
26
|
-
if (!this.shouldSample())
|
|
27
|
-
return;
|
|
28
|
-
const snapshot = {
|
|
29
|
-
id: randomUUID(),
|
|
30
|
-
operation: input.operation,
|
|
31
|
-
input: structuredCloneSafe(input.input),
|
|
32
|
-
output: structuredCloneSafe(input.output),
|
|
33
|
-
error: input.error ? structuredCloneSafe(input.error) : undefined,
|
|
34
|
-
success: input.success,
|
|
35
|
-
timestamp: new Date,
|
|
36
|
-
durationMs: input.durationMs,
|
|
37
|
-
tenantId: input.tenantId,
|
|
38
|
-
userId: input.userId,
|
|
39
|
-
channel: input.channel,
|
|
40
|
-
metadata: input.metadata
|
|
41
|
-
};
|
|
42
|
-
const sanitized = this.sanitize ? this.sanitize(snapshot) : snapshot;
|
|
43
|
-
await this.store.save(sanitized);
|
|
44
|
-
}
|
|
45
|
-
shouldSample() {
|
|
46
|
-
if (this.sampleRate >= 1)
|
|
47
|
-
return true;
|
|
48
|
-
return Math.random() <= this.sampleRate;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function structuredCloneSafe(value) {
|
|
52
|
-
if (value == null)
|
|
53
|
-
return value ?? undefined;
|
|
54
|
-
try {
|
|
55
|
-
const clone = globalThis.structuredClone;
|
|
56
|
-
if (typeof clone === "function") {
|
|
57
|
-
return clone(value);
|
|
58
|
-
}
|
|
59
|
-
return JSON.parse(JSON.stringify(value));
|
|
60
|
-
} catch {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// src/generator/golden-test-generator.ts
|
|
65
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
66
|
-
import { performance } from "node:perf_hooks";
|
|
67
|
-
|
|
68
1
|
// src/generator/assertion-builder.ts
|
|
69
2
|
function buildAssertions(testCase, ctx) {
|
|
70
3
|
if (testCase.success) {
|
|
@@ -86,60 +19,60 @@ function serialize(value) {
|
|
|
86
19
|
}, 2);
|
|
87
20
|
}
|
|
88
21
|
|
|
89
|
-
// src/adapters/
|
|
90
|
-
function
|
|
22
|
+
// src/adapters/jest-adapter.ts
|
|
23
|
+
function generateJestSuite(options) {
|
|
91
24
|
const caseBlocks = options.cases.map((testCase) => {
|
|
92
25
|
const inputConst = serialize(testCase.input);
|
|
93
26
|
const metadataConst = serialize(testCase.metadata ?? {});
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
] : [
|
|
98
|
-
`await expect(${options.runnerFunction}(input${testCase.id}, metadata${testCase.id})).rejects.toMatchObject(${serialize(testCase.expectedError ?? { message: "expected failure" })});`
|
|
99
|
-
];
|
|
27
|
+
const successBlock = `const result = await ${options.runnerFunction}(input${testCase.id}, metadata${testCase.id});
|
|
28
|
+
expect(result).toEqual(${serialize(testCase.expectedOutput ?? null)});`;
|
|
29
|
+
const failureBlock = `await expect(${options.runnerFunction}(input${testCase.id}, metadata${testCase.id})).rejects.toMatchObject(${serialize(testCase.expectedError ?? { message: "expected failure" })});`;
|
|
100
30
|
return `
|
|
101
|
-
|
|
31
|
+
test('${testCase.name}', async () => {
|
|
102
32
|
const input${testCase.id} = ${inputConst};
|
|
103
33
|
const metadata${testCase.id} = ${metadataConst};
|
|
104
|
-
${
|
|
105
|
-
`)}
|
|
34
|
+
${testCase.success ? successBlock : failureBlock}
|
|
106
35
|
});`;
|
|
107
36
|
}).join(`
|
|
108
37
|
`);
|
|
109
38
|
return `
|
|
110
|
-
import { describe, it, expect } from 'bun:test';
|
|
111
39
|
import { ${options.runnerFunction} } from '${options.runnerImport}';
|
|
112
40
|
|
|
113
41
|
describe('${options.suiteName}', () => {${caseBlocks}
|
|
114
42
|
});
|
|
115
43
|
`.trim();
|
|
116
44
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
function generateJestSuite(options) {
|
|
45
|
+
// src/adapters/vitest-adapter.ts
|
|
46
|
+
function generateVitestSuite(options) {
|
|
120
47
|
const caseBlocks = options.cases.map((testCase) => {
|
|
121
48
|
const inputConst = serialize(testCase.input);
|
|
122
49
|
const metadataConst = serialize(testCase.metadata ?? {});
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
50
|
+
const assertions = testCase.success ? [
|
|
51
|
+
`const result = await ${options.runnerFunction}(input${testCase.id}, metadata${testCase.id});`,
|
|
52
|
+
`expect(result).toEqual(${serialize(testCase.expectedOutput ?? null)});`
|
|
53
|
+
] : [
|
|
54
|
+
`await expect(${options.runnerFunction}(input${testCase.id}, metadata${testCase.id})).rejects.toMatchObject(${serialize(testCase.expectedError ?? { message: "expected failure" })});`
|
|
55
|
+
];
|
|
126
56
|
return `
|
|
127
|
-
|
|
57
|
+
it('${testCase.name}', async () => {
|
|
128
58
|
const input${testCase.id} = ${inputConst};
|
|
129
59
|
const metadata${testCase.id} = ${metadataConst};
|
|
130
|
-
${
|
|
60
|
+
${assertions.join(`
|
|
61
|
+
`)}
|
|
131
62
|
});`;
|
|
132
63
|
}).join(`
|
|
133
64
|
`);
|
|
134
65
|
return `
|
|
66
|
+
import { describe, it, expect } from 'bun:test';
|
|
135
67
|
import { ${options.runnerFunction} } from '${options.runnerImport}';
|
|
136
68
|
|
|
137
69
|
describe('${options.suiteName}', () => {${caseBlocks}
|
|
138
70
|
});
|
|
139
71
|
`.trim();
|
|
140
72
|
}
|
|
141
|
-
|
|
142
73
|
// src/generator/golden-test-generator.ts
|
|
74
|
+
import { randomUUID } from "node:crypto";
|
|
75
|
+
import { performance } from "node:perf_hooks";
|
|
143
76
|
class GoldenTestGenerator {
|
|
144
77
|
serializeMetadata;
|
|
145
78
|
constructor(serializeMetadata = (snapshot) => ({
|
|
@@ -151,7 +84,7 @@ class GoldenTestGenerator {
|
|
|
151
84
|
}
|
|
152
85
|
createCases(snapshots) {
|
|
153
86
|
return snapshots.map((snapshot, index) => ({
|
|
154
|
-
id: snapshot.id ??
|
|
87
|
+
id: snapshot.id ?? randomUUID(),
|
|
155
88
|
name: snapshot.success ? `case-${index + 1}-success` : `case-${index + 1}-failure`,
|
|
156
89
|
input: snapshot.input,
|
|
157
90
|
expectedOutput: snapshot.output,
|
|
@@ -211,6 +144,69 @@ async function runGoldenTests(cases, runner) {
|
|
|
211
144
|
}
|
|
212
145
|
return results;
|
|
213
146
|
}
|
|
147
|
+
// src/recorder/traffic-recorder.ts
|
|
148
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
149
|
+
|
|
150
|
+
class InMemoryTrafficStore {
|
|
151
|
+
items = [];
|
|
152
|
+
async save(snapshot) {
|
|
153
|
+
this.items.push(snapshot);
|
|
154
|
+
}
|
|
155
|
+
async list(operation) {
|
|
156
|
+
if (!operation)
|
|
157
|
+
return [...this.items];
|
|
158
|
+
return this.items.filter((item) => item.operation.name === operation);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
class TrafficRecorder {
|
|
163
|
+
store;
|
|
164
|
+
sampleRate;
|
|
165
|
+
sanitize;
|
|
166
|
+
constructor(options) {
|
|
167
|
+
this.store = options.store;
|
|
168
|
+
this.sampleRate = options.sampleRate ?? 1;
|
|
169
|
+
this.sanitize = options.sanitize;
|
|
170
|
+
}
|
|
171
|
+
async record(input) {
|
|
172
|
+
if (!this.shouldSample())
|
|
173
|
+
return;
|
|
174
|
+
const snapshot = {
|
|
175
|
+
id: randomUUID2(),
|
|
176
|
+
operation: input.operation,
|
|
177
|
+
input: structuredCloneSafe(input.input),
|
|
178
|
+
output: structuredCloneSafe(input.output),
|
|
179
|
+
error: input.error ? structuredCloneSafe(input.error) : undefined,
|
|
180
|
+
success: input.success,
|
|
181
|
+
timestamp: new Date,
|
|
182
|
+
durationMs: input.durationMs,
|
|
183
|
+
tenantId: input.tenantId,
|
|
184
|
+
userId: input.userId,
|
|
185
|
+
channel: input.channel,
|
|
186
|
+
metadata: input.metadata
|
|
187
|
+
};
|
|
188
|
+
const sanitized = this.sanitize ? this.sanitize(snapshot) : snapshot;
|
|
189
|
+
await this.store.save(sanitized);
|
|
190
|
+
}
|
|
191
|
+
shouldSample() {
|
|
192
|
+
if (this.sampleRate >= 1)
|
|
193
|
+
return true;
|
|
194
|
+
return Math.random() <= this.sampleRate;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function structuredCloneSafe(value) {
|
|
198
|
+
if (value == null)
|
|
199
|
+
return value ?? undefined;
|
|
200
|
+
try {
|
|
201
|
+
const clone = globalThis.structuredClone;
|
|
202
|
+
if (typeof clone === "function") {
|
|
203
|
+
return clone(value);
|
|
204
|
+
}
|
|
205
|
+
return JSON.parse(JSON.stringify(value));
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
214
210
|
export {
|
|
215
211
|
serialize,
|
|
216
212
|
runGoldenTests,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/lib.testing",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.10",
|
|
4
4
|
"description": "Contract-aware testing utilities and runners",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -23,20 +23,20 @@
|
|
|
23
23
|
"dev": "contractspec-bun-build dev",
|
|
24
24
|
"clean": "rimraf dist .turbo",
|
|
25
25
|
"lint": "bun lint:fix",
|
|
26
|
-
"lint:fix": "
|
|
27
|
-
"lint:check": "
|
|
26
|
+
"lint:fix": "biome check --write --unsafe --only=nursery/useSortedClasses . && biome check --write .",
|
|
27
|
+
"lint:check": "biome check .",
|
|
28
28
|
"test": "bun test",
|
|
29
29
|
"prebuild": "contractspec-bun-build prebuild",
|
|
30
30
|
"typecheck": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@contractspec/lib.schema": "3.7.
|
|
34
|
-
"@contractspec/lib.contracts-spec": "
|
|
33
|
+
"@contractspec/lib.schema": "3.7.8",
|
|
34
|
+
"@contractspec/lib.contracts-spec": "4.1.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@contractspec/tool.typescript": "3.7.
|
|
37
|
+
"@contractspec/tool.typescript": "3.7.8",
|
|
38
38
|
"typescript": "^5.9.3",
|
|
39
|
-
"@contractspec/tool.bun": "3.7.
|
|
39
|
+
"@contractspec/tool.bun": "3.7.8"
|
|
40
40
|
},
|
|
41
41
|
"exports": {
|
|
42
42
|
".": {
|