@lssm/lib.testing 0.0.0-canary-20251206160926
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 +48 -0
- package/dist/adapters/jest-adapter.d.ts +12 -0
- package/dist/adapters/jest-adapter.js +13 -0
- package/dist/adapters/vitest-adapter.d.ts +12 -0
- package/dist/adapters/vitest-adapter.js +14 -0
- package/dist/generator/assertion-builder.d.ts +11 -0
- package/dist/generator/assertion-builder.js +2 -0
- package/dist/generator/golden-test-generator.d.ts +26 -0
- package/dist/generator/golden-test-generator.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -0
- package/dist/recorder/traffic-recorder.d.ts +39 -0
- package/dist/recorder/traffic-recorder.js +1 -0
- package/dist/types.d.ts +42 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @lssm/lib.testing
|
|
2
|
+
|
|
3
|
+
**Safe regeneration verification** — Capture production traffic and generate golden tests automatically.
|
|
4
|
+
|
|
5
|
+
Golden-test utilities that record real requests/responses and generate runnable test suites. Prove that regenerated code behaves identically to the original.
|
|
6
|
+
|
|
7
|
+
- **TrafficRecorder** captures production requests/responses with sampling + sanitization.
|
|
8
|
+
- **GoldenTestGenerator** converts snapshots into runnable suites.
|
|
9
|
+
- **Adapters** output Vitest or Jest files and helper runners.
|
|
10
|
+
|
|
11
|
+
### Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
TrafficRecorder,
|
|
16
|
+
InMemoryTrafficStore,
|
|
17
|
+
} from '@lssm/lib.testing/recorder';
|
|
18
|
+
|
|
19
|
+
const recorder = new TrafficRecorder({
|
|
20
|
+
store: new InMemoryTrafficStore(),
|
|
21
|
+
sampleRate: 0.01,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await recorder.record({
|
|
25
|
+
operation: { name: 'orders.create', version: 3 },
|
|
26
|
+
input: payload,
|
|
27
|
+
output,
|
|
28
|
+
success: true,
|
|
29
|
+
timestamp: new Date(),
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See `GoldenTestGenerator` for generating suites and CLI in `@lssm/app.contracts-cli`.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GoldenTestCase } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/adapters/jest-adapter.d.ts
|
|
4
|
+
interface JestAdapterOptions {
|
|
5
|
+
suiteName: string;
|
|
6
|
+
cases: GoldenTestCase[];
|
|
7
|
+
runnerImport: string;
|
|
8
|
+
runnerFunction: string;
|
|
9
|
+
}
|
|
10
|
+
declare function generateJestSuite(options: JestAdapterOptions): string;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { JestAdapterOptions, generateJestSuite };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{serialize as e}from"../generator/assertion-builder.js";function t(t){let n=t.cases.map(n=>{let r=e(n.input),i=e(n.metadata??{}),a=`const result = await ${t.runnerFunction}(input${n.id}, metadata${n.id});
|
|
2
|
+
expect(result).toEqual(${e(n.expectedOutput??null)});`,o=`await expect(${t.runnerFunction}(input${n.id}, metadata${n.id})).rejects.toMatchObject(${e(n.expectedError??{message:`expected failure`})});`;return`
|
|
3
|
+
test('${n.name}', async () => {
|
|
4
|
+
const input${n.id} = ${r};
|
|
5
|
+
const metadata${n.id} = ${i};
|
|
6
|
+
${n.success?a:o}
|
|
7
|
+
});`}).join(`
|
|
8
|
+
`);return`
|
|
9
|
+
import { ${t.runnerFunction} } from '${t.runnerImport}';
|
|
10
|
+
|
|
11
|
+
describe('${t.suiteName}', () => {${n}
|
|
12
|
+
});
|
|
13
|
+
`.trim()}export{t as generateJestSuite};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GoldenTestCase } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/adapters/vitest-adapter.d.ts
|
|
4
|
+
interface VitestAdapterOptions {
|
|
5
|
+
suiteName: string;
|
|
6
|
+
cases: GoldenTestCase[];
|
|
7
|
+
runnerImport: string;
|
|
8
|
+
runnerFunction: string;
|
|
9
|
+
}
|
|
10
|
+
declare function generateVitestSuite(options: VitestAdapterOptions): string;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { VitestAdapterOptions, generateVitestSuite };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import{serialize as e}from"../generator/assertion-builder.js";function t(t){let n=t.cases.map(n=>{let r=e(n.input),i=e(n.metadata??{}),a=n.success?[`const result = await ${t.runnerFunction}(input${n.id}, metadata${n.id});`,`expect(result).toEqual(${e(n.expectedOutput??null)});`]:[`await expect(${t.runnerFunction}(input${n.id}, metadata${n.id})).rejects.toMatchObject(${e(n.expectedError??{message:`expected failure`})});`];return`
|
|
2
|
+
it('${n.name}', async () => {
|
|
3
|
+
const input${n.id} = ${r};
|
|
4
|
+
const metadata${n.id} = ${i};
|
|
5
|
+
${a.join(`
|
|
6
|
+
`)}
|
|
7
|
+
});`}).join(`
|
|
8
|
+
`);return`
|
|
9
|
+
import { describe, it, expect } from 'bun:test';
|
|
10
|
+
import { ${t.runnerFunction} } from '${t.runnerImport}';
|
|
11
|
+
|
|
12
|
+
describe('${t.suiteName}', () => {${n}
|
|
13
|
+
});
|
|
14
|
+
`.trim()}export{t as generateVitestSuite};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { GoldenTestCase } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/generator/assertion-builder.d.ts
|
|
4
|
+
interface AssertionContext {
|
|
5
|
+
runnerCall: string;
|
|
6
|
+
caseRef: string;
|
|
7
|
+
}
|
|
8
|
+
declare function buildAssertions(testCase: GoldenTestCase, ctx: AssertionContext): string;
|
|
9
|
+
declare function serialize(value: unknown): string;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { AssertionContext, buildAssertions, serialize };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e,n){return e.success?[`const result = await ${n.runnerCall};`,`expect(result).toEqual(${t(e.expectedOutput??null)});`].join(`
|
|
2
|
+
`):`await expect(${n.runnerCall}).rejects.toMatchObject(${t(e.expectedError??{message:`expected failure`})});`}function t(e){return JSON.stringify(e,(e,t)=>t instanceof Date?t.toISOString():t===void 0?null:t,2)}export{e as buildAssertions,t as serialize};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { GoldenTestCase, TrafficSnapshot } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/generator/golden-test-generator.d.ts
|
|
4
|
+
interface GoldenTestGeneratorOptions {
|
|
5
|
+
suiteName: string;
|
|
6
|
+
runnerImport: string;
|
|
7
|
+
runnerFunction: string;
|
|
8
|
+
framework?: 'vitest' | 'jest';
|
|
9
|
+
serializeMetadata?: (snapshot: TrafficSnapshot) => Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
declare class GoldenTestGenerator {
|
|
12
|
+
private readonly serializeMetadata;
|
|
13
|
+
constructor(serializeMetadata?: GoldenTestGeneratorOptions['serializeMetadata']);
|
|
14
|
+
createCases(snapshots: TrafficSnapshot[]): GoldenTestCase[];
|
|
15
|
+
generate(snapshots: TrafficSnapshot[], options: GoldenTestGeneratorOptions): string;
|
|
16
|
+
}
|
|
17
|
+
type GoldenTestRunner = (input: unknown, metadata?: Record<string, unknown>) => Promise<unknown>;
|
|
18
|
+
interface GoldenTestRunResult {
|
|
19
|
+
caseId: string;
|
|
20
|
+
passed: boolean;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
error?: unknown;
|
|
23
|
+
}
|
|
24
|
+
declare function runGoldenTests(cases: GoldenTestCase[], runner: GoldenTestRunner): Promise<GoldenTestRunResult[]>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { GoldenTestGenerator, GoldenTestGeneratorOptions, GoldenTestRunResult, GoldenTestRunner, runGoldenTests };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{generateVitestSuite as e}from"../adapters/vitest-adapter.js";import{generateJestSuite as t}from"../adapters/jest-adapter.js";import{randomUUID as n}from"node:crypto";import{performance as r}from"node:perf_hooks";var i=class{constructor(e=e=>({tenantId:e.tenantId,userId:e.userId,channel:e.channel})){this.serializeMetadata=e}createCases(e){return e.map((e,t)=>({id:e.id??n(),name:e.success?`case-${t+1}-success`:`case-${t+1}-failure`,input:e.input,expectedOutput:e.output,expectedError:e.error,success:e.success,metadata:this.serializeMetadata?.(e)}))}generate(n,r){let i=this.createCases(n);return r.framework===`jest`?t({suiteName:r.suiteName,cases:i,runnerImport:r.runnerImport,runnerFunction:r.runnerFunction}):e({suiteName:r.suiteName,cases:i,runnerImport:r.runnerImport,runnerFunction:r.runnerFunction})}};async function a(e,t){let n=[];for(let i of e){let e=r.now();try{let a=await t(i.input,i.metadata);if(!i.success){n.push({caseId:i.id,passed:!1,durationMs:r.now()-e,error:Error(`Expected failure but runner resolved`)});continue}let o=JSON.stringify(a)===JSON.stringify(i.expectedOutput??null);n.push({caseId:i.id,passed:o,durationMs:r.now()-e,error:o?void 0:{expected:i.expectedOutput,received:a}})}catch(t){let a=r.now()-e;i.success?n.push({caseId:i.id,passed:!1,durationMs:a,error:t}):n.push({caseId:i.id,passed:!0,durationMs:a})}}return n}export{i as GoldenTestGenerator,a as runGoldenTests};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { GoldenTestCase, RuntimeContract, TrafficSnapshot } from "./types.js";
|
|
2
|
+
import { InMemoryTrafficStore, RecordOperationInput, TrafficRecorder, TrafficRecorderOptions, TrafficStore } from "./recorder/traffic-recorder.js";
|
|
3
|
+
import { GoldenTestGenerator, GoldenTestGeneratorOptions, GoldenTestRunResult, GoldenTestRunner, runGoldenTests } from "./generator/golden-test-generator.js";
|
|
4
|
+
import { AssertionContext, buildAssertions, serialize } from "./generator/assertion-builder.js";
|
|
5
|
+
import { VitestAdapterOptions, generateVitestSuite } from "./adapters/vitest-adapter.js";
|
|
6
|
+
import { JestAdapterOptions, generateJestSuite } from "./adapters/jest-adapter.js";
|
|
7
|
+
export { AssertionContext, GoldenTestCase, GoldenTestGenerator, GoldenTestGeneratorOptions, GoldenTestRunResult, GoldenTestRunner, InMemoryTrafficStore, JestAdapterOptions, RecordOperationInput, RuntimeContract, TrafficRecorder, TrafficRecorderOptions, TrafficSnapshot, TrafficStore, VitestAdapterOptions, buildAssertions, generateJestSuite, generateVitestSuite, runGoldenTests, serialize };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{InMemoryTrafficStore as e,TrafficRecorder as t}from"./recorder/traffic-recorder.js";import{buildAssertions as n,serialize as r}from"./generator/assertion-builder.js";import{generateVitestSuite as i}from"./adapters/vitest-adapter.js";import{generateJestSuite as a}from"./adapters/jest-adapter.js";import{GoldenTestGenerator as o,runGoldenTests as s}from"./generator/golden-test-generator.js";export{o as GoldenTestGenerator,e as InMemoryTrafficStore,t as TrafficRecorder,n as buildAssertions,a as generateJestSuite,i as generateVitestSuite,s as runGoldenTests,r as serialize};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TrafficSnapshot } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/recorder/traffic-recorder.d.ts
|
|
4
|
+
interface TrafficStore {
|
|
5
|
+
save(snapshot: TrafficSnapshot): Promise<void>;
|
|
6
|
+
list(operation?: TrafficSnapshot['operation']['name']): Promise<TrafficSnapshot[]>;
|
|
7
|
+
}
|
|
8
|
+
declare class InMemoryTrafficStore implements TrafficStore {
|
|
9
|
+
private readonly items;
|
|
10
|
+
save(snapshot: TrafficSnapshot): Promise<void>;
|
|
11
|
+
list(operation?: string): Promise<TrafficSnapshot[]>;
|
|
12
|
+
}
|
|
13
|
+
interface TrafficRecorderOptions {
|
|
14
|
+
store: TrafficStore;
|
|
15
|
+
sampleRate?: number;
|
|
16
|
+
sanitize?: (snapshot: TrafficSnapshot) => TrafficSnapshot;
|
|
17
|
+
}
|
|
18
|
+
interface RecordOperationInput {
|
|
19
|
+
operation: TrafficSnapshot['operation'];
|
|
20
|
+
input: unknown;
|
|
21
|
+
output?: unknown;
|
|
22
|
+
error?: TrafficSnapshot['error'];
|
|
23
|
+
success: boolean;
|
|
24
|
+
durationMs?: number;
|
|
25
|
+
tenantId?: string;
|
|
26
|
+
userId?: string;
|
|
27
|
+
channel?: string;
|
|
28
|
+
metadata?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
declare class TrafficRecorder {
|
|
31
|
+
private readonly store;
|
|
32
|
+
private readonly sampleRate;
|
|
33
|
+
private readonly sanitize?;
|
|
34
|
+
constructor(options: TrafficRecorderOptions);
|
|
35
|
+
record(input: RecordOperationInput): Promise<void>;
|
|
36
|
+
private shouldSample;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { InMemoryTrafficStore, RecordOperationInput, TrafficRecorder, TrafficRecorderOptions, TrafficStore };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUID as e}from"node:crypto";var t=class{items=[];async save(e){this.items.push(e)}async list(e){return e?this.items.filter(t=>t.operation.name===e):[...this.items]}},n=class{store;sampleRate;sanitize;constructor(e){this.store=e.store,this.sampleRate=e.sampleRate??1,this.sanitize=e.sanitize}async record(t){if(!this.shouldSample())return;let n={id:e(),operation:t.operation,input:r(t.input),output:r(t.output),error:t.error?r(t.error):void 0,success:t.success,timestamp:new Date,durationMs:t.durationMs,tenantId:t.tenantId,userId:t.userId,channel:t.channel,metadata:t.metadata},i=this.sanitize?this.sanitize(n):n;await this.store.save(i)}shouldSample(){return this.sampleRate>=1?!0:Math.random()<=this.sampleRate}};function r(e){if(e==null)return e??void 0;try{let t=globalThis.structuredClone;return typeof t==`function`?t(e):JSON.parse(JSON.stringify(e))}catch{return}}export{t as InMemoryTrafficStore,n as TrafficRecorder};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ContractSpec, ResourceRefDescriptor } from "@lssm/lib.contracts";
|
|
2
|
+
import { AnySchemaModel } from "@lssm/lib.schema";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
interface TrafficSnapshot {
|
|
6
|
+
id: string;
|
|
7
|
+
operation: {
|
|
8
|
+
name: string;
|
|
9
|
+
version: number;
|
|
10
|
+
};
|
|
11
|
+
input: unknown;
|
|
12
|
+
output?: unknown;
|
|
13
|
+
error?: {
|
|
14
|
+
name?: string;
|
|
15
|
+
message?: string;
|
|
16
|
+
stack?: string;
|
|
17
|
+
code?: string;
|
|
18
|
+
};
|
|
19
|
+
success: boolean;
|
|
20
|
+
timestamp: Date;
|
|
21
|
+
durationMs?: number;
|
|
22
|
+
tenantId?: string;
|
|
23
|
+
userId?: string;
|
|
24
|
+
channel?: string;
|
|
25
|
+
metadata?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
interface GoldenTestCase {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
input: unknown;
|
|
31
|
+
expectedOutput?: unknown;
|
|
32
|
+
expectedError?: {
|
|
33
|
+
name?: string;
|
|
34
|
+
message?: string;
|
|
35
|
+
code?: string;
|
|
36
|
+
};
|
|
37
|
+
success: boolean;
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
type RuntimeContract = ContractSpec<AnySchemaModel, AnySchemaModel | ResourceRefDescriptor<boolean>>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { GoldenTestCase, RuntimeContract, TrafficSnapshot };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lssm/lib.testing",
|
|
3
|
+
"version": "0.0.0-canary-20251206160926",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
14
|
+
"build": "bun build:bundle && bun build:types",
|
|
15
|
+
"build:bundle": "tsdown",
|
|
16
|
+
"build:types": "tsc --noEmit",
|
|
17
|
+
"dev": "bun build:bundle --watch",
|
|
18
|
+
"clean": "rimraf dist .turbo",
|
|
19
|
+
"lint": "bun lint:fix",
|
|
20
|
+
"lint:fix": "eslint src --fix",
|
|
21
|
+
"lint:check": "eslint src",
|
|
22
|
+
"test": "bun run"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@lssm/lib.contracts": "workspace:*"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@lssm/tool.tsdown": "workspace:*",
|
|
29
|
+
"@lssm/tool.typescript": "workspace:*",
|
|
30
|
+
"tsdown": "^0.17.0",
|
|
31
|
+
"typescript": "^5.9.3"
|
|
32
|
+
},
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./dist/index.js",
|
|
35
|
+
"./*": "./*"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|