@adobe/data-testing 0.9.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/dist/conformance/discover.d.ts +7 -0
  4. package/dist/conformance/discover.js +48 -0
  5. package/dist/conformance/discover.js.map +1 -0
  6. package/dist/conformance/effects.type-test.d.ts +1 -0
  7. package/dist/conformance/effects.type-test.js +39 -0
  8. package/dist/conformance/effects.type-test.js.map +1 -0
  9. package/dist/conformance/entity-ref.d.ts +5 -0
  10. package/dist/conformance/entity-ref.js +28 -0
  11. package/dist/conformance/entity-ref.js.map +1 -0
  12. package/dist/conformance/public.d.ts +7 -0
  13. package/dist/conformance/public.js +7 -0
  14. package/dist/conformance/public.js.map +1 -0
  15. package/dist/conformance/record-effects.d.ts +16 -0
  16. package/dist/conformance/record-effects.js +93 -0
  17. package/dist/conformance/record-effects.js.map +1 -0
  18. package/dist/conformance/resolve.d.ts +3 -0
  19. package/dist/conformance/resolve.js +8 -0
  20. package/dist/conformance/resolve.js.map +1 -0
  21. package/dist/conformance/run-actions.d.ts +14 -0
  22. package/dist/conformance/run-actions.js +34 -0
  23. package/dist/conformance/run-actions.js.map +1 -0
  24. package/dist/conformance/run-computeds.d.ts +14 -0
  25. package/dist/conformance/run-computeds.js +45 -0
  26. package/dist/conformance/run-computeds.js.map +1 -0
  27. package/dist/conformance/run-feature.d.ts +30 -0
  28. package/dist/conformance/run-feature.js +61 -0
  29. package/dist/conformance/run-feature.js.map +1 -0
  30. package/dist/conformance/run-spec.d.ts +10 -0
  31. package/dist/conformance/run-spec.empty-cases.test.d.ts +1 -0
  32. package/dist/conformance/run-spec.empty-cases.test.js +14 -0
  33. package/dist/conformance/run-spec.empty-cases.test.js.map +1 -0
  34. package/dist/conformance/run-spec.empty.test.d.ts +1 -0
  35. package/dist/conformance/run-spec.empty.test.js +11 -0
  36. package/dist/conformance/run-spec.empty.test.js.map +1 -0
  37. package/dist/conformance/run-spec.js +83 -0
  38. package/dist/conformance/run-spec.js.map +1 -0
  39. package/dist/conformance/run-spec.test.d.ts +1 -0
  40. package/dist/conformance/run-spec.test.js +28 -0
  41. package/dist/conformance/run-spec.test.js.map +1 -0
  42. package/dist/conformance/run-transactions.d.ts +13 -0
  43. package/dist/conformance/run-transactions.js +31 -0
  44. package/dist/conformance/run-transactions.js.map +1 -0
  45. package/dist/conformance/types.d.ts +29 -0
  46. package/dist/conformance/types.js +3 -0
  47. package/dist/conformance/types.js.map +1 -0
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +9 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/match/assert.d.ts +2 -0
  52. package/dist/match/assert.js +19 -0
  53. package/dist/match/assert.js.map +1 -0
  54. package/dist/match/match.d.ts +10 -0
  55. package/dist/match/match.js +82 -0
  56. package/dist/match/match.js.map +1 -0
  57. package/dist/match/match.test.d.ts +1 -0
  58. package/dist/match/match.test.js +42 -0
  59. package/dist/match/match.test.js.map +1 -0
  60. package/dist/match/matchers.d.ts +2 -0
  61. package/dist/match/matchers.js +14 -0
  62. package/dist/match/matchers.js.map +1 -0
  63. package/dist/match/public.d.ts +3 -0
  64. package/dist/match/public.js +5 -0
  65. package/dist/match/public.js.map +1 -0
  66. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Adobe, Inc.
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,44 @@
1
+ # @adobe/data-testing
2
+
3
+ Conformance-testing utilities for `@adobe/data` ECS features — the `Match` and
4
+ `Conformance` namespaces used by the co-located conformance pattern (see
5
+ `@adobe/data-ai`'s `features/data/state.md` and
6
+ `features/services/main-service/conformance.md`).
7
+
8
+ This package exists separately from `@adobe/data` so that installing
9
+ `@adobe/data` never pulls in a `vitest` peer dependency — only projects that
10
+ actually author conformance tests (and therefore already depend on `vitest`)
11
+ need it.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ pnpm add -D @adobe/data-testing vitest
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ // data/state/conformance-case.ts
23
+ import { Conformance as ConformanceApi } from "@adobe/data-testing";
24
+ import type { State } from "./state.js";
25
+
26
+ export type Conformance<F extends (...args: never[]) => unknown> = ConformanceApi.Cases<State, F>;
27
+ export const entity = ConformanceApi.entity;
28
+ ```
29
+
30
+ ```ts
31
+ // data/state/spec.test.ts
32
+ import { Conformance } from "@adobe/data-testing";
33
+ import { State } from "./state.js";
34
+ import { transitions } from "./transitions.js";
35
+
36
+ Conformance.runSpec({ state: State, transitions });
37
+ ```
38
+
39
+ `Match` provides tolerant, matcher-aware value comparison (`matches`, `ref`,
40
+ `anyNumber`, `anyString`) used by the `after` side of a conformance case.
41
+ `Conformance` provides the case types (`Cases`, `DerivationCases`, `Effects`)
42
+ and the runner drivers (`runSpec`, `runTransactions`, `runActions`,
43
+ `runComputeds`, `runFeature`) that drive vitest's `describe`/`it` against
44
+ spec-owned cases.
@@ -0,0 +1,7 @@
1
+ export interface Discovered {
2
+ readonly fn: (...args: unknown[]) => unknown;
3
+ readonly cases: readonly Record<string, unknown>[];
4
+ }
5
+ export declare const discoverTransitions: (modules: Record<string, Record<string, unknown>>) => Map<string, Discovered>;
6
+ export declare const discoverDerivations: (modules: Record<string, Record<string, unknown>>) => Map<string, Discovered>;
7
+ export declare const discoverOps: (source: Record<string, unknown>) => Map<string, (...a: never[]) => unknown>;
@@ -0,0 +1,48 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ const scan = (modules, isKind) => {
3
+ const out = new Map();
4
+ for (const [path, module] of Object.entries(modules)) {
5
+ const names = Object.keys(module);
6
+ if (!names.includes("cases"))
7
+ continue;
8
+ const cases = module["cases"];
9
+ if (!Array.isArray(cases) || cases.length === 0)
10
+ continue;
11
+ const first = cases[0];
12
+ if (typeof first !== "object" || first === null || !isKind(first))
13
+ continue;
14
+ const fnName = names.find((key) => typeof module[key] === "function");
15
+ if (!fnName)
16
+ throw new Error(`${path} exports \`cases\` but no function to pair`);
17
+ out.set(fnName, { fn: module[fnName], cases: cases });
18
+ }
19
+ return out;
20
+ };
21
+ // Transitions — files whose cases are `{ before, args?, after }` — keyed by the
22
+ // transform's function name (the name the ecs transaction/action must share).
23
+ export const discoverTransitions = (modules) => scan(modules, (c) => "after" in c);
24
+ // Derivations — files whose cases are `{ input, value }` — keyed by the
25
+ // derivation's function name (the name the ecs computed must share).
26
+ export const discoverDerivations = (modules) => scan(modules, (c) => "value" in c);
27
+ // Normalize the ecs-op source to `name → fn`. Accepts EITHER a facet barrel
28
+ // (`import * as x` — values are the functions, keyed by export name) OR a directory
29
+ // glob (`import.meta.glob(..., { eager: true })` — values are modules, each
30
+ // contributing its function exports). The glob form finds ops that live beside a
31
+ // barrel but aren't registered in it (a conformance-only action kept out of the
32
+ // plugin facet), so they still pair by name.
33
+ export const discoverOps = (source) => {
34
+ const out = new Map();
35
+ for (const [key, value] of Object.entries(source)) {
36
+ if (typeof value === "function") {
37
+ out.set(key, value);
38
+ }
39
+ else if (value !== null && typeof value === "object") {
40
+ for (const [name, member] of Object.entries(value)) {
41
+ if (typeof member === "function")
42
+ out.set(name, member);
43
+ }
44
+ }
45
+ }
46
+ return out;
47
+ };
48
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../../src/conformance/discover.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAQvD,MAAM,IAAI,GAAG,CACX,OAAgD,EAChD,MAAuD,EAC9B,EAAE;IAC3B,MAAM,GAAG,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAS;QACvC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAgC,CAAC;YAAE,SAAS;QACvG,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,UAAU,CAAC,CAAC;QACtE,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,4CAA4C,CAAC,CAAC;QAClF,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,MAAM,CAAqB,EAAE,KAAK,EAAE,KAA4B,EAAE,CAAC,CAAC;IACnG,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,gFAAgF;AAChF,8EAA8E;AAC9E,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAgD,EAA2B,EAAE,CAC/G,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;AAErC,wEAAwE;AACxE,qEAAqE;AACrE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAgD,EAA2B,EAAE,CAC/G,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;AAErC,4EAA4E;AAC5E,oFAAoF;AACpF,4EAA4E;AAC5E,iFAAiF;AACjF,gFAAgF;AAChF,6CAA6C;AAC7C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAA+B,EAA2C,EAAE;IACtG,MAAM,GAAG,GAAG,IAAI,GAAG,EAAsC,CAAC;IAC1D,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;YAChC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAmC,CAAC,CAAC;QACpD,CAAC;aAAM,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvD,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnD,IAAI,OAAO,MAAM,KAAK,UAAU;oBAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,MAAoC,CAAC,CAAC;YACxF,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ // ===== POSITIVE — must compile =====
2
+ const ordered = { analytics: [["todoCreated", { name: "a" }], ["todoToggled"]] };
3
+ const anyOrder = {
4
+ analytics: new Set([["todoToggled"], ["allTodosCleared"]]),
5
+ };
6
+ const noArgMethod = { analytics: [["displayCompletedToggled"]] };
7
+ const empty = {};
8
+ void ordered;
9
+ void anyOrder;
10
+ void noArgMethod;
11
+ void empty;
12
+ // ===== NEGATIVE — each must error =====
13
+ const badMethod = {
14
+ // @ts-expect-error - "noSuchMethod" is not a method of AnalyticsService
15
+ analytics: [["noSuchMethod", { name: "a" }]],
16
+ };
17
+ const badArgs = {
18
+ // @ts-expect-error - todoCreated takes { name: string }, not { count }
19
+ analytics: [["todoCreated", { count: 1 }]],
20
+ };
21
+ const missingArgs = {
22
+ // @ts-expect-error - todoCreated requires its args element
23
+ analytics: [["todoCreated"]],
24
+ };
25
+ const extraArg = {
26
+ // @ts-expect-error - todoToggled takes no args
27
+ analytics: [["todoToggled", { name: "a" }]],
28
+ };
29
+ const dataKey = {
30
+ // @ts-expect-error - "name" is a data arg, not a service
31
+ name: [["todoCreated", { name: "a" }]],
32
+ };
33
+ void badMethod;
34
+ void badArgs;
35
+ void missingArgs;
36
+ void extraArg;
37
+ void dataKey;
38
+ export {};
39
+ //# sourceMappingURL=effects.type-test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effects.type-test.js","sourceRoot":"","sources":["../../src/conformance/effects.type-test.ts"],"names":[],"mappings":"AAqBA,sCAAsC;AACtC,MAAM,OAAO,GAAkB,EAAE,SAAS,EAAE,CAAC,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;AAChG,MAAM,QAAQ,GAAkB;IAC9B,SAAS,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,aAAa,CAAU,EAAE,CAAC,iBAAiB,CAAU,CAAC,CAAC;CAC7E,CAAC;AACF,MAAM,WAAW,GAAkB,EAAE,SAAS,EAAE,CAAC,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC;AAChF,MAAM,KAAK,GAAkB,EAAE,CAAC;AAChC,KAAK,OAAO,CAAC;AACb,KAAK,QAAQ,CAAC;AACd,KAAK,WAAW,CAAC;AACjB,KAAK,KAAK,CAAC;AAEX,yCAAyC;AACzC,MAAM,SAAS,GAAkB;IAC/B,wEAAwE;IACxE,SAAS,EAAE,CAAC,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;CAC7C,CAAC;AACF,MAAM,OAAO,GAAkB;IAC7B,uEAAuE;IACvE,SAAS,EAAE,CAAC,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;CAC3C,CAAC;AACF,MAAM,WAAW,GAAkB;IACjC,2DAA2D;IAC3D,SAAS,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC;CAC7B,CAAC;AACF,MAAM,QAAQ,GAAkB;IAC9B,+CAA+C;IAC/C,SAAS,EAAE,CAAC,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;CAC5C,CAAC;AACF,MAAM,OAAO,GAAkB;IAC7B,yDAAyD;IACzD,IAAI,EAAE,CAAC,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;CACvC,CAAC;AACF,KAAK,SAAS,CAAC;AACf,KAAK,OAAO,CAAC;AACb,KAAK,WAAW,CAAC;AACjB,KAAK,QAAQ,CAAC;AACd,KAAK,OAAO,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Entity } from "@adobe/data/ecs";
2
+ import type { Resolve } from "./resolve.js";
3
+ export declare const entity: <T>(specId: T) => T;
4
+ export declare const adaptArgs: <Args>(args: Args, resolve?: Resolve<unknown>) => Args;
5
+ export type { Entity };
@@ -0,0 +1,28 @@
1
+ // A spec entity reference inside a case's `args`: `{ id: entity(1) }` names "the
2
+ // entity seeded for spec-id 1". It closes the one irreducible spec↔ecs vocabulary
3
+ // gap — identity: the pure spec reads the data-id, the ecs reads the entity the
4
+ // runner resolves. Typed as the id it stands in for, so it slots into the
5
+ // transform's own arg type (`{ id: number }`), exactly like `Match.anyNumber`.
6
+ const ENTITY_REF = Symbol.for("@adobe/data-testing:entity-ref");
7
+ export const entity = (specId) => ({ [ENTITY_REF]: specId });
8
+ const isEntityRef = (value) => typeof value === "object" && value !== null && ENTITY_REF in value;
9
+ // Adapt a case's `args` for one side of the conformance. `resolve` present → the
10
+ // ecs side (refs become seeded entities); absent → the pure-spec side (refs become
11
+ // their data-id). Only the top-level arg values are inspected — a `ref` is always a
12
+ // direct arg field. Non-ref values pass through untouched; a non-object `args`
13
+ // (a scalar `dt`, or `undefined`) passes through whole.
14
+ export const adaptArgs = (args, resolve) => {
15
+ if (args === null || typeof args !== "object" || Array.isArray(args))
16
+ return args;
17
+ let changed = false;
18
+ const next = { ...args };
19
+ for (const [key, value] of Object.entries(next)) {
20
+ if (isEntityRef(value)) {
21
+ const specId = value[ENTITY_REF];
22
+ next[key] = resolve ? resolve(specId) : specId;
23
+ changed = true;
24
+ }
25
+ }
26
+ return (changed ? next : args);
27
+ };
28
+ //# sourceMappingURL=entity-ref.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-ref.js","sourceRoot":"","sources":["../../src/conformance/entity-ref.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,kFAAkF;AAClF,gFAAgF;AAChF,0EAA0E;AAC1E,+EAA+E;AAC/E,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;AAEhE,MAAM,CAAC,MAAM,MAAM,GAAG,CAAI,MAAS,EAAK,EAAE,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAiB,CAAC;AAEtF,MAAM,WAAW,GAAG,CAAC,KAAc,EAA+C,EAAE,CAClF,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU,IAAI,KAAK,CAAC;AAErE,iFAAiF;AACjF,mFAAmF;AACnF,oFAAoF;AACpF,+EAA+E;AAC/E,wDAAwD;AACxD,MAAM,CAAC,MAAM,SAAS,GAAG,CAAO,IAAU,EAAE,OAA0B,EAAQ,EAAE;IAC9E,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClF,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,IAAI,GAA4B,EAAE,GAAI,IAAe,EAAE,CAAC;IAC9D,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAE,OAAO,CAAC,MAAM,CAAa,CAAC,CAAC,CAAC,MAAM,CAAC;YAC5D,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAS,CAAC;AACzC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ export type { Case, Cases, DerivationCase, DerivationCases, Effects, ServiceCall } from "./types.js";
2
+ export { entity } from "./entity-ref.js";
3
+ export { runSpec, type SpecRunConfig } from "./run-spec.js";
4
+ export { runTransactions, type TransactionRunConfig } from "./run-transactions.js";
5
+ export { runActions, type ActionRunConfig } from "./run-actions.js";
6
+ export { runComputeds, type ComputedRunConfig } from "./run-computeds.js";
7
+ export { runFeature, type FeatureRunConfig, type Projection } from "./run-feature.js";
@@ -0,0 +1,7 @@
1
+ export { entity } from "./entity-ref.js";
2
+ export { runSpec } from "./run-spec.js";
3
+ export { runTransactions } from "./run-transactions.js";
4
+ export { runActions } from "./run-actions.js";
5
+ export { runComputeds } from "./run-computeds.js";
6
+ export { runFeature } from "./run-feature.js";
7
+ //# sourceMappingURL=public.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.js","sourceRoot":"","sources":["../../src/conformance/public.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAsB,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,eAAe,EAA6B,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,YAAY,EAA0B,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAA0C,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { Effects } from "./types.js";
2
+ export type RecordedCall = readonly [string, ...unknown[]];
3
+ export declare const recordCalls: <S extends object>(service: S) => {
4
+ service: S;
5
+ calls: RecordedCall[];
6
+ };
7
+ export declare const recordArgServices: <Args>(args: Args) => {
8
+ args: Args;
9
+ calls: Record<string, RecordedCall[]>;
10
+ };
11
+ export declare const splitAndRecordServices: <Args>(args: Args) => {
12
+ services: Record<string, object>;
13
+ input: Record<string, unknown>;
14
+ calls: Record<string, RecordedCall[]>;
15
+ };
16
+ export declare const expectEffects: (calls: Record<string, readonly RecordedCall[]>, effects: Effects<Record<string, unknown>> | undefined) => void;
@@ -0,0 +1,93 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { equalsUnordered } from "@adobe/data";
3
+ import { matches } from "../match/match.js";
4
+ // A runtime service value: a non-array object with at least one method (mirrors
5
+ // the compile-time service detection in `types.ts`).
6
+ const isServiceValue = (value) => typeof value === "object" &&
7
+ value !== null &&
8
+ !Array.isArray(value) &&
9
+ Object.values(value).some((member) => typeof member === "function");
10
+ // Wrap a plain-object service so each method call is recorded, then delegates.
11
+ // No Proxy (services are plain objects with own enumerable methods), so we
12
+ // enumerate and closure-wrap each function.
13
+ export const recordCalls = (service) => {
14
+ const calls = [];
15
+ const wrapped = Object.fromEntries(Object.entries(service).map(([key, value]) => [
16
+ key,
17
+ typeof value === "function"
18
+ ? (...args) => {
19
+ calls.push([key, ...args]);
20
+ return value(...args);
21
+ }
22
+ : value,
23
+ ]));
24
+ return { service: wrapped, calls };
25
+ };
26
+ // Wrap the injected services IN PLACE within a case's `args` (leaving plain data
27
+ // untouched), returning the args ready to pass to a pure transform plus the
28
+ // per-service `calls` map. Used by the pure spec runner, which calls the transform
29
+ // with its full args; the ecs action runner uses `splitAndRecordServices` instead.
30
+ export const recordArgServices = (args) => {
31
+ const calls = {};
32
+ if (args === null || typeof args !== "object" || Array.isArray(args))
33
+ return { args, calls };
34
+ const next = { ...args };
35
+ for (const [key, value] of Object.entries(args)) {
36
+ if (isServiceValue(value)) {
37
+ const recorded = recordCalls(value);
38
+ next[key] = recorded.service;
39
+ calls[key] = recorded.calls;
40
+ }
41
+ }
42
+ return { args: next, calls };
43
+ };
44
+ // Split a case's `args` into the injected services (wrapped for recording) and
45
+ // the remaining plain data. Keyed by the same arg name so `calls` matches against
46
+ // `effects`. A no-arg case (`undefined` args) splits into nothing.
47
+ export const splitAndRecordServices = (args) => {
48
+ const services = {};
49
+ const input = {};
50
+ const calls = {};
51
+ if (args !== null && typeof args === "object" && !Array.isArray(args)) {
52
+ for (const [key, value] of Object.entries(args)) {
53
+ if (isServiceValue(value)) {
54
+ const recorded = recordCalls(value);
55
+ services[key] = recorded.service;
56
+ calls[key] = recorded.calls;
57
+ }
58
+ else {
59
+ input[key] = value;
60
+ }
61
+ }
62
+ }
63
+ return { services, input, calls };
64
+ };
65
+ const showCalls = (calls) => {
66
+ try {
67
+ return JSON.stringify(calls) ?? String(calls);
68
+ }
69
+ catch {
70
+ return String(calls);
71
+ }
72
+ };
73
+ // Assert the calls recorded against one service match the case's expectation for
74
+ // it: an Array expects exactly these calls in order; a Set expects the same calls
75
+ // in any order (multiset). Absent expectation ⇒ no calls expected. Ordered calls
76
+ // compare matcher-aware, so a call arg may itself use `anyNumber`.
77
+ const expectServiceCalls = (key, recorded, expected) => {
78
+ const ok = expected instanceof Set ? equalsUnordered(recorded, [...expected]) : matches(recorded, expected ?? []);
79
+ if (!ok) {
80
+ throw new Error(`effects mismatch on "${key}":\n recorded: ${showCalls(recorded)}\n expected: ${showCalls(expected instanceof Set ? [...expected] : (expected ?? []))}`);
81
+ }
82
+ };
83
+ // Assert each service DECLARED in `effects` saw exactly its expected calls (an
84
+ // extra or missing call on a declared service fails). Services not listed — e.g. a
85
+ // value-returning read like `generateName` — are ignored, so `effects` captures the
86
+ // fire-and-forget side effects a case chooses to assert.
87
+ export const expectEffects = (calls, effects) => {
88
+ const expected = (effects ?? {});
89
+ for (const key of Object.keys(expected)) {
90
+ expectServiceCalls(key, calls[key] ?? [], expected[key]);
91
+ }
92
+ };
93
+ //# sourceMappingURL=record-effects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-effects.js","sourceRoot":"","sources":["../../src/conformance/record-effects.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { Entity } from "@adobe/data/ecs";
2
+ export type Resolve<Id> = (id: Id) => Entity;
3
+ export declare const resolver: <Id>(seeded: ReadonlyMap<Id, Entity> | void) => Resolve<Id>;
@@ -0,0 +1,8 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { Entity } from "@adobe/data/ecs";
3
+ // Build a resolver from a `fromState` seed map. A feature whose transactions are
4
+ // addressed by index or are singleton (no id → entity mapping) returns `void`
5
+ // from `fromState`; its resolver is then never called, and any id resolves to
6
+ // `Entity.none`.
7
+ export const resolver = (seeded) => (id) => (seeded ? seeded.get(id) : undefined) ?? Entity.none;
8
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/conformance/resolve.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AASzC,iFAAiF;AACjF,8EAA8E;AAC9E,8EAA8E;AAC9E,iBAAiB;AACjB,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAK,MAAsC,EAAe,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAC1F,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Entity } from "@adobe/data/ecs";
2
+ import type { MatchOptions } from "../match/match.js";
3
+ export interface ActionRunConfig<Db, Store, State> {
4
+ readonly makeDb: (services: Record<string, object>) => Db;
5
+ readonly store: (db: Db) => Store;
6
+ readonly fromState: (store: Store, before: State) => ReadonlyMap<unknown, Entity> | void;
7
+ readonly toState: (store: Store) => State;
8
+ readonly transitions: Record<string, Record<string, unknown>>;
9
+ readonly actions: Record<string, unknown>;
10
+ readonly initial?: State;
11
+ readonly seedContext?: (db: Db, before: State, args: unknown) => void;
12
+ readonly match?: MatchOptions;
13
+ }
14
+ export declare function runActions<Db, Store, State>(config: ActionRunConfig<Db, Store, State>): void;
@@ -0,0 +1,34 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, it } from "vitest";
3
+ import { assert } from "../match/assert.js";
4
+ import { adaptArgs } from "./entity-ref.js";
5
+ import { discoverTransitions, discoverOps } from "./discover.js";
6
+ import { splitAndRecordServices, expectEffects } from "./record-effects.js";
7
+ import { resolver } from "./resolve.js";
8
+ // The single conformance test for every ecs action: each transition's cases run
9
+ // against its same-named action, asserting state and the declared effects.
10
+ export function runActions(config) {
11
+ const transitions = discoverTransitions(config.transitions);
12
+ for (const [name, action] of discoverOps(config.actions)) {
13
+ const paired = transitions.get(name);
14
+ if (!paired)
15
+ continue; // action with no transition (e.g. a streaming port) — not conformed here
16
+ describe(`${name} action conforms`, () => {
17
+ for (const testCase of paired.cases) {
18
+ it(testCase.name, async () => {
19
+ const { services, input, calls } = splitAndRecordServices(testCase.args);
20
+ // Case `before` is a delta over the feature default.
21
+ const before = { ...(config.initial ?? {}), ...testCase.before };
22
+ const db = config.makeDb(services);
23
+ const resolve = resolver(config.fromState(config.store(db), before));
24
+ config.seedContext?.(db, before, testCase.args);
25
+ await action(db, adaptArgs(input, resolve));
26
+ // `after` is a writes patch — compare `toState` against it merged over `before`.
27
+ assert(config.toState(config.store(db)), { ...before, ...testCase.after }, config.match);
28
+ expectEffects(calls, testCase.effects);
29
+ });
30
+ }
31
+ });
32
+ }
33
+ }
34
+ //# sourceMappingURL=run-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-actions.js","sourceRoot":"","sources":["../../src/conformance/run-actions.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAwBxC,gFAAgF;AAChF,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CAAmB,MAAyC;IACpF,MAAM,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5D,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACzD,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM;YAAE,SAAS,CAAC,yEAAyE;QAChG,QAAQ,CAAC,GAAG,IAAI,kBAAkB,EAAE,GAAG,EAAE;YACvC,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACpC,EAAE,CAAC,QAAQ,CAAC,IAAc,EAAE,KAAK,IAAI,EAAE;oBACrC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,sBAAsB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;oBACzE,qDAAqD;oBACrD,MAAM,MAAM,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,GAAI,QAAQ,CAAC,MAAiB,EAAW,CAAC;oBACtF,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;oBACnC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;oBACrE,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;oBAChD,MAAO,MAAuD,CAAC,EAAE,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;oBAC9F,iFAAiF;oBACjF,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,GAAI,MAAiB,EAAE,GAAI,QAAQ,CAAC,KAAgB,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;oBACjH,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAgB,CAAC,CAAC;gBAClD,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Entity } from "@adobe/data/ecs";
2
+ import type { MatchOptions } from "../match/match.js";
3
+ export interface ComputedRunConfig<Db, Store, State> {
4
+ readonly makeDb: () => Db;
5
+ readonly store: (db: Db) => Store;
6
+ readonly fromState: (store: Store, input: State) => unknown;
7
+ readonly toData?: (store: Store, entity: Entity) => unknown;
8
+ readonly derivations: Record<string, Record<string, unknown>>;
9
+ readonly computeds: Record<string, unknown>;
10
+ readonly hydrate?: readonly string[];
11
+ readonly initial?: State;
12
+ readonly match?: MatchOptions;
13
+ }
14
+ export declare function runComputeds<Db, Store, State>(config: ComputedRunConfig<Db, Store, State>): void;
@@ -0,0 +1,45 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, it } from "vitest";
3
+ import { assert } from "../match/assert.js";
4
+ import { discoverDerivations, discoverOps } from "./discover.js";
5
+ // Read a computed's synchronous emission: subscribe once, capture, unsubscribe.
6
+ const readComputed = (observe) => {
7
+ let value;
8
+ let read = false;
9
+ const unsubscribe = observe((next) => {
10
+ value = next;
11
+ read = true;
12
+ });
13
+ unsubscribe();
14
+ if (!read)
15
+ throw new Error("computed did not emit synchronously on subscribe");
16
+ return value;
17
+ };
18
+ // The single conformance test for every ecs computed backing a `data/state`
19
+ // derivation. Build `makeDb` from the `ComputedDatabase` layer (not the assembled
20
+ // db): a behaviour layer above may `withCache` a pre-seed value that a direct
21
+ // `fromState` seed cannot invalidate.
22
+ export function runComputeds(config) {
23
+ const derivations = discoverDerivations(config.derivations);
24
+ const hydrate = new Set(config.hydrate ?? []);
25
+ for (const [name, computed] of discoverOps(config.computeds)) {
26
+ const paired = derivations.get(name);
27
+ if (!paired)
28
+ continue; // computed with no `state/` derivation — covered by its data/<type> helper
29
+ describe(`${name} computed conforms`, () => {
30
+ for (const testCase of paired.cases) {
31
+ it(testCase.name, () => {
32
+ const db = config.makeDb();
33
+ const input = { ...(config.initial ?? {}), ...testCase.input };
34
+ config.fromState(config.store(db), input);
35
+ const raw = readComputed(computed(db));
36
+ const value = hydrate.has(name) && config.toData
37
+ ? raw.map((e) => config.toData(config.store(db), e))
38
+ : raw;
39
+ assert(value, testCase.value, config.match);
40
+ });
41
+ }
42
+ });
43
+ }
44
+ }
45
+ //# sourceMappingURL=run-computeds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-computeds.js","sourceRoot":"","sources":["../../src/conformance/run-computeds.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjE,gFAAgF;AAChF,MAAM,YAAY,GAAG,CAAI,OAAmB,EAAK,EAAE;IACjD,IAAI,KAAS,CAAC;IACd,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACnC,KAAK,GAAG,IAAI,CAAC;QACb,IAAI,GAAG,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IACH,WAAW,EAAE,CAAC;IACd,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IAC/E,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAsBF,4EAA4E;AAC5E,kFAAkF;AAClF,8EAA8E;AAC9E,sCAAsC;AACtC,MAAM,UAAU,YAAY,CAAmB,MAA2C;IACxF,MAAM,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7D,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM;YAAE,SAAS,CAAC,2EAA2E;QAClG,QAAQ,CAAC,GAAG,IAAI,oBAAoB,EAAE,GAAG,EAAE;YACzC,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACpC,EAAE,CAAC,QAAQ,CAAC,IAAc,EAAE,GAAG,EAAE;oBAC/B,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;oBAC3B,MAAM,KAAK,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,GAAI,QAAQ,CAAC,KAAgB,EAAW,CAAC;oBACpF,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;oBAC1C,MAAM,GAAG,GAAG,YAAY,CAAE,QAAwC,CAAC,EAAE,CAAC,CAAC,CAAC;oBACxE,MAAM,KAAK,GACT,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM;wBAChC,CAAC,CAAE,GAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,MAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;wBAC5E,CAAC,CAAC,GAAG,CAAC;oBACV,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9C,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { Database } from "@adobe/data/ecs";
2
+ import type { Entity } from "@adobe/data/ecs";
3
+ import type { MatchOptions } from "../match/match.js";
4
+ export interface Projection<Store, State> {
5
+ readonly fromState: (store: Store, state: State) => ReadonlyMap<unknown, Entity> | void;
6
+ readonly toState: (store: Store) => State;
7
+ readonly toData?: (store: Store, entity: Entity) => unknown;
8
+ }
9
+ export interface FeatureRunConfig<State, StoreT, Db extends {
10
+ store: StoreT;
11
+ }> {
12
+ readonly state: {
13
+ create(): State;
14
+ readonly samples?: readonly State[];
15
+ };
16
+ readonly transitions: Record<string, Record<string, unknown>>;
17
+ readonly plugin: Database.Plugin;
18
+ readonly computedPlugin?: Database.Plugin;
19
+ readonly projection: Projection<StoreT, State>;
20
+ readonly hydrate?: readonly string[];
21
+ readonly match?: MatchOptions;
22
+ readonly ops?: {
23
+ readonly transactions?: Record<string, unknown>;
24
+ readonly actions?: Record<string, unknown>;
25
+ readonly computeds?: Record<string, unknown>;
26
+ };
27
+ }
28
+ export declare function runFeature<State, StoreT, Db extends {
29
+ store: StoreT;
30
+ }>(config: FeatureRunConfig<State, StoreT, Db>): void;
@@ -0,0 +1,61 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, it } from "vitest";
3
+ import { Database, Store } from "@adobe/data/ecs";
4
+ import { assert } from "../match/assert.js";
5
+ import { runTransactions } from "./run-transactions.js";
6
+ import { runActions } from "./run-actions.js";
7
+ import { runComputeds } from "./run-computeds.js";
8
+ export function runFeature(config) {
9
+ const initial = config.state.create();
10
+ const { fromState, toState, toData } = config.projection;
11
+ const facets = config.plugin;
12
+ // A plugin carries the schema facets, so `Store.create` / `Database.create`
13
+ // accept it; the resulting store/db is the projection's `StoreT`/`Db`.
14
+ const makeStore = () => Store.create(config.plugin);
15
+ runTransactions({
16
+ createStore: makeStore,
17
+ fromState,
18
+ toState,
19
+ initial,
20
+ transitions: config.transitions,
21
+ transactions: config.ops?.transactions ?? facets.transactions,
22
+ match: config.match,
23
+ });
24
+ runActions({
25
+ makeDb: (services) => Database.toSystemDatabase(Database.create(config.plugin, { services })),
26
+ store: (db) => db.store,
27
+ fromState,
28
+ toState,
29
+ initial,
30
+ transitions: config.transitions,
31
+ actions: config.ops?.actions ?? facets.actions,
32
+ match: config.match,
33
+ });
34
+ if (config.computedPlugin) {
35
+ const computedFacets = config.computedPlugin;
36
+ runComputeds({
37
+ makeDb: () => Database.toSystemDatabase(Database.create(config.computedPlugin)),
38
+ store: (db) => db.store,
39
+ fromState,
40
+ toData,
41
+ initial,
42
+ derivations: config.transitions,
43
+ computeds: config.ops?.computeds ?? computedFacets.computed,
44
+ hydrate: config.hydrate,
45
+ match: config.match,
46
+ });
47
+ }
48
+ const samples = config.state.samples ?? [];
49
+ if (samples.length > 0) {
50
+ describe("projection round-trips (toState ∘ fromState ≡ identity)", () => {
51
+ samples.forEach((sample, index) => {
52
+ it(`sample ${index}`, () => {
53
+ const store = makeStore();
54
+ fromState(store, sample);
55
+ assert(toState(store), sample, config.match);
56
+ });
57
+ });
58
+ });
59
+ }
60
+ }
61
+ //# sourceMappingURL=run-feature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-feature.js","sourceRoot":"","sources":["../../src/conformance/run-feature.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { MatchOptions } from "../match/match.js";
2
+ export interface SpecRunConfig {
3
+ readonly state?: {
4
+ create(): object;
5
+ };
6
+ readonly transitions: Record<string, Record<string, unknown>>;
7
+ readonly match?: MatchOptions;
8
+ readonly label?: (path: string, fnName: string | undefined) => string;
9
+ }
10
+ export declare const runSpec: (config: SpecRunConfig) => void;
@@ -0,0 +1 @@
1
+ export {};