@substrat-run/contract-tests 0.87.0 → 0.89.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.
Files changed (48) hide show
  1. package/dist/concurrency-suite.d.ts +3 -0
  2. package/dist/concurrency-suite.d.ts.map +1 -0
  3. package/dist/concurrency-suite.js +228 -0
  4. package/dist/concurrency-suite.js.map +1 -0
  5. package/dist/conformance.d.ts +85 -0
  6. package/dist/conformance.d.ts.map +1 -0
  7. package/dist/conformance.js +17 -0
  8. package/dist/conformance.js.map +1 -0
  9. package/dist/entity-check-plan.d.ts +138 -0
  10. package/dist/entity-check-plan.d.ts.map +1 -0
  11. package/dist/entity-check-plan.js +181 -0
  12. package/dist/entity-check-plan.js.map +1 -0
  13. package/dist/entity-check-suite.d.ts +3 -90
  14. package/dist/entity-check-suite.d.ts.map +1 -1
  15. package/dist/entity-check-suite.js +42 -71
  16. package/dist/entity-check-suite.js.map +1 -1
  17. package/dist/entity-version-suite.d.ts +3 -0
  18. package/dist/entity-version-suite.d.ts.map +1 -0
  19. package/dist/entity-version-suite.js +160 -0
  20. package/dist/entity-version-suite.js.map +1 -0
  21. package/dist/idempotency-suite.d.ts +3 -0
  22. package/dist/idempotency-suite.d.ts.map +1 -0
  23. package/dist/idempotency-suite.js +243 -0
  24. package/dist/idempotency-suite.js.map +1 -0
  25. package/dist/index.d.ts +9 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +8 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/input-parse-suite.d.ts +3 -0
  30. package/dist/input-parse-suite.d.ts.map +1 -0
  31. package/dist/input-parse-suite.js +149 -0
  32. package/dist/input-parse-suite.js.map +1 -0
  33. package/dist/modules.d.ts +482 -160
  34. package/dist/modules.d.ts.map +1 -1
  35. package/dist/modules.js +367 -2
  36. package/dist/modules.js.map +1 -1
  37. package/dist/node-only-suite.d.ts +28 -0
  38. package/dist/node-only-suite.d.ts.map +1 -1
  39. package/dist/node-only-suite.js +43 -1
  40. package/dist/node-only-suite.js.map +1 -1
  41. package/dist/permission-suite.d.ts.map +1 -1
  42. package/dist/permission-suite.js +125 -0
  43. package/dist/permission-suite.js.map +1 -1
  44. package/dist/timeline-suite.d.ts +3 -0
  45. package/dist/timeline-suite.d.ts.map +1 -0
  46. package/dist/timeline-suite.js +223 -0
  47. package/dist/timeline-suite.js.map +1 -0
  48. package/package.json +11 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-version-suite.js","sourceRoot":"","sources":["../src/entity-version-suite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EACL,aAAa,EACb,aAAa,EACb,eAAe,EACf,WAAW,EACX,OAAO,EACP,QAAQ,GAET,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,IAAI,EAAkC,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5D,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;AAEpD,MAAM,UAAU,0BAA0B,CACxC,WAAmB,EACnB,WAA4C;IAE5C,QAAQ,CAAC,mCAAmC,WAAW,EAAE,EAAE,GAAG,EAAE;QAC9D,IAAI,OAAyB,CAAC;QAC9B,IAAI,IAAe,CAAC;QACpB,IAAI,IAAe,CAAC;QACpB,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAClC,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACjC,MAAM,KAAK,GAAgB,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAE5C,MAAM,SAAS,GAAG,CAAC,QAAgB,EAAE,UAAU,GAAG,YAAY,EAAE,EAAE,CAChE,IAAI,CAAC,MAAM,CAAgB,iBAAiB,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC1E,MAAM,SAAS,GAAG,CAAC,QAAgB,EAAE,OAAgB,EAAE,EAAE,CACvD,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAEhF,SAAS,CAAC,KAAK,IAAI,EAAE;YACnB,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;YAC9B,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;YACpB,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC;gBAClE,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACtC,CAAC;YACD,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;YAC7B,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAC;YACjG,MAAM,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,EAAE,EAAE,EAAE,SAAS,CAAC,CAAC;YACxD,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE;gBACrC,GAAG,EAAE,eAAe;gBACpB,WAAW,EAAE,CAAC,QAAQ,CAAC;gBACvB,MAAM,EAAE,UAAU;aACnB,CAAC,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE;gBACjC,WAAW,EAAE,KAAK;gBAClB,OAAO,EAAE,eAAe;gBACxB,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;aACtC,CAAC,CAAC;YACH,MAAM,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,kBAAkB,EAAE,CAAC,CAAC;YAC9F,MAAM,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YAC9C,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,KAAK,IAAI,EAAE;YAClB,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;YACjE,0EAA0E;YAC1E,2EAA2E;YAC3E,4EAA4E;YAC5E,2CAA2C;YAC3C,MAAM,CAAC,MAAM,SAAS,CAAC,eAAe,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;YACvE,MAAM,EAAE,GAAG,KAAK,IAAI,EAAE,EAAE,CAAC;YACzB,MAAM,CAAC,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YAEvC,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YACpB,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YAClC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAE7B,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YACpB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YACnC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/B,uEAAuE;YACvE,4EAA4E;YAC5E,4EAA4E;YAC5E,oCAAoC;YACpC,MAAM,CAAC,MAAO,GAAG,KAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;YAC1D,MAAM,EAAE,GAAG,KAAK,IAAI,EAAE,EAAE,CAAC;YACzB,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YACpB,MAAM,CAAC,GAAG,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YAC9B,MAAM,CAAC,GAAG,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YAC9B,MAAM,CAAC,GAAG,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YAC9B,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;YACxE,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,KAAK,IAAI,EAAE,EAAE,CAAC;YAC5B,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;YACtB,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;YACvB,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;YAEzC,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;YACvB,2EAA2E;YAC3E,0EAA0E;YAC1E,MAAM,CAAC,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/C,MAAM,CAAC,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;YACjF,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE,EAAE,CAAC;YAC7B,MAAM,SAAS,CAAC,MAAM,CAAC,CAAC;YACxB,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;YACtD,yEAAyE;YACzE,8CAA8C;YAC9C,MAAM,CAAC,MAAM,SAAS,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC1D,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;YACnE,MAAM,EAAE,GAAG,KAAK,IAAI,EAAE,EAAE,CAAC;YACzB,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CACzC,wBAAwB,EACxB,EAAE,QAAQ,EAAE,EAAE,EAAE,CACjB,CAAC;YACF,0EAA0E;YAC1E,4EAA4E;YAC5E,uEAAuE;YACvE,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC1B,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC/B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;YACtE,MAAM,EAAE,GAAG,KAAK,IAAI,EAAE,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YAC5C,MAAM,SAAS,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;YAC7B,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YACnC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAE9B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;YACtE,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAEvC,2EAA2E;YAC3E,6EAA6E;YAC7E,uEAAuE;YACvE,uDAAuD;YACvD,MAAM,CAAC,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;YAC/D,MAAM,EAAE,GAAG,KAAK,IAAI,EAAE,EAAE,CAAC;YACzB,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YACpB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC;YAEnC,MAAM,IAAI,CAAC,MAAM,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;YAE9C,6EAA6E;YAC7E,4EAA4E;YAC5E,0EAA0E;YAC1E,wEAAwE;YACxE,4EAA4E;YAC5E,yEAAyE;YACzE,6CAA6C;YAC7C,MAAM,CAAC,MAAM,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ScopeHostFixture } from './scope-host-suite.js';
2
+ export declare function idempotencyContractSuite(adapterName: string, makeFixture: () => Promise<ScopeHostFixture>): void;
3
+ //# sourceMappingURL=idempotency-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency-suite.d.ts","sourceRoot":"","sources":["../src/idempotency-suite.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAU9D,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAC3C,IAAI,CA6PN"}
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Contract suite for request idempotency — `Idempotency-Key` (#116).
3
+ *
4
+ * What it pins: **a retry does not do the work twice, and the host is what makes
5
+ * that true.** Everything else about this feature is a projection — the header
6
+ * name is a wire detail `vertical-host` owns, the opt-out is a model detail
7
+ * `contracts` owns, and neither is what makes a retry free. This is: a recording
8
+ * written in the same transaction as the work, and a second request answered
9
+ * from it without running anything.
10
+ *
11
+ * **Every case counts executions rather than comparing responses.** A suite that
12
+ * asserted only on the returned value would pass against a host that re-ran the
13
+ * operation and produced the same answer again — which is exactly the bug, since
14
+ * a duplicated work order looks a great deal like the original. So the fixture
15
+ * appends a row per invocation and `idem/runs` reports the count, and that count
16
+ * is the assertion.
17
+ *
18
+ * The cases that matter are not the happy path:
19
+ *
20
+ * - **A failed request leaves no recording.** The row is written inside the
21
+ * operation's transaction, so a throw takes it with the writes it describes and
22
+ * the retry executes — correctly, because nothing happened the first time.
23
+ * - **A key belongs to the subject that sent it.** Two principals choosing `1`
24
+ * must each get their own execution; a lookup that crossed that boundary would
25
+ * replay one caller's response to another.
26
+ * - **A reused key is refused, never served.** Same key, different request means
27
+ * the client's assertion is false, and answering with the earlier response
28
+ * would be a lie it acts on.
29
+ * - **An unrecordable response fails closed.** A replay that cannot be answered
30
+ * is a 409, not a second execution.
31
+ */
32
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
33
+ import { errorCodeOf, permissionKey, platformActorId, principalId, scopeId, tenantId, IDEMPOTENCY_REPLAY_UNAVAILABLE, IDEMPOTENCY_REUSED, } from '@substrat-run/contracts';
34
+ import { ulid } from '@substrat-run/kernel';
35
+ import { idempotencyMod } from './modules.js';
36
+ const IDEM_USE = permissionKey.parse('idem:use');
37
+ /** The `reason` slug a refusal carried, or undefined — read off the thrown error. */
38
+ function reasonOf(err) {
39
+ return err?.extensions?.['reason'];
40
+ }
41
+ export function idempotencyContractSuite(adapterName, makeFixture) {
42
+ describe(`request idempotency (Idempotency-Key): ${adapterName}`, () => {
43
+ let fixture;
44
+ let host;
45
+ let stub;
46
+ /** A second principal, for the case that a key is one caller's and not another's. */
47
+ let otherStub;
48
+ const t1 = tenantId.parse(ulid());
49
+ const s1 = scopeId.parse(ulid());
50
+ const alice = principalId.parse(ulid());
51
+ const bob = principalId.parse(ulid());
52
+ const staff = platformActorId.parse(ulid());
53
+ /** Whether each invocation was answered from a recording, newest last. */
54
+ let replays = [];
55
+ const sink = () => {
56
+ let replayed = false;
57
+ return {
58
+ options: {
59
+ onIdempotentReplay: () => {
60
+ replayed = true;
61
+ },
62
+ },
63
+ done: () => replays.push(replayed),
64
+ };
65
+ };
66
+ const create = async (thingId, key, label, as = () => stub) => {
67
+ const probe = sink();
68
+ try {
69
+ return await as().invoke('idem/create', { thingId, ...(label === undefined ? {} : { label }) }, { ...probe.options, ...(key === undefined ? {} : { idempotencyKey: key }) });
70
+ }
71
+ finally {
72
+ probe.done();
73
+ }
74
+ };
75
+ /** How many times a handler in this module has actually run. */
76
+ const runs = async () => (await stub.invoke('idem/runs', {})).count;
77
+ beforeAll(async () => {
78
+ fixture = await makeFixture();
79
+ host = fixture.host;
80
+ host.registerModule(idempotencyMod);
81
+ await host.admin.createTenant(staff, { id: t1, slug: 'idem-tenant', name: 'Idem Tenant' });
82
+ await host.admin.grantEntitlement(staff, t1, 'idem');
83
+ await host.admin.defineRole(staff, t1, {
84
+ key: 'idem-admin',
85
+ permissions: [IDEM_USE],
86
+ source: 'vertical',
87
+ });
88
+ for (const principal of [alice, bob]) {
89
+ await host.admin.assignRole(staff, {
90
+ principalId: principal,
91
+ roleKey: 'idem-admin',
92
+ node: { tenantId: t1, scopeId: null },
93
+ });
94
+ }
95
+ await host.provisionScope(staff, { tenantId: t1, scopeId: s1, vertical: 'idem-vertical' });
96
+ await host.admin.activateScope(staff, t1, s1);
97
+ stub = await host.getScope(alice, t1, s1);
98
+ otherStub = await host.getScope(bob, t1, s1);
99
+ });
100
+ afterAll(async () => {
101
+ await fixture.cleanup();
102
+ });
103
+ it('runs twice for two identical requests carrying no key', async () => {
104
+ const before = await runs();
105
+ await create('baseline');
106
+ await create('baseline');
107
+ // The baseline the whole feature is measured against: without a key there is
108
+ // no dedupe, and identical requests are two requests. A suite that skipped
109
+ // this could not tell a working replay from an operation that happens to be
110
+ // naturally idempotent.
111
+ expect(await runs()).toBe(before + 2);
112
+ });
113
+ it('runs once for a repeated request under one key, and replays the response', async () => {
114
+ const key = `k-${ulid()}`;
115
+ const before = await runs();
116
+ replays = [];
117
+ const first = await create('once', key, 'hello');
118
+ const second = await create('once', key, 'hello');
119
+ // The assertion that matters is the count, not the equality below it.
120
+ expect(await runs()).toBe(before + 1);
121
+ // `run` is a fresh ULID per execution, so identical values here mean the
122
+ // second response came from the recording rather than from a second run
123
+ // that agreed by luck.
124
+ expect(second).toEqual(first);
125
+ expect(replays).toEqual([false, true]);
126
+ });
127
+ it('scopes a key to the subject that sent it', async () => {
128
+ const key = `shared-${ulid()}`;
129
+ const before = await runs();
130
+ replays = [];
131
+ const mine = await create('theirs', key, 'a', () => stub);
132
+ const theirs = await create('theirs', key, 'a', () => otherStub);
133
+ // Two principals will choose the same key — `1` is a key someone sends. A
134
+ // lookup that found the other's row would replay a response across a
135
+ // principal boundary, which is a disclosure and not a convenience.
136
+ expect(await runs()).toBe(before + 2);
137
+ expect(theirs).not.toEqual(mine);
138
+ expect(replays).toEqual([false, false]);
139
+ });
140
+ it('refuses a key reused for a different request, and runs nothing', async () => {
141
+ const key = `reuse-${ulid()}`;
142
+ await create('reused', key, 'original');
143
+ const before = await runs();
144
+ await expect(create('reused', key, 'CHANGED')).rejects.toSatisfy((err) => {
145
+ return errorCodeOf(err) === 'conflict' && reasonOf(err) === IDEMPOTENCY_REUSED;
146
+ });
147
+ // Refused, and refused BEFORE anything ran. Serving the first request's
148
+ // response would be worse — the client would act on an answer to a question
149
+ // it did not ask.
150
+ expect(await runs()).toBe(before);
151
+ });
152
+ it('treats a different key as a different request', async () => {
153
+ const before = await runs();
154
+ await create('twice', `k-${ulid()}`, 'same input');
155
+ await create('twice', `k-${ulid()}`, 'same input');
156
+ // Dedupe is by KEY, never by content: two deliberate identical writes are
157
+ // two writes, and a client that wanted one sends one key.
158
+ expect(await runs()).toBe(before + 2);
159
+ });
160
+ it('retries — does not replay — a request that failed', async () => {
161
+ const key = `fail-${ulid()}`;
162
+ const before = await runs();
163
+ await expect(stub.invoke('idem/fails', { thingId: 'nope' }, { idempotencyKey: key })).rejects.toThrow();
164
+ // The handler wrote a row before throwing and the transaction rolled it
165
+ // back, so the count has not moved — which is also why there is nothing to
166
+ // replay.
167
+ expect(await runs()).toBe(before);
168
+ await expect(stub.invoke('idem/fails', { thingId: 'nope' }, { idempotencyKey: key })).rejects.toThrow();
169
+ // Executed again rather than replaying the failure. Recording failures would
170
+ // have meant deciding which are permanent, and a retry after a 500 is the
171
+ // most ordinary thing a client does.
172
+ expect(await runs()).toBe(before);
173
+ // The same key is still free for the request that eventually succeeds: the
174
+ // first two attempts left no row, so this is a first request.
175
+ const ok = await stub.invoke('idem/create', { thingId: 'recovered' }, { idempotencyKey: key });
176
+ expect(ok).toMatchObject({ id: 'recovered' });
177
+ });
178
+ it('refuses a replay it cannot answer rather than executing again', async () => {
179
+ const key = `big-${ulid()}`;
180
+ const before = await runs();
181
+ await stub.invoke('idem/big', { thingId: 'large' }, { idempotencyKey: key });
182
+ expect(await runs()).toBe(before + 1);
183
+ await expect(stub.invoke('idem/big', { thingId: 'large' }, { idempotencyKey: key })).rejects.toSatisfy((err) => {
184
+ return errorCodeOf(err) === 'conflict' && reasonOf(err) === IDEMPOTENCY_REPLAY_UNAVAILABLE;
185
+ });
186
+ // Fail closed. An error the caller can act on beats the duplicate execution
187
+ // the key was sent to prevent — and the original did complete, so re-running
188
+ // is the one answer that is certainly wrong.
189
+ expect(await runs()).toBe(before + 1);
190
+ });
191
+ it('refuses a key on an operation that declared `idempotency: false`', async () => {
192
+ const before = await runs();
193
+ await expect(stub.invoke('idem/secret', { thingId: 's1' }, { idempotencyKey: `s-${ulid()}` })).rejects.toThrow(/idempotency: false|cannot honour an Idempotency-Key/i);
194
+ // Refused rather than ignored: a caller who sent a key and got a 200 would
195
+ // believe the retry is safe, and here the response was never recorded — the
196
+ // second request would mint a second secret.
197
+ expect(await runs()).toBe(before);
198
+ // The operation itself still works; it is the header it refuses.
199
+ await expect(stub.invoke('idem/secret', { thingId: 's1' })).resolves.toMatchObject({
200
+ id: 's1',
201
+ });
202
+ });
203
+ it('refuses a malformed key', async () => {
204
+ const before = await runs();
205
+ for (const bad of ['', 'has space', 'x'.repeat(256)]) {
206
+ await expect(stub.invoke('idem/create', { thingId: 'bad' }, { idempotencyKey: bad })).rejects.toSatisfy((err) => errorCodeOf(err) === 'validation_failed');
207
+ }
208
+ expect(await runs()).toBe(before);
209
+ });
210
+ it('replays the entity version alongside the body', async () => {
211
+ const key = `guard-${ulid()}`;
212
+ const tags = [];
213
+ const options = {
214
+ idempotencyKey: key,
215
+ onEntityVersion: (v) => tags.push(v),
216
+ };
217
+ const first = await stub.invoke('idem/create-guarded', { thingId: 'g1' }, options);
218
+ const second = await stub.invoke('idem/create-guarded', { thingId: 'g1' }, options);
219
+ expect(second).toEqual(first);
220
+ // The two seams compose or neither works: a replayed response with no `ETag`
221
+ // leaves the client holding no validator, so its next conditional write has
222
+ // nothing to send and the lost-update protection quietly switches off.
223
+ expect(tags).toHaveLength(2);
224
+ expect(tags[1]).toBe(tags[0]);
225
+ expect(tags[0]).not.toBeNull();
226
+ });
227
+ it('does not report a version for a replay of an unguarded operation', async () => {
228
+ const key = `plain-${ulid()}`;
229
+ const tags = [];
230
+ const options = {
231
+ idempotencyKey: key,
232
+ onEntityVersion: (v) => tags.push(v),
233
+ };
234
+ await stub.invoke('idem/create', { thingId: 'p1' }, options);
235
+ await stub.invoke('idem/create', { thingId: 'p1' }, options);
236
+ // #129's rule survives the replay path: an operation that declared no
237
+ // `concurrency` never reports a tag, and a recording must not become the
238
+ // route by which one appears.
239
+ expect(tags).toEqual([]);
240
+ });
241
+ });
242
+ }
243
+ //# sourceMappingURL=idempotency-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency-suite.js","sourceRoot":"","sources":["../src/idempotency-suite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EACL,WAAW,EACX,aAAa,EACb,eAAe,EACf,WAAW,EACX,OAAO,EACP,QAAQ,EACR,8BAA8B,EAC9B,kBAAkB,GAEnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,IAAI,EAAkC,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AAEjD,qFAAqF;AACrF,SAAS,QAAQ,CAAC,GAAY;IAC5B,OAAQ,GAAgD,EAAE,UAAU,EAAE,CAAC,QAAQ,CAAC,CAAC;AACnF,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,WAAmB,EACnB,WAA4C;IAE5C,QAAQ,CAAC,0CAA0C,WAAW,EAAE,EAAE,GAAG,EAAE;QACrE,IAAI,OAAyB,CAAC;QAC9B,IAAI,IAAe,CAAC;QACpB,IAAI,IAAe,CAAC;QACpB,qFAAqF;QACrF,IAAI,SAAoB,CAAC;QACzB,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAClC,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACjC,MAAM,KAAK,GAAgB,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,GAAG,GAAgB,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAE5C,0EAA0E;QAC1E,IAAI,OAAO,GAAc,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,IAAI,QAAQ,GAAG,KAAK,CAAC;YACrB,OAAO;gBACL,OAAO,EAAE;oBACP,kBAAkB,EAAE,GAAG,EAAE;wBACvB,QAAQ,GAAG,IAAI,CAAC;oBAClB,CAAC;iBACF;gBACD,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;aACnC,CAAC;QACJ,CAAC,CAAC;QAEF,MAAM,MAAM,GAAG,KAAK,EAClB,OAAe,EACf,GAAY,EACZ,KAAc,EACd,EAAE,GAAoB,GAAG,EAAE,CAAC,IAAI,EAChC,EAAE;YACF,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC;YACrB,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,EAAE,CAAC,MAAM,CACtB,aAAa,EACb,EAAE,OAAO,EAAE,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EACtD,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,EAAE,CAC5E,CAAC;YACJ,CAAC;oBAAS,CAAC;gBACT,KAAK,CAAC,IAAI,EAAE,CAAC;YACf,CAAC;QACH,CAAC,CAAC;QAEF,gEAAgE;QAChE,MAAM,IAAI,GAAG,KAAK,IAAqB,EAAE,CACtC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,CAAuB,CAAC,KAAK,CAAC;QAEpE,SAAS,CAAC,KAAK,IAAI,EAAE;YACnB,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;YAC9B,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;YACpB,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;YACpC,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;YAC3F,MAAM,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;YACrD,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE;gBACrC,GAAG,EAAE,YAAY;gBACjB,WAAW,EAAE,CAAC,QAAQ,CAAC;gBACvB,MAAM,EAAE,UAAU;aACnB,CAAC,CAAC;YACH,KAAK,MAAM,SAAS,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE;oBACjC,WAAW,EAAE,SAAS;oBACtB,OAAO,EAAE,YAAY;oBACrB,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;iBACtC,CAAC,CAAC;YACL,CAAC;YACD,MAAM,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,CAAC;YAC3F,MAAM,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YAC9C,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YAC1C,SAAS,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,KAAK,IAAI,EAAE;YAClB,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;YACrE,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAC5B,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;YACzB,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;YACzB,6EAA6E;YAC7E,2EAA2E;YAC3E,4EAA4E;YAC5E,wBAAwB;YACxB,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0EAA0E,EAAE,KAAK,IAAI,EAAE;YACxF,MAAM,GAAG,GAAG,KAAK,IAAI,EAAE,EAAE,CAAC;YAC1B,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAC5B,OAAO,GAAG,EAAE,CAAC;YAEb,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;YAElD,sEAAsE;YACtE,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACtC,yEAAyE;YACzE,wEAAwE;YACxE,uBAAuB;YACvB,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC9B,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;YACxD,MAAM,GAAG,GAAG,UAAU,IAAI,EAAE,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAC5B,OAAO,GAAG,EAAE,CAAC;YAEb,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAC1D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAEjE,0EAA0E;YAC1E,qEAAqE;YACrE,mEAAmE;YACnE,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACtC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACjC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;YAC9E,MAAM,GAAG,GAAG,SAAS,IAAI,EAAE,EAAE,CAAC;YAC9B,MAAM,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC;YACxC,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAE5B,MAAM,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,GAAY,EAAE,EAAE;gBAChF,OAAO,WAAW,CAAC,GAAG,CAAC,KAAK,UAAU,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,kBAAkB,CAAC;YACjF,CAAC,CAAC,CAAC;YAEH,wEAAwE;YACxE,4EAA4E;YAC5E,kBAAkB;YAClB,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;YAC7D,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAC5B,MAAM,MAAM,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,EAAE,EAAE,YAAY,CAAC,CAAC;YACnD,MAAM,MAAM,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,EAAE,EAAE,YAAY,CAAC,CAAC;YACnD,0EAA0E;YAC1E,0DAA0D;YAC1D,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;YACjE,MAAM,GAAG,GAAG,QAAQ,IAAI,EAAE,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAE5B,MAAM,MAAM,CACV,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,CACxE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACpB,wEAAwE;YACxE,2EAA2E;YAC3E,UAAU;YACV,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAElC,MAAM,MAAM,CACV,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,CACxE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACpB,6EAA6E;YAC7E,0EAA0E;YAC1E,qCAAqC;YACrC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAElC,2EAA2E;YAC3E,8DAA8D;YAC9D,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAC1B,aAAa,EACb,EAAE,OAAO,EAAE,WAAW,EAAE,EACxB,EAAE,cAAc,EAAE,GAAG,EAAE,CACxB,CAAC;YACF,MAAM,CAAC,EAAE,CAAC,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;YAC7E,MAAM,GAAG,GAAG,OAAO,IAAI,EAAE,EAAE,CAAC;YAC5B,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAC5B,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;YAC7E,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAEtC,MAAM,MAAM,CACV,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,CACvE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,GAAY,EAAE,EAAE;gBACnC,OAAO,WAAW,CAAC,GAAG,CAAC,KAAK,UAAU,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,8BAA8B,CAAC;YAC7F,CAAC,CAAC,CAAC;YAEH,4EAA4E;YAC5E,6EAA6E;YAC7E,6CAA6C;YAC7C,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;YAChF,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAC5B,MAAM,MAAM,CACV,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,cAAc,EAAE,KAAK,IAAI,EAAE,EAAE,EAAE,CAAC,CACjF,CAAC,OAAO,CAAC,OAAO,CAAC,sDAAsD,CAAC,CAAC;YAC1E,2EAA2E;YAC3E,4EAA4E;YAC5E,6CAA6C;YAC7C,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAElC,iEAAiE;YACjE,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC;gBACjF,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yBAAyB,EAAE,KAAK,IAAI,EAAE;YACvC,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;YAC5B,KAAK,MAAM,GAAG,IAAI,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBACrD,MAAM,MAAM,CACV,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,CACxE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,mBAAmB,CAAC,CAAC;YAClF,CAAC;YACD,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;YAC7D,MAAM,GAAG,GAAG,SAAS,IAAI,EAAE,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAsB,EAAE,CAAC;YACnC,MAAM,OAAO,GAAG;gBACd,cAAc,EAAE,GAAG;gBACnB,eAAe,EAAE,CAAC,CAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;aACpD,CAAC;YACF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,qBAAqB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;YACnF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,qBAAqB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;YAEpF,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC9B,6EAA6E;YAC7E,4EAA4E;YAC5E,uEAAuE;YACvE,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;YAChF,MAAM,GAAG,GAAG,SAAS,IAAI,EAAE,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAsB,EAAE,CAAC;YACnC,MAAM,OAAO,GAAG;gBACd,cAAc,EAAE,GAAG;gBACnB,eAAe,EAAE,CAAC,CAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;aACpD,CAAC;YACF,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;YAC7D,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;YAC7D,sEAAsE;YACtE,yEAAyE;YACzE,8BAA8B;YAC9B,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
package/dist/index.d.ts CHANGED
@@ -3,12 +3,19 @@ export type { ScopeHostFixture, ScopeHostSuiteOptions } from './scope-host-suite
3
3
  export { permissionContractSuite } from './permission-suite.js';
4
4
  export { atomicContractSuite } from './atomic-suite.js';
5
5
  export { searchContractSuite } from './search-suite.js';
6
+ export { entityVersionContractSuite } from './entity-version-suite.js';
7
+ export { timelineContractSuite } from './timeline-suite.js';
8
+ export { concurrencyContractSuite } from './concurrency-suite.js';
9
+ export { idempotencyContractSuite } from './idempotency-suite.js';
6
10
  export { listContractSuite } from './list-suite.js';
7
11
  export { scheduleContractSuite } from './schedule-suite.js';
12
+ export { inputParseContractSuite } from './input-parse-suite.js';
8
13
  export { entityCheckConformanceSuite, planEntityCheckCoverage } from './entity-check-suite.js';
9
14
  export type { EntityCheckFixture, EntityCheckSuiteOptions, PlannedCheck } from './entity-check-suite.js';
10
- export { nodeOnlySuite } from './node-only-suite.js';
15
+ export { nodeOnlySuite, declaredNodeOnlySuite } from './node-only-suite.js';
11
16
  export type { NodeOnlyOptions } from './node-only-suite.js';
12
- export { atomicMod, billedMod, brokenMod, connectorMod, contractTestBareOps, contractTestInitialModules, contractTestModules, permMod, scheduleMod, searchMod, searchModManifest, listMod, listModManifest, } from './modules.js';
17
+ export { declareEntityChecks, declareNodeOnly, assertNodeOnly } from './conformance.js';
18
+ export type { ConformanceDeclaration, DrivenConformance, DeclaredNodeOnlyConformance, AssertedNodeOnlyConformance, } from './conformance.js';
19
+ export { atomicMod, billedMod, brokenMod, connectorMod, contractTestBareOps, contractTestInitialModules, contractTestModules, permMod, scheduleMod, searchMod, searchModManifest, listMod, listModManifest, parseMod, parseModManifest, } from './modules.js';
13
20
  export { connectorCalls, connectorTestFetch, resetConnectorCalls, type ConnectorCall, } from './connector-fixture.js';
14
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACzG,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,OAAO,EACP,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACzG,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC5E,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACxF,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,OAAO,EACP,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -2,10 +2,16 @@ export { scopeHostContractSuite } from './scope-host-suite.js';
2
2
  export { permissionContractSuite } from './permission-suite.js';
3
3
  export { atomicContractSuite } from './atomic-suite.js';
4
4
  export { searchContractSuite } from './search-suite.js';
5
+ export { entityVersionContractSuite } from './entity-version-suite.js';
6
+ export { timelineContractSuite } from './timeline-suite.js';
7
+ export { concurrencyContractSuite } from './concurrency-suite.js';
8
+ export { idempotencyContractSuite } from './idempotency-suite.js';
5
9
  export { listContractSuite } from './list-suite.js';
6
10
  export { scheduleContractSuite } from './schedule-suite.js';
11
+ export { inputParseContractSuite } from './input-parse-suite.js';
7
12
  export { entityCheckConformanceSuite, planEntityCheckCoverage } from './entity-check-suite.js';
8
- export { nodeOnlySuite } from './node-only-suite.js';
9
- export { atomicMod, billedMod, brokenMod, connectorMod, contractTestBareOps, contractTestInitialModules, contractTestModules, permMod, scheduleMod, searchMod, searchModManifest, listMod, listModManifest, } from './modules.js';
13
+ export { nodeOnlySuite, declaredNodeOnlySuite } from './node-only-suite.js';
14
+ export { declareEntityChecks, declareNodeOnly, assertNodeOnly } from './conformance.js';
15
+ export { atomicMod, billedMod, brokenMod, connectorMod, contractTestBareOps, contractTestInitialModules, contractTestModules, permMod, scheduleMod, searchMod, searchModManifest, listMod, listModManifest, parseMod, parseModManifest, } from './modules.js';
10
16
  export { connectorCalls, connectorTestFetch, resetConnectorCalls, } from './connector-fixture.js';
11
17
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAE/F,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,OAAO,EACP,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,GAEpB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAE/F,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOxF,OAAO,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,OAAO,EACP,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,GAEpB,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ScopeHostFixture } from './scope-host-suite.js';
2
+ export declare function inputParseContractSuite(adapterName: string, makeFixture: () => Promise<ScopeHostFixture>): void;
3
+ //# sourceMappingURL=input-parse-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-parse-suite.d.ts","sourceRoot":"","sources":["../src/input-parse-suite.ts"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAK9D,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAC3C,IAAI,CA4HN"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Contract suite for the declared-input parse at the scope door (#893).
3
+ *
4
+ * What it pins is one sentence: **an operation's declared `input` is parsed by
5
+ * the HOST, before the guards and the handler, on every path into a scope.**
6
+ *
7
+ * ## Why this is a host contract and not a house rule
8
+ *
9
+ * `OperationShape.input` has always described itself as *"the SAME Zod object
10
+ * the handler parses"*. Across the fleet it mostly was not: of ~85 declared
11
+ * inputs, 40 were parsed, and `demos/rally` declared 32 and parsed 2. The
12
+ * declaration was true about the shape — the compiler holds `idFrom` and
13
+ * `entityIdFrom` to it — and false about the parsing, which is the half that
14
+ * refuses a malformed call. Every operation authored after the fix would have
15
+ * had to remember, forever, in every vertical.
16
+ *
17
+ * The alternative was a lint rule, and it is strictly weaker: it can ask only
18
+ * whether *some* `.parse` appears in a handler body, never whether it is the
19
+ * declared schema at the boundary. It also cannot be satisfied at all where the
20
+ * schema is declared inline (`input: z.object({…})`) — callout, handlebar and
21
+ * todo declare 25 such inputs between them, the reference implementation among
22
+ * them. So the host parses, from the declaration that already produces the
23
+ * manifest, the routes and the OpenAPI document. `mountOperations` had already
24
+ * made this argument for the page trio, in those words.
25
+ *
26
+ * ## Why it belongs to every adapter
27
+ *
28
+ * A vertical's isolation now depends on it. If one host parses and another does
29
+ * not, the same operation refuses a malformed call on SQLite and accepts it on
30
+ * the DO — and "parse, don't trust" becomes a fact about which substrate a
31
+ * tenant landed on. That is exactly the class of divergence `atomicContractSuite`
32
+ * exists to close for `ctx.atomic`.
33
+ *
34
+ * The suite deliberately drives operations through `stub.invoke` rather than
35
+ * over HTTP. Parsing at the HTTP mount alone would leave every scenario test,
36
+ * seed and schedule on an unparsed path — the demos' own suites would exercise
37
+ * the one route the fix did not cover.
38
+ */
39
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
40
+ import { errorCodeOf, moduleId, permissionKey, platformActorId, principalId, scopeId, tenantId, toProblem, } from '@substrat-run/contracts';
41
+ import { ulid } from '@substrat-run/kernel';
42
+ import { parseMod, parseModManifest } from './modules.js';
43
+ const PARSE_USE = permissionKey.parse('parse:use');
44
+ export function inputParseContractSuite(adapterName, makeFixture) {
45
+ describe(`declared input is parsed at the door (#893): ${adapterName}`, () => {
46
+ let fixture;
47
+ let host;
48
+ let stub;
49
+ const t1 = tenantId.parse(ulid());
50
+ const s1 = scopeId.parse(ulid());
51
+ const alice = principalId.parse(ulid());
52
+ const staff = platformActorId.parse(ulid());
53
+ beforeAll(async () => {
54
+ fixture = await makeFixture();
55
+ host = fixture.host;
56
+ host.registerModule(parseMod);
57
+ await host.admin.createTenant(staff, { id: t1, slug: 'parse-tenant', name: 'Parse Tenant' });
58
+ await host.admin.grantEntitlement(staff, t1, 'parse');
59
+ await host.admin.defineRole(staff, t1, {
60
+ key: 'parser',
61
+ permissions: [PARSE_USE],
62
+ source: 'vertical',
63
+ });
64
+ await host.admin.assignRole(staff, {
65
+ principalId: alice,
66
+ roleKey: 'parser',
67
+ node: { tenantId: t1, scopeId: null },
68
+ });
69
+ await host.provisionScope(staff, { tenantId: t1, scopeId: s1, vertical: 'parse-vertical' });
70
+ await host.admin.activateScope(staff, t1, s1);
71
+ stub = await host.getScope(alice, t1, s1);
72
+ });
73
+ afterAll(async () => {
74
+ await fixture.cleanup();
75
+ });
76
+ const received = async (op, input) => (await stub.invoke(op, input)).received;
77
+ it('refuses an invocation whose declared field has the wrong type', async () => {
78
+ // The handler never runs: a `name` of the wrong type is not a domain
79
+ // decision the module gets to make badly, it is a call that never had a
80
+ // meaning. Refused before the transaction opens.
81
+ await expect(stub.invoke('parse/echo', { name: 42 })).rejects.toThrow();
82
+ });
83
+ it('refuses an invocation missing a required declared field', async () => {
84
+ await expect(stub.invoke('parse/echo', { tag: 'x' })).rejects.toThrow();
85
+ });
86
+ /**
87
+ * #831. Refusing is half a contract; saying WHICH field was wrong is the half a
88
+ * client — or a build agent — recovers from without a person reading a log.
89
+ *
90
+ * This belongs beside the refusals above rather than in `contracts` alone, because
91
+ * the two substrates lose the answer differently. Under `adapter-sqlite` the parse
92
+ * throws in-process and the `ZodError` arrives with `issues` intact. Under
93
+ * `adapter-cloudflare` it is raised INSIDE the ScopeDO and crosses the hop, where a
94
+ * throw carries only its message — so the field list used to survive nowhere but as
95
+ * JSON inside that message, and every vertical re-parsed the string to get it back.
96
+ *
97
+ * A suite that ran on one adapter would have called that fixed.
98
+ */
99
+ it('names the field that was wrong, whichever substrate refused it', async () => {
100
+ const err = await stub.invoke('parse/echo', { name: 42 }).then(() => {
101
+ throw new Error('the invoke should have been refused');
102
+ }, (e) => e);
103
+ expect(errorCodeOf(err)).toBe('validation_failed');
104
+ const body = toProblem(err);
105
+ expect(body.status).toBe(400);
106
+ expect(body.errors?.map((issue) => issue.path)).toContain('name');
107
+ });
108
+ it('strips a field the declaration does not name', async () => {
109
+ // The undeclared key is gone before module code sees it — so a caller
110
+ // cannot smuggle a field past a handler that reads more than it declared.
111
+ const out = await received('parse/echo', { name: 'a', unexpected: 'smuggled' });
112
+ expect(out).not.toHaveProperty('unexpected');
113
+ expect(out).toMatchObject({ name: 'a' });
114
+ });
115
+ it("applies the declaration's own default", async () => {
116
+ // The case a handler cannot fake by being careful: `size` arrives SET on a
117
+ // call that never sent it, which only a parse against the declaration does.
118
+ expect(await received('parse/echo', { name: 'a' })).toMatchObject({ size: 7 });
119
+ });
120
+ it('keeps an optional declared field absent rather than inventing it', async () => {
121
+ const out = await received('parse/echo', { name: 'a' });
122
+ expect(out).not.toHaveProperty('tag');
123
+ });
124
+ it('lets the platform page trio through a paged operation (#811)', async () => {
125
+ // A strict parse against `input` alone would strip these back out and hand
126
+ // every paged read an unpaged request.
127
+ const out = await received('parse/paged', { q: 'find', limit: 5, cursor: 'c1', order: 'desc', sort: 'name' });
128
+ expect(out).toMatchObject({ q: 'find', limit: 5, cursor: 'c1', order: 'desc', sort: 'name' });
129
+ });
130
+ it('rejects a page value outside the platform ceiling', async () => {
131
+ await expect(stub.invoke('parse/paged', { limit: 10_000 })).rejects.toThrow();
132
+ });
133
+ it('hands an operation that declares no input its `undefined` unchanged', async () => {
134
+ // `z.object({})` cannot say "no body", so the host must not invent one:
135
+ // a handler typed for `undefined` is not one accepting `{}`.
136
+ expect(await received('parse/bare')).toBeNull();
137
+ });
138
+ it('refuses a schema declared for an operation the module does not bind', () => {
139
+ // A schema on nothing enforces nothing while reading as coverage — the
140
+ // same reasoning `checksDeclaredElsewhere` applies to a stale exemption.
141
+ expect(() => host.registerModule({
142
+ manifest: { ...parseModManifest, id: moduleId.parse('@test/parse-unbound') },
143
+ operations: { 'unbound/act': (() => 'ran') },
144
+ operationInputs: { 'unbound/nope': { parse: (v) => v } },
145
+ })).toThrow(/unbound/i);
146
+ });
147
+ });
148
+ }
149
+ //# sourceMappingURL=input-parse-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-parse-suite.js","sourceRoot":"","sources":["../src/input-parse-suite.ts"],"names":[],"mappings":"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"}