@app-studio/qa-harness 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # @app-studio/qa-harness
2
+
3
+ The shape of a test harness, and a record/replay transport whose missing
4
+ fixtures always fail.
5
+
6
+ ## The harness
7
+
8
+ Two small types and a helper, deliberately:
9
+
10
+ ```ts
11
+ import { defineActors, type TestApp } from '@app-studio/qa-harness';
12
+
13
+ export async function createTestApp(): Promise<TestApp<Client>> { /* your bootstrap */ }
14
+
15
+ const actors = defineActors(app, (app) => ({
16
+ signUp: (email: string) => /* … */,
17
+ createWorkspace: (owner: Actor) => /* … */,
18
+ addMember: (workspace: string, actor: Actor, role: Role) => /* … */,
19
+ }));
20
+ ```
21
+
22
+ What a harness must *be* is a rule, not a library, and this package cannot
23
+ enforce it — only say it clearly:
24
+
25
+ **The application under test has to be the application that ships.** Same
26
+ composition, same HTTP configuration, same startup. A hand-assembled test app
27
+ validates something that does not exist in production, and the resulting green
28
+ suite is a statement about a program nobody runs. Write the factory against your
29
+ real bootstrap.
30
+
31
+ **Prefer listening on a real port** if the application attaches anything to the
32
+ HTTP server — WebSockets, most commonly. On a server that was never started, the
33
+ socket library's takeover of the request listeners races the framework's own
34
+ handler, and the occasional request falls through as a 404 on a route that
35
+ plainly exists. That failure is intermittent, which is the worst kind.
36
+
37
+ **A fresh client per actor.** Sharing one client across two roles is how an
38
+ authorization test quietly starts proving nothing.
39
+
40
+ Actors are what make authorization testing cheap enough to actually do. When "an
41
+ owner, a member and an outsider" costs three lines, every endpoint gets its
42
+ cross-tenant probe; when it costs thirty, the probe is what gets skipped — and
43
+ cross-tenant access is the single most common real-world API defect.
44
+
45
+ ## Record and replay
46
+
47
+ For anything whose answer is neither stable nor free: a model, a payment
48
+ provider, a third-party search.
49
+
50
+ ```ts
51
+ import { assertReplayBootSafety, createReplayTransport } from '@app-studio/qa-harness/replay';
52
+
53
+ assertReplayBootSafety({ mode: process.env.AI_MODE ?? 'live' });
54
+
55
+ const ask = createReplayTransport<Request, Response>({
56
+ mode: process.env.AI_MODE as ReplayMode,
57
+ dir: 'tests/fixtures/ai',
58
+ live: (request) => realProvider.complete(request),
59
+ source: 'gpt-4o-2026-01',
60
+ });
61
+ ```
62
+
63
+ Three rules, each the fix for a specific way this goes wrong.
64
+
65
+ **A missing fixture always fails.** Never a fallback, never a default answer. A
66
+ transport that quietly substitutes something plausible turns "this test no
67
+ longer asks the question it was written for" into a green run — the exact
68
+ failure the fixtures exist to prevent.
69
+
70
+ **The key is a digest of the request.** Not a filename somebody chose, and not
71
+ the test's name. Change what you ask and the fixture stops matching, so a suite
72
+ cannot keep replaying the answer to a question it no longer asks. Key order does
73
+ not affect the digest, so refactoring the request object does not invalidate
74
+ every recording.
75
+
76
+ **A fixture says where it came from.** A recording captured against a version of
77
+ a service that has since changed is indistinguishable from a fresh one, and
78
+ tests replaying it assert last year's behaviour with total confidence. The
79
+ provenance block is required on read; `qa-guard` denies committing a fixture
80
+ without one.
81
+
82
+ ### The boot guard
83
+
84
+ `assertReplayBootSafety` refuses to start in `live` or `record` mode under
85
+ `NODE_ENV=test`. A guard at the transport is too late and too easy to bypass;
86
+ the point is that a process which *could* reach the live service never starts
87
+ inside a test run in the first place — a run that reaches a real service is
88
+ slow, costs money, and is not reproducible.
89
+
90
+ ### Housekeeping
91
+
92
+ `unusedFixtures(dir, usedDigests)` lists recordings nothing asked for. Worth
93
+ checking after a recording session: a fixture nobody replays is either dead
94
+ weight or evidence that a code path stopped being exercised, and both are things
95
+ to know before they are a year old.
96
+
97
+ ## Requirements
98
+
99
+ Node.js 20 or newer. No runtime dependencies beyond `@app-studio/qa-core`.
package/dist/app.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The shape of a test harness.
3
+ *
4
+ * Deliberately thin. What the harness must *be* is a rule, not a library: the
5
+ * application under test has to be the application that ships — the same
6
+ * composition, the same HTTP configuration, the same startup — because a
7
+ * hand-assembled test app validates something that does not exist in
8
+ * production. That rule cannot be enforced by a type, only by writing the
9
+ * factory against the real bootstrap; what this package can do is give the
10
+ * factory a common shape so actors, resets and fixtures compose across
11
+ * projects.
12
+ *
13
+ * One detail is worth stating because it is invisible until it bites: prefer
14
+ * *listening* on a real port over an in-process handler if the application
15
+ * attaches anything to the HTTP server — WebSockets, most commonly. On a server
16
+ * that was never started, the socket library's takeover of the request
17
+ * listeners races the framework's own handler, and the occasional request falls
18
+ * through as a 404 on a route that plainly exists.
19
+ */
20
+ export interface TestApp<Client = unknown> {
21
+ /** Where the application is listening, e.g. `http://127.0.0.1:53314`. */
22
+ baseUrl: string;
23
+ /**
24
+ * A fresh client per call — one per actor, so their sessions stay separate.
25
+ * Sharing one client across two roles is how an authorization test quietly
26
+ * starts proving nothing.
27
+ */
28
+ client(): Client;
29
+ close(): Promise<void>;
30
+ }
31
+ export type AppFactory<Options, Client> = (options?: Options) => Promise<TestApp<Client>>;
32
+ /**
33
+ * Binds a set of actor helpers to a running application.
34
+ *
35
+ * Actors are what make authorization testing cheap enough to actually do. When
36
+ * "an owner, a member and an outsider" costs three lines, every endpoint gets
37
+ * its cross-tenant probe; when it costs thirty, the probe is the thing that
38
+ * gets skipped — and cross-tenant access is the single most common real-world
39
+ * API defect.
40
+ */
41
+ export declare function defineActors<Client, Actors extends Record<string, unknown>>(app: TestApp<Client>, build: (app: TestApp<Client>) => Actors): Actors;
42
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,OAAO,CAAC,MAAM,GAAG,OAAO;IACvC,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,IAAI,MAAM,CAAC;IACjB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAE1F;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzE,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,EACpB,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,MAAM,GACtC,MAAM,CAER"}
package/dist/app.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The shape of a test harness.
3
+ *
4
+ * Deliberately thin. What the harness must *be* is a rule, not a library: the
5
+ * application under test has to be the application that ships — the same
6
+ * composition, the same HTTP configuration, the same startup — because a
7
+ * hand-assembled test app validates something that does not exist in
8
+ * production. That rule cannot be enforced by a type, only by writing the
9
+ * factory against the real bootstrap; what this package can do is give the
10
+ * factory a common shape so actors, resets and fixtures compose across
11
+ * projects.
12
+ *
13
+ * One detail is worth stating because it is invisible until it bites: prefer
14
+ * *listening* on a real port over an in-process handler if the application
15
+ * attaches anything to the HTTP server — WebSockets, most commonly. On a server
16
+ * that was never started, the socket library's takeover of the request
17
+ * listeners races the framework's own handler, and the occasional request falls
18
+ * through as a 404 on a route that plainly exists.
19
+ */
20
+ /**
21
+ * Binds a set of actor helpers to a running application.
22
+ *
23
+ * Actors are what make authorization testing cheap enough to actually do. When
24
+ * "an owner, a member and an outsider" costs three lines, every endpoint gets
25
+ * its cross-tenant probe; when it costs thirty, the probe is the thing that
26
+ * gets skipped — and cross-tenant access is the single most common real-world
27
+ * API defect.
28
+ */
29
+ export function defineActors(app, build) {
30
+ return build(app);
31
+ }
32
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAgBH;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAoB,EACpB,KAAuC;IAEvC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC;AACpB,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Factories for test data.
3
+ *
4
+ * The problem they solve is not typing convenience. It is that hand-written
5
+ * fixtures collide: two tests both build "a user" with the same email, they
6
+ * pass alone and fail together, and the failure looks like a bug in the
7
+ * application. Every value that has to be unique comes from a sequence here, so
8
+ * the collision cannot happen.
9
+ *
10
+ * The second thing they solve is intent. `buildUser({ email: 'a@b.c', name: 'x',
11
+ * role: 'ADMIN', workspaceId: 'w1', … })` buries the one field the test is
12
+ * about in nine that it is not. A factory states the default world once and lets
13
+ * each test override only what it is testing — so the override *is* the
14
+ * documentation of what the test is about.
15
+ */
16
+ export interface FactoryContext {
17
+ /** Increments on every build. Use it for anything that must be unique. */
18
+ sequence: number;
19
+ }
20
+ export type Trait<T> = Partial<T> | ((context: FactoryContext) => Partial<T>);
21
+ export interface FactoryDefinition<T, TraitName extends string = string> {
22
+ /** The ordinary case — what this thing looks like when nothing special is going on. */
23
+ build(context: FactoryContext): T;
24
+ /**
25
+ * Named variations, e.g. `expired`, `admin`, `deleted`.
26
+ *
27
+ * A trait is worth having when a variation recurs across tests; a one-off
28
+ * difference is better expressed as an override at the call site, where the
29
+ * reader can see it.
30
+ */
31
+ traits?: Record<TraitName, Trait<T>>;
32
+ }
33
+ export interface Factory<T, TraitName extends string = string> {
34
+ build(overrides?: Partial<T>): T;
35
+ buildList(count: number, overrides?: Partial<T> | ((index: number) => Partial<T>)): T[];
36
+ /** A factory with these traits applied, left to right. Does not mutate this one. */
37
+ with(...traits: TraitName[]): Factory<T, TraitName>;
38
+ /**
39
+ * Resets the sequence.
40
+ *
41
+ * Rarely what you want: a suite that needs the sequence reset to pass is a
42
+ * suite asserting on generated values, which will break the next time
43
+ * anything else builds one first. Provided for the case where a snapshot
44
+ * genuinely has to be stable.
45
+ */
46
+ reset(): void;
47
+ }
48
+ export declare function defineFactory<T, TraitName extends string = string>(definition: FactoryDefinition<T, TraitName>, applied?: TraitName[]): Factory<T, TraitName>;
49
+ /**
50
+ * Pairs a factory with something that persists what it builds.
51
+ *
52
+ * Kept separate from `defineFactory` so the pure half stays usable in a unit
53
+ * test with no database, and so persistence goes through the application's own
54
+ * services rather than an insert — the same rule as the seeds. A record written
55
+ * straight to the database has none of the side effects the real path produces,
56
+ * and every test that then relies on those is testing a world the application
57
+ * cannot make.
58
+ */
59
+ export declare function persisted<T, Persisted = T, TraitName extends string = string>(factory: Factory<T, TraitName>, persist: (attributes: T) => Promise<Persisted>): {
60
+ build: Factory<T, TraitName>['build'];
61
+ create(overrides?: Partial<T>): Promise<Persisted>;
62
+ createList(count: number, overrides?: Partial<T> | ((index: number) => Partial<T>)): Promise<Persisted[]>;
63
+ };
64
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAE9E,MAAM,WAAW,iBAAiB,CAAC,CAAC,EAAE,SAAS,SAAS,MAAM,GAAG,MAAM;IACrE,uFAAuF;IACvF,KAAK,CAAC,OAAO,EAAE,cAAc,GAAG,CAAC,CAAC;IAClC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,OAAO,CAAC,CAAC,EAAE,SAAS,SAAS,MAAM,GAAG,MAAM;IAC3D,KAAK,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACjC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IACxF,oFAAoF;IACpF,IAAI,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACpD;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,SAAS,SAAS,MAAM,GAAG,MAAM,EAChE,UAAU,EAAE,iBAAiB,CAAC,CAAC,EAAE,SAAS,CAAC,EAC3C,OAAO,GAAE,SAAS,EAAO,GACxB,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,CAyCvB;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,EAAE,SAAS,SAAS,MAAM,GAAG,MAAM,EAC3E,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,EAC9B,OAAO,EAAE,CAAC,UAAU,EAAE,CAAC,KAAK,OAAO,CAAC,SAAS,CAAC,GAC7C;IACD,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACnD,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;CAC3G,CAcA"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Factories for test data.
3
+ *
4
+ * The problem they solve is not typing convenience. It is that hand-written
5
+ * fixtures collide: two tests both build "a user" with the same email, they
6
+ * pass alone and fail together, and the failure looks like a bug in the
7
+ * application. Every value that has to be unique comes from a sequence here, so
8
+ * the collision cannot happen.
9
+ *
10
+ * The second thing they solve is intent. `buildUser({ email: 'a@b.c', name: 'x',
11
+ * role: 'ADMIN', workspaceId: 'w1', … })` buries the one field the test is
12
+ * about in nine that it is not. A factory states the default world once and lets
13
+ * each test override only what it is testing — so the override *is* the
14
+ * documentation of what the test is about.
15
+ */
16
+ export function defineFactory(definition, applied = []) {
17
+ let sequence = 0;
18
+ const factory = {
19
+ build(overrides = {}) {
20
+ sequence += 1;
21
+ const context = { sequence };
22
+ let value = definition.build(context);
23
+ for (const name of applied) {
24
+ const trait = definition.traits?.[name];
25
+ if (trait === undefined) {
26
+ throw new Error(`This factory has no trait "${name}". Declared: ${Object.keys(definition.traits ?? {}).join(', ') || '(none)'}.`);
27
+ }
28
+ value = { ...value, ...(typeof trait === 'function' ? trait(context) : trait) };
29
+ }
30
+ return { ...value, ...overrides };
31
+ },
32
+ buildList(count, overrides) {
33
+ return Array.from({ length: count }, (_unused, index) => factory.build(typeof overrides === 'function' ? overrides(index) : overrides));
34
+ },
35
+ with(...traits) {
36
+ // A new factory rather than a mutation: `userFactory.with('admin')` must
37
+ // not turn every later `userFactory.build()` into an administrator, which
38
+ // is the kind of action-at-a-distance that costs an afternoon.
39
+ return defineFactory(definition, [...applied, ...traits]);
40
+ },
41
+ reset() {
42
+ sequence = 0;
43
+ },
44
+ };
45
+ return factory;
46
+ }
47
+ /**
48
+ * Pairs a factory with something that persists what it builds.
49
+ *
50
+ * Kept separate from `defineFactory` so the pure half stays usable in a unit
51
+ * test with no database, and so persistence goes through the application's own
52
+ * services rather than an insert — the same rule as the seeds. A record written
53
+ * straight to the database has none of the side effects the real path produces,
54
+ * and every test that then relies on those is testing a world the application
55
+ * cannot make.
56
+ */
57
+ export function persisted(factory, persist) {
58
+ return {
59
+ build: (overrides) => factory.build(overrides),
60
+ create: async (overrides) => persist(factory.build(overrides)),
61
+ createList: async (count, overrides) => {
62
+ const created = [];
63
+ // Sequentially: concurrent creates against one database routinely deadlock
64
+ // on the same unique index, and the failure reads as flakiness.
65
+ for (const attributes of factory.buildList(count, overrides)) {
66
+ created.push(await persist(attributes));
67
+ }
68
+ return created;
69
+ },
70
+ };
71
+ }
72
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAsCH,MAAM,UAAU,aAAa,CAC3B,UAA2C,EAC3C,UAAuB,EAAE;IAEzB,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,MAAM,OAAO,GAA0B;QACrC,KAAK,CAAC,SAAS,GAAG,EAAE;YAClB,QAAQ,IAAI,CAAC,CAAC;YACd,MAAM,OAAO,GAAmB,EAAE,QAAQ,EAAE,CAAC;YAC7C,IAAI,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAEtC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;gBACxC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBACxB,MAAM,IAAI,KAAK,CACb,8BAA8B,IAAI,gBAAgB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,GAAG,CACjH,CAAC;gBACJ,CAAC;gBACD,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,CAAC,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YAClF,CAAC;YAED,OAAO,EAAE,GAAG,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;QACpC,CAAC;QAED,SAAS,CAAC,KAAK,EAAE,SAAS;YACxB,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CACtD,OAAO,CAAC,KAAK,CAAC,OAAO,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAC9E,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,GAAG,MAAM;YACZ,yEAAyE;YACzE,0EAA0E;YAC1E,+DAA+D;YAC/D,OAAO,aAAa,CAAC,UAAU,EAAE,CAAC,GAAG,OAAO,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC;QAC5D,CAAC;QAED,KAAK;YACH,QAAQ,GAAG,CAAC,CAAC;QACf,CAAC;KACF,CAAC;IAEF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CACvB,OAA8B,EAC9B,OAA8C;IAM9C,OAAO;QACL,KAAK,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC;QAC9C,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC9D,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;YACrC,MAAM,OAAO,GAAgB,EAAE,CAAC;YAChC,2EAA2E;YAC3E,gEAAgE;YAChE,KAAK,MAAM,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;gBAC7D,OAAO,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YAC1C,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { defineActors, type AppFactory, type TestApp } from './app.js';
2
+ export { MissingFixtureError, ReplayBootError, assertReplayBootSafety, createReplayTransport, digestOf, unusedFixtures, type Fixture, type Provenance, type ReplayMode, type ReplayTransportOptions, } from './replay/index.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAEvE,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,QAAQ,EACR,cAAc,EACd,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,sBAAsB,GAC5B,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { defineActors } from './app.js';
2
+ export { MissingFixtureError, ReplayBootError, assertReplayBootSafety, createReplayTransport, digestOf, unusedFixtures, } from './replay/index.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAiC,MAAM,UAAU,CAAC;AAEvE,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,QAAQ,EACR,cAAc,GAKf,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,67 @@
1
+ import type { TestApp } from '../app.js';
2
+ /**
3
+ * The harness for a Nest application, with the one detail nobody gets right twice.
4
+ *
5
+ * Everything here is the same in any Nest repository: build a testing module,
6
+ * apply the overrides, create the application, hand it to the host's own HTTP
7
+ * configuration, start it, hand back a client factory and a close. What is *not*
8
+ * the same — which modules compose the app, how it is configured, what a client
9
+ * is — is passed in.
10
+ *
11
+ * **It listens on a real port rather than calling `init`.** That is the detail.
12
+ * If the application attaches anything to the HTTP server — a WebSocket gateway,
13
+ * most commonly — then on a server that was never started, the socket library's
14
+ * takeover of the request listeners races the framework's own handler, and the
15
+ * occasional request falls through as a 404 on a route that plainly exists. It
16
+ * looks like flakiness, it reproduces once an hour, and it costs an afternoon
17
+ * every time somebody meets it. Both repositories this was drawn from had found
18
+ * it and written the workaround in a comment; here it is not a comment.
19
+ *
20
+ * Typed structurally, so this package does not resolve its own copy of Nest —
21
+ * two Nest instances is two injection containers that cannot see each other's
22
+ * providers.
23
+ */
24
+ export interface NestTestingBuilder {
25
+ overrideProvider(token: unknown): {
26
+ useValue(value: unknown): NestTestingBuilder;
27
+ };
28
+ compile(): Promise<{
29
+ createNestApplication(): NestApplication;
30
+ }>;
31
+ }
32
+ export interface NestApplication {
33
+ listen(port: number): Promise<unknown>;
34
+ getHttpServer(): unknown;
35
+ getUrl?(): Promise<string>;
36
+ close(): Promise<void>;
37
+ get<T>(token: unknown): T;
38
+ }
39
+ export interface NestHarnessOptions<Client> {
40
+ /**
41
+ * Builds the testing module. Given the modules to import, so a caller can
42
+ * leave one out and prove the application still composes without it.
43
+ */
44
+ createBuilder(imports: unknown[]): NestTestingBuilder;
45
+ /** The modules that make the application, in the order production uses. */
46
+ imports(): unknown[];
47
+ /**
48
+ * The host's own HTTP configuration — pipes, filters, cookie parsing.
49
+ *
50
+ * Required rather than optional, and unapologetically: an application
51
+ * assembled differently for tests validates something that does not ship.
52
+ */
53
+ configure(app: NestApplication): void | Promise<void>;
54
+ /** A fresh client per call, one per actor, so sessions stay separate. */
55
+ client(server: unknown): Client;
56
+ /** Providers to swap at true external boundaries only. */
57
+ overrides?: Array<{
58
+ token: unknown;
59
+ value: unknown;
60
+ }>;
61
+ }
62
+ export interface NestTestApp<Client> extends TestApp<Client> {
63
+ /** The running application, for the rare test that needs a provider. */
64
+ app: NestApplication;
65
+ }
66
+ export declare function createNestTestApp<Client>(options: NestHarnessOptions<Client>): Promise<NestTestApp<Client>>;
67
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/nest/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAAA;KAAE,CAAC;IACnF,OAAO,IAAI,OAAO,CAAC;QAAE,qBAAqB,IAAI,eAAe,CAAA;KAAE,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,aAAa,IAAI,OAAO,CAAC;IACzB,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB,CAAC,MAAM;IACxC;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,kBAAkB,CAAC;IACtD,2EAA2E;IAC3E,OAAO,IAAI,OAAO,EAAE,CAAC;IACrB;;;;;OAKG;IACH,SAAS,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,yEAAyE;IACzE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;IAChC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,WAAW,CAAC,MAAM,CAAE,SAAQ,OAAO,CAAC,MAAM,CAAC;IAC1D,wEAAwE;IACxE,GAAG,EAAE,eAAe,CAAC;CACtB;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,kBAAkB,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAuBjH"}
@@ -0,0 +1,19 @@
1
+ export async function createNestTestApp(options) {
2
+ let builder = options.createBuilder(options.imports());
3
+ for (const { token, value } of options.overrides ?? []) {
4
+ builder = builder.overrideProvider(token).useValue(value);
5
+ }
6
+ const moduleRef = await builder.compile();
7
+ const app = moduleRef.createNestApplication();
8
+ await options.configure(app);
9
+ // Port zero: the operating system picks a free one, so suites never collide.
10
+ await app.listen(0);
11
+ const baseUrl = app.getUrl === undefined ? '' : await app.getUrl();
12
+ return {
13
+ app,
14
+ baseUrl,
15
+ client: () => options.client(app.getHttpServer()),
16
+ close: () => app.close(),
17
+ };
18
+ }
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/nest/index.ts"],"names":[],"mappings":"AAgEA,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAS,OAAmC;IACjF,IAAI,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAEvD,KAAK,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,OAAO,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC;QACvD,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5D,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IAC1C,MAAM,GAAG,GAAG,SAAS,CAAC,qBAAqB,EAAE,CAAC;IAE9C,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAE7B,6EAA6E;IAC7E,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAEpB,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC;IAEnE,OAAO;QACL,GAAG;QACH,OAAO;QACP,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;QACjD,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE;KACzB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Record and replay for non-deterministic services — a model, a payment
3
+ * provider, anything whose answer is neither stable nor free.
4
+ *
5
+ * Three rules, each the fix for a specific way this goes wrong.
6
+ *
7
+ * **A missing fixture always fails.** Never a fallback, never a default answer.
8
+ * A transport that quietly substitutes something plausible turns "this test no
9
+ * longer asks the question it was written for" into a green run — the exact
10
+ * failure the fixtures exist to prevent.
11
+ *
12
+ * **The key is a digest of the request.** Not a filename somebody chose, and
13
+ * not the test's name. Change what you ask and the fixture stops matching, so a
14
+ * suite cannot keep replaying the answer to a question it no longer asks.
15
+ *
16
+ * **A fixture says where it came from.** A recording captured against a version
17
+ * of a service that has since changed is indistinguishable from a fresh one,
18
+ * and tests replaying it assert last year's behaviour with total confidence.
19
+ */
20
+ export type ReplayMode = 'live' | 'record' | 'replay';
21
+ export interface Provenance {
22
+ /** What produced it — a model name, a provider, a version. */
23
+ source: string;
24
+ recordedAt: string;
25
+ requestDigest: string;
26
+ /** Anything else worth knowing when this is read a year from now. */
27
+ notes?: string;
28
+ }
29
+ export interface Fixture<Response> {
30
+ provenance: Provenance;
31
+ response: Response;
32
+ }
33
+ export declare class MissingFixtureError extends Error {
34
+ readonly digest: string;
35
+ readonly dir: string;
36
+ readonly known: string[];
37
+ constructor(digest: string, dir: string, known: string[]);
38
+ }
39
+ export declare class ReplayBootError extends Error {
40
+ constructor(mode: string);
41
+ }
42
+ /**
43
+ * Call this where the application boots.
44
+ *
45
+ * A guard at the transport is too late and too easy to bypass; the point is
46
+ * that a process which *could* reach the live service never starts inside a
47
+ * test run in the first place.
48
+ */
49
+ export declare function assertReplayBootSafety(options: {
50
+ mode: string;
51
+ nodeEnv?: string | undefined;
52
+ }): void;
53
+ /** Stable digest of anything JSON-serialisable, with object keys sorted. */
54
+ export declare function digestOf(request: unknown): string;
55
+ export interface ReplayTransportOptions<Request, Response> {
56
+ mode: ReplayMode;
57
+ /** Where fixtures live, absolute or relative to the process. */
58
+ dir: string;
59
+ /** Overrides the default digest — use it to drop fields that legitimately vary. */
60
+ digest?: (request: Request) => string;
61
+ /** The real service. Required for `live` and `record`. */
62
+ live?: (request: Request) => Promise<Response>;
63
+ /** What to write into a new recording's provenance. */
64
+ source?: string;
65
+ now?: () => Date;
66
+ }
67
+ export declare function createReplayTransport<Request, Response>(options: ReplayTransportOptions<Request, Response>): (request: Request) => Promise<Response>;
68
+ /**
69
+ * Fixtures in `dir` that nothing asked for during a run.
70
+ *
71
+ * Worth reporting after a recording session: a fixture nobody replays is either
72
+ * dead weight or evidence that a code path stopped being exercised, and both
73
+ * are things you want to know before they are a year old.
74
+ */
75
+ export declare function unusedFixtures(dir: string, usedDigests: Iterable<string>): string[];
76
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/replay/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,OAAO,CAAC,QAAQ;IAC/B,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAE1C,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE;gBAFf,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EAAE;CAa3B;AAED,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,IAAI,EAAE,MAAM;CAQzB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAAG,IAAI,CAKpG;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAEjD;AAcD,MAAM,WAAW,sBAAsB,CAAC,OAAO,EAAE,QAAQ;IACvD,IAAI,EAAE,UAAU,CAAC;IACjB,gEAAgE;IAChE,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACtC,0DAA0D;IAC1D,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/C,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,QAAQ,EACrD,OAAO,EAAE,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC,GACjD,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA0CzC;AAaD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAGnF"}
@@ -0,0 +1,116 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export class MissingFixtureError extends Error {
5
+ digest;
6
+ dir;
7
+ known;
8
+ constructor(digest, dir, known) {
9
+ super(`No recorded answer for request ${digest} in ${dir}.\n` +
10
+ 'This is a failure, not a reason to fall back: an invented answer would let a test keep passing after ' +
11
+ 'it stopped asking the question it was written for.\n' +
12
+ (known.length === 0
13
+ ? 'Nothing has been recorded here yet. Run with mode "record" against the real service.'
14
+ : `Recorded here: ${known.slice(0, 5).join(', ')}${known.length > 5 ? `, +${known.length - 5} more` : ''}. ` +
15
+ 'If the request changed on purpose, re-record it; the old fixture answers a question nobody asks any more.'));
16
+ this.digest = digest;
17
+ this.dir = dir;
18
+ this.known = known;
19
+ this.name = 'MissingFixtureError';
20
+ }
21
+ }
22
+ export class ReplayBootError extends Error {
23
+ constructor(mode) {
24
+ super(`Refusing to start in "${mode}" mode under NODE_ENV=test. A test run that reaches a real service is ` +
25
+ 'slow, costs money, and — worse — is not reproducible: the suite passes or fails on something outside ' +
26
+ 'the repository. Use mode "replay", or unset NODE_ENV if this really is a recording session.');
27
+ this.name = 'ReplayBootError';
28
+ }
29
+ }
30
+ /**
31
+ * Call this where the application boots.
32
+ *
33
+ * A guard at the transport is too late and too easy to bypass; the point is
34
+ * that a process which *could* reach the live service never starts inside a
35
+ * test run in the first place.
36
+ */
37
+ export function assertReplayBootSafety(options) {
38
+ const nodeEnv = options.nodeEnv ?? process.env['NODE_ENV'];
39
+ if (nodeEnv !== 'test')
40
+ return;
41
+ if (options.mode === 'replay')
42
+ return;
43
+ throw new ReplayBootError(options.mode);
44
+ }
45
+ /** Stable digest of anything JSON-serialisable, with object keys sorted. */
46
+ export function digestOf(request) {
47
+ return createHash('sha256').update(canonicalise(request)).digest('hex').slice(0, 16);
48
+ }
49
+ function canonicalise(value) {
50
+ if (value === null || typeof value !== 'object')
51
+ return JSON.stringify(value) ?? 'null';
52
+ if (Array.isArray(value))
53
+ return `[${value.map(canonicalise).join(',')}]`;
54
+ const entries = Object.entries(value)
55
+ .filter(([, entry]) => entry !== undefined)
56
+ .sort(([a], [b]) => a.localeCompare(b))
57
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalise(entry)}`);
58
+ return `{${entries.join(',')}}`;
59
+ }
60
+ export function createReplayTransport(options) {
61
+ const digestOfRequest = options.digest ?? ((request) => digestOf(request));
62
+ return async function transport(request) {
63
+ const digest = digestOfRequest(request);
64
+ const path = join(options.dir, `${digest}.json`);
65
+ if (options.mode === 'replay') {
66
+ if (!existsSync(path))
67
+ throw new MissingFixtureError(digest, options.dir, recordedDigests(options.dir));
68
+ const fixture = JSON.parse(readFileSync(path, 'utf8'));
69
+ if (fixture.provenance === undefined) {
70
+ throw new Error(`${path} has no provenance block, so there is no way to tell whether it was recorded against a ` +
71
+ 'version of the service that still exists. Re-record it.');
72
+ }
73
+ return fixture.response;
74
+ }
75
+ if (options.live === undefined) {
76
+ throw new Error(`Mode "${options.mode}" needs a live implementation to call.`);
77
+ }
78
+ const response = await options.live(request);
79
+ if (options.mode === 'record') {
80
+ mkdirSync(options.dir, { recursive: true });
81
+ const fixture = {
82
+ provenance: {
83
+ source: options.source ?? 'unknown',
84
+ recordedAt: (options.now?.() ?? new Date()).toISOString(),
85
+ requestDigest: digest,
86
+ },
87
+ response,
88
+ };
89
+ writeFileSync(path, `${JSON.stringify(fixture, null, 2)}\n`, 'utf8');
90
+ }
91
+ return response;
92
+ };
93
+ }
94
+ function recordedDigests(dir) {
95
+ try {
96
+ return readdirSync(dir)
97
+ .filter((entry) => entry.endsWith('.json'))
98
+ .map((entry) => entry.replace(/\.json$/, ''))
99
+ .sort();
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ }
105
+ /**
106
+ * Fixtures in `dir` that nothing asked for during a run.
107
+ *
108
+ * Worth reporting after a recording session: a fixture nobody replays is either
109
+ * dead weight or evidence that a code path stopped being exercised, and both
110
+ * are things you want to know before they are a year old.
111
+ */
112
+ export function unusedFixtures(dir, usedDigests) {
113
+ const used = new Set(usedDigests);
114
+ return recordedDigests(dir).filter((digest) => !used.has(digest));
115
+ }
116
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/replay/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@app-studio/qa-harness",
3
+ "version": "0.2.0",
4
+ "description": "The shape of a test harness — the real application under test, actors that make multi-role tests cheap — plus a record/replay transport whose missing fixtures always fail.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./replay": {
15
+ "types": "./dist/replay/index.d.ts",
16
+ "default": "./dist/replay/index.js"
17
+ },
18
+ "./nest": {
19
+ "types": "./dist/nest/index.d.ts",
20
+ "default": "./dist/nest/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@app-studio/qa-core": "^0.2.0"
35
+ },
36
+ "scripts": {
37
+ "build": "rimraf dist && tsc -p tsconfig.json",
38
+ "test": "vitest run"
39
+ }
40
+ }