@substrat-run/contract-tests 0.147.0 → 0.149.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 (89) hide show
  1. package/dist/bind-module.d.ts +4 -0
  2. package/dist/bind-module.d.ts.map +1 -1
  3. package/dist/capability-expiry-suite.d.ts.map +1 -1
  4. package/dist/capability-expiry-suite.js +6 -4
  5. package/dist/capability-expiry-suite.js.map +1 -1
  6. package/dist/counter-runner.d.ts +22 -0
  7. package/dist/counter-runner.d.ts.map +1 -0
  8. package/dist/counter-runner.js +96 -0
  9. package/dist/counter-runner.js.map +1 -0
  10. package/dist/derived-module.d.ts +4 -0
  11. package/dist/derived-module.d.ts.map +1 -1
  12. package/dist/directory-restore-suite.d.ts.map +1 -1
  13. package/dist/directory-restore-suite.js +66 -2
  14. package/dist/directory-restore-suite.js.map +1 -1
  15. package/dist/directory-shapes.d.ts +6 -0
  16. package/dist/directory-shapes.d.ts.map +1 -1
  17. package/dist/directory-shapes.js +42 -0
  18. package/dist/directory-shapes.js.map +1 -1
  19. package/dist/entity-state-module.d.ts +8 -0
  20. package/dist/entity-state-module.d.ts.map +1 -1
  21. package/dist/entity-state-module.js +3 -0
  22. package/dist/entity-state-module.js.map +1 -1
  23. package/dist/entity-trash-module.d.ts +4 -0
  24. package/dist/entity-trash-module.d.ts.map +1 -1
  25. package/dist/entity-trash-suite.d.ts.map +1 -1
  26. package/dist/entity-trash-suite.js +20 -0
  27. package/dist/entity-trash-suite.js.map +1 -1
  28. package/dist/erasure-module.d.ts +4 -0
  29. package/dist/erasure-module.d.ts.map +1 -1
  30. package/dist/held-migrations.d.ts +27 -0
  31. package/dist/held-migrations.d.ts.map +1 -0
  32. package/dist/held-migrations.js +39 -0
  33. package/dist/held-migrations.js.map +1 -0
  34. package/dist/index.d.ts +12 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +13 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/migration-fence-suite.d.ts +3 -0
  39. package/dist/migration-fence-suite.d.ts.map +1 -0
  40. package/dist/migration-fence-suite.js +173 -0
  41. package/dist/migration-fence-suite.js.map +1 -0
  42. package/dist/migration-fence.d.ts +42 -0
  43. package/dist/migration-fence.d.ts.map +1 -0
  44. package/dist/migration-fence.js +66 -0
  45. package/dist/migration-fence.js.map +1 -0
  46. package/dist/modules.d.ts +126 -0
  47. package/dist/modules.d.ts.map +1 -1
  48. package/dist/modules.js +23 -0
  49. package/dist/modules.js.map +1 -1
  50. package/dist/orphan-sweep-suite.d.ts +23 -0
  51. package/dist/orphan-sweep-suite.d.ts.map +1 -0
  52. package/dist/orphan-sweep-suite.js +411 -0
  53. package/dist/orphan-sweep-suite.js.map +1 -0
  54. package/dist/permission-suite.d.ts.map +1 -1
  55. package/dist/permission-suite.js +19 -0
  56. package/dist/permission-suite.js.map +1 -1
  57. package/dist/scope-host-suite.d.ts.map +1 -1
  58. package/dist/scope-host-suite.js +465 -10
  59. package/dist/scope-host-suite.js.map +1 -1
  60. package/dist/scope-store-runner-suite.d.ts +46 -0
  61. package/dist/scope-store-runner-suite.d.ts.map +1 -0
  62. package/dist/scope-store-runner-suite.js +187 -0
  63. package/dist/scope-store-runner-suite.js.map +1 -0
  64. package/dist/scope-store-suite.d.ts.map +1 -1
  65. package/dist/scope-store-suite.js +55 -103
  66. package/dist/scope-store-suite.js.map +1 -1
  67. package/dist/stamps-module.d.ts +4 -0
  68. package/dist/stamps-module.d.ts.map +1 -1
  69. package/dist/store-driver-release-suite.d.ts +12 -0
  70. package/dist/store-driver-release-suite.d.ts.map +1 -0
  71. package/dist/store-driver-release-suite.js +561 -0
  72. package/dist/store-driver-release-suite.js.map +1 -0
  73. package/dist/upload-link-module.d.ts +172 -0
  74. package/dist/upload-link-module.d.ts.map +1 -0
  75. package/dist/upload-link-module.js +157 -0
  76. package/dist/upload-link-module.js.map +1 -0
  77. package/dist/upload-link-replay-suite.d.ts +3 -0
  78. package/dist/upload-link-replay-suite.d.ts.map +1 -0
  79. package/dist/upload-link-replay-suite.js +291 -0
  80. package/dist/upload-link-replay-suite.js.map +1 -0
  81. package/dist/upload-link-suite.d.ts +3 -0
  82. package/dist/upload-link-suite.d.ts.map +1 -0
  83. package/dist/upload-link-suite.js +485 -0
  84. package/dist/upload-link-suite.js.map +1 -0
  85. package/dist/vertical-events-suite.d.ts +8 -0
  86. package/dist/vertical-events-suite.d.ts.map +1 -1
  87. package/dist/vertical-events-suite.js +46 -4
  88. package/dist/vertical-events-suite.js.map +1 -1
  89. package/package.json +3 -3
@@ -0,0 +1,46 @@
1
+ import { type StoreRequest } from '@substrat-run/contracts';
2
+ import { type ScopeStoreRunner } from '@substrat-run/kernel';
3
+ /**
4
+ * What a driver hands the runner suite. The suite knows nothing about the driver beyond
5
+ * two calls of its own catalog.
6
+ */
7
+ export interface ScopeStoreRunnerFixture {
8
+ /** A runner nobody has started. Called once per check. */
9
+ makeRunner(): ScopeStoreRunner | Promise<ScopeStoreRunner>;
10
+ /** A driver operation that changes the object. Each call must change what `read` answers. */
11
+ readonly write: StoreRequest;
12
+ /** A driver operation that reads the object back. Equal answers mean equal objects. */
13
+ readonly read: StoreRequest;
14
+ }
15
+ /** One check of the runner contract. It throws when the runner breaks it. */
16
+ export interface ScopeStoreRunnerCheck {
17
+ readonly name: string;
18
+ run(): Promise<void>;
19
+ }
20
+ /**
21
+ * The runner contract (#2213, RFC §7) as a list of checks, so a broken runner can be
22
+ * shown to fail the one it breaks. {@link scopeStoreRunnerContractSuite} runs them all.
23
+ *
24
+ * Every call goes through the kernel's callers both hosts use (`callScopeStore`,
25
+ * `callReservedStoreOperation`), so an answer is held to the same envelope and result
26
+ * schema a host would hold it to.
27
+ *
28
+ * A restart is `stop()` then `start()` on the same runner instance: what a host does
29
+ * between a `close` and the next first store call. A driver's repositories must survive
30
+ * it. Blob cells in an export are not asserted (their spelling over JSON is not settled).
31
+ */
32
+ export declare function scopeStoreRunnerChecks(fixture: ScopeStoreRunnerFixture): ScopeStoreRunnerCheck[];
33
+ /**
34
+ * The runner contract a scope-store driver's runner passes (#2213). A driver runs it in its
35
+ * own repository, against its own runner: the platform runs only the trivial driver.
36
+ *
37
+ * ```ts
38
+ * scopeStoreRunnerContractSuite('graph', {
39
+ * makeRunner: () => graphRunner({ dir }),
40
+ * write: { operation: 'apply', input: { … } },
41
+ * read: { operation: 'inspect' },
42
+ * });
43
+ * ```
44
+ */
45
+ export declare function scopeStoreRunnerContractSuite(driver: string, fixture: ScopeStoreRunnerFixture): void;
46
+ //# sourceMappingURL=scope-store-runner-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-store-runner-suite.d.ts","sourceRoot":"","sources":["../src/scope-store-runner-suite.ts"],"names":[],"mappings":"AACA,OAAO,EAA8C,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACxG,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAE9B;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,0DAA0D;IAC1D,UAAU,IAAI,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC3D,6FAA6F;IAC7F,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;CAC7B;AAED,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAUD;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,qBAAqB,EAAE,CAuJhG;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAMpG"}
@@ -0,0 +1,187 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { storeResponse } from '@substrat-run/contracts';
3
+ import { callReservedStoreOperation, callScopeStore, ulid, } from '@substrat-run/kernel';
4
+ /** An address no other check, and no earlier run, has used. */
5
+ function freshAddress() {
6
+ return { tenantId: ulid(), vertical: 'conformance', scopeId: ulid(), binding: 'STORE' };
7
+ }
8
+ /** An operation name no driver declares, and still a valid label. */
9
+ const UNKNOWN_OPERATION = 'conformance-unknown-operation';
10
+ /**
11
+ * The runner contract (#2213, RFC §7) as a list of checks, so a broken runner can be
12
+ * shown to fail the one it breaks. {@link scopeStoreRunnerContractSuite} runs them all.
13
+ *
14
+ * Every call goes through the kernel's callers both hosts use (`callScopeStore`,
15
+ * `callReservedStoreOperation`), so an answer is held to the same envelope and result
16
+ * schema a host would hold it to.
17
+ *
18
+ * A restart is `stop()` then `start()` on the same runner instance: what a host does
19
+ * between a `close` and the next first store call. A driver's repositories must survive
20
+ * it. Blob cells in an export are not asserted (their spelling over JSON is not settled).
21
+ */
22
+ export function scopeStoreRunnerChecks(fixture) {
23
+ /** A started runner for one check, stopped after it whatever happens. */
24
+ const withRunner = async (body) => {
25
+ const runner = await fixture.makeRunner();
26
+ await runner.start();
27
+ try {
28
+ await body(runner);
29
+ }
30
+ finally {
31
+ await runner.stop();
32
+ }
33
+ };
34
+ const call = (runner, address, request) => callScopeStore((r) => runner.invoke(address, r), request.operation, request.input);
35
+ const read = (runner, address) => call(runner, address, fixture.read);
36
+ const write = (runner, address) => call(runner, address, fixture.write);
37
+ const reserved = (runner, address, operation, input) => callReservedStoreOperation((r) => runner.invoke(address, r), operation, input);
38
+ return [
39
+ {
40
+ name: 'a stop without a start resolves',
41
+ async run() {
42
+ const runner = await fixture.makeRunner();
43
+ await runner.stop();
44
+ },
45
+ },
46
+ {
47
+ name: 'a driver call answers the envelope, and an unknown operation answers its error',
48
+ async run() {
49
+ await withRunner(async (runner) => {
50
+ const address = freshAddress();
51
+ expect(storeResponse.safeParse(await runner.invoke(address, fixture.read)).success).toBe(true);
52
+ const unknown = storeResponse.parse(await runner.invoke(address, { operation: UNKNOWN_OPERATION }));
53
+ expect('error' in unknown).toBe(true);
54
+ });
55
+ },
56
+ },
57
+ {
58
+ name: 'a write is read back on its own address',
59
+ async run() {
60
+ await withRunner(async (runner) => {
61
+ const address = freshAddress();
62
+ const before = await read(runner, address);
63
+ await write(runner, address);
64
+ expect(await read(runner, address)).not.toEqual(before);
65
+ });
66
+ },
67
+ },
68
+ {
69
+ name: 'the object is keyed on the whole address: tenant, vertical, scope and binding',
70
+ async run() {
71
+ await withRunner(async (runner) => {
72
+ const address = freshAddress();
73
+ const empty = await read(runner, freshAddress());
74
+ await write(runner, address);
75
+ // Each neighbour differs from the written address in exactly one field.
76
+ const neighbours = [
77
+ { ...address, tenantId: ulid() },
78
+ { ...address, vertical: 'conformance-other' },
79
+ { ...address, scopeId: ulid() },
80
+ { ...address, binding: 'OTHER' },
81
+ ];
82
+ for (const neighbour of neighbours) {
83
+ expect(await read(runner, neighbour), JSON.stringify(neighbour)).toEqual(empty);
84
+ }
85
+ });
86
+ },
87
+ },
88
+ {
89
+ name: 'a stop and a start serve the same repositories',
90
+ async run() {
91
+ await withRunner(async (runner) => {
92
+ const address = freshAddress();
93
+ await write(runner, address);
94
+ const written = await read(runner, address);
95
+ await runner.stop();
96
+ await runner.start();
97
+ expect(await read(runner, address)).toEqual(written);
98
+ });
99
+ },
100
+ },
101
+ {
102
+ name: 'substrat.size answers { bytes }',
103
+ async run() {
104
+ await withRunner(async (runner) => {
105
+ const address = freshAddress();
106
+ await write(runner, address);
107
+ const { bytes } = await reserved(runner, address, 'substrat.size');
108
+ expect(Number.isInteger(bytes)).toBe(true);
109
+ });
110
+ },
111
+ },
112
+ {
113
+ name: 'substrat.export answers { tables } in the scope-dump table shape',
114
+ async run() {
115
+ await withRunner(async (runner) => {
116
+ const address = freshAddress();
117
+ await write(runner, address);
118
+ const { tables } = await reserved(runner, address, 'substrat.export');
119
+ expect(tables.length).toBeGreaterThan(0);
120
+ });
121
+ },
122
+ },
123
+ {
124
+ name: 'substrat.import replaces the whole object and answers the tables it wrote',
125
+ async run() {
126
+ await withRunner(async (runner) => {
127
+ const source = freshAddress();
128
+ const target = freshAddress();
129
+ await write(runner, source);
130
+ await write(runner, target);
131
+ await write(runner, target);
132
+ // The target holds more than the source, so a merge cannot pass for a replace.
133
+ expect(await read(runner, target)).not.toEqual(await read(runner, source));
134
+ const { tables } = await reserved(runner, source, 'substrat.export');
135
+ const imported = await reserved(runner, target, 'substrat.import', { tables });
136
+ expect(imported.tables).toBe(tables.length);
137
+ expect(await read(runner, target)).toEqual(await read(runner, source));
138
+ });
139
+ },
140
+ },
141
+ {
142
+ name: 'substrat.wipe answers {} and leaves an empty object, also where nothing was written',
143
+ async run() {
144
+ await withRunner(async (runner) => {
145
+ const address = freshAddress();
146
+ const empty = await read(runner, freshAddress());
147
+ await write(runner, address);
148
+ expect(await reserved(runner, address, 'substrat.wipe')).toEqual({});
149
+ expect(await read(runner, address)).toEqual(empty);
150
+ expect(await reserved(runner, freshAddress(), 'substrat.wipe')).toEqual({});
151
+ });
152
+ },
153
+ },
154
+ {
155
+ name: 'substrat.erase answers { erased }, and a repeat erases nothing more',
156
+ async run() {
157
+ await withRunner(async (runner) => {
158
+ const address = freshAddress();
159
+ const subjectId = ulid();
160
+ await write(runner, address);
161
+ await reserved(runner, address, 'substrat.erase', { subjectId });
162
+ expect(await reserved(runner, address, 'substrat.erase', { subjectId })).toEqual({ erased: 0 });
163
+ });
164
+ },
165
+ },
166
+ ];
167
+ }
168
+ /**
169
+ * The runner contract a scope-store driver's runner passes (#2213). A driver runs it in its
170
+ * own repository, against its own runner: the platform runs only the trivial driver.
171
+ *
172
+ * ```ts
173
+ * scopeStoreRunnerContractSuite('graph', {
174
+ * makeRunner: () => graphRunner({ dir }),
175
+ * write: { operation: 'apply', input: { … } },
176
+ * read: { operation: 'inspect' },
177
+ * });
178
+ * ```
179
+ */
180
+ export function scopeStoreRunnerContractSuite(driver, fixture) {
181
+ describe(`scope-store runner (#2213): ${driver}`, () => {
182
+ for (const check of scopeStoreRunnerChecks(fixture)) {
183
+ it(check.name, () => check.run());
184
+ }
185
+ });
186
+ }
187
+ //# sourceMappingURL=scope-store-runner-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-store-runner-suite.js","sourceRoot":"","sources":["../src/scope-store-runner-suite.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"scope-store-suite.d.ts","sourceRoot":"","sources":["../src/scope-store-suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgD,KAAK,gBAAgB,EAAwB,MAAM,sBAAsB,CAAC;AAEjI,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAuG9D;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,CACX,WAAW,EAAE;IAAE,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,gBAAgB,CAAA;KAAE,EAAE,CAAA;CAAE,KACtE,OAAO,CAAC,gBAAgB,CAAC,GAC7B,IAAI,CA2jBN"}
1
+ {"version":3,"file":"scope-store-suite.d.ts","sourceRoot":"","sources":["../src/scope-store-suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,gBAAgB,EAAwB,MAAM,sBAAsB,CAAC;AAGzG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAiD9D;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,CACX,WAAW,EAAE;IAAE,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,gBAAgB,CAAA;KAAE,EAAE,CAAA;CAAE,KACtE,OAAO,CAAC,gBAAgB,CAAC,GAC7B,IAAI,CA8jBN"}
@@ -1,104 +1,53 @@
1
1
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { errorCodeOf, platformActorId, principalId, scopeId, tenantId } from '@substrat-run/contracts';
2
+ import { errorCodeOf, platformActorId, principalId, scopeId, storeEraseInput, tenantId } from '@substrat-run/contracts';
3
3
  import { ulid } from '@substrat-run/kernel';
4
4
  import { STORE_CALL_SINK_KEY, tallyStoreCall } from '@substrat-run/kernel/invocation-line';
5
+ import { counterScopeStoreRunner, invalidStoreInput, objectNameOf } from './counter-runner.js';
5
6
  /**
6
- * A counter in the process. One number per address, so two scopes and two
7
- * verticals stay apart when the host passes the address it built.
7
+ * The trivial driver, with what this suite reads back about the host's calls:
8
+ * `address` (what the host handed in), `erased` (the subjects erased on this object),
9
+ * `wiped` (every object wiped), and `fail-wipe` (`true` makes each later `substrat.wipe`
10
+ * answer 503, until `false`).
8
11
  */
9
- function counterRunner() {
10
- const objects = new Map();
12
+ function instrumentedCounterRunner() {
13
+ const counter = counterScopeStoreRunner();
11
14
  const erased = new Map();
12
15
  const wiped = new Set();
13
- // `fail-wipe` makes every later `substrat.wipe` fail, until it is turned off again.
14
16
  let failWipe = false;
15
- const key = (address) => `${address.tenantId}/${address.vertical}/${address.scopeId}/${address.binding}`;
16
17
  return {
17
- kind: 'counter',
18
- async start() { },
19
- async stop() { },
18
+ ...counter,
20
19
  async invoke(address, request) {
21
- const current = objects.get(key(address)) ?? 0;
22
- if (request.operation === 'read')
23
- return { result: current };
24
- if (request.operation === 'address') {
25
- return {
26
- result: {
27
- tenantId: address.tenantId,
28
- vertical: address.vertical,
29
- scopeId: address.scopeId,
30
- binding: address.binding,
31
- },
32
- };
33
- }
34
- if (request.operation === 'increment') {
35
- const by = typeof request.input === 'object' &&
36
- request.input !== null &&
37
- typeof Reflect.get(request.input, 'by') === 'number'
38
- ? Number(Reflect.get(request.input, 'by'))
39
- : 1;
40
- const next = current + by;
41
- objects.set(key(address), next);
42
- return { result: next };
43
- }
44
- if (request.operation === 'substrat.size')
45
- return { result: { bytes: current } };
46
- if (request.operation === 'fail-wipe') {
47
- failWipe = request.input === true;
48
- return { result: failWipe };
49
- }
50
- if (request.operation === 'substrat.wipe') {
51
- if (failWipe)
52
- return { error: { code: 'unavailable', status: 503 } };
53
- objects.set(key(address), 0);
54
- wiped.add(key(address));
55
- return { result: {} };
56
- }
57
- if (request.operation === 'wiped')
58
- return { result: [...wiped] };
59
- if (request.operation === 'substrat.erase') {
60
- const subjectId = typeof request.input === 'object' && request.input !== null
61
- ? Reflect.get(request.input, 'subjectId')
62
- : undefined;
63
- if (typeof subjectId !== 'string' || subjectId.length === 0) {
64
- return { error: { code: 'validation_failed', status: 400 } };
20
+ const id = objectNameOf(address);
21
+ switch (request.operation) {
22
+ case 'address': {
23
+ const { tenantId, vertical, scopeId, binding } = address;
24
+ return { result: { tenantId, vertical, scopeId, binding } };
65
25
  }
66
- const subjects = erased.get(key(address)) ?? new Set();
67
- const already = subjects.has(subjectId);
68
- subjects.add(subjectId);
69
- erased.set(key(address), subjects);
70
- return { result: { erased: already ? 0 : 1 } };
71
- }
72
- if (request.operation === 'erased') {
73
- return { result: [...(erased.get(key(address)) ?? [])] };
74
- }
75
- if (request.operation === 'substrat.export') {
76
- return {
77
- result: {
78
- tables: [
79
- {
80
- name: 'counter',
81
- ddl: 'CREATE TABLE counter (n INTEGER NOT NULL)',
82
- columns: ['n'],
83
- rows: [[current]],
84
- },
85
- ],
86
- },
87
- };
88
- }
89
- if (request.operation === 'substrat.import') {
90
- const tables = typeof request.input === 'object' && request.input !== null
91
- ? Reflect.get(request.input, 'tables')
92
- : undefined;
93
- const count = Array.isArray(tables) ? tables.length : 0;
94
- const first = Array.isArray(tables) ? tables[0] : undefined;
95
- const rows = typeof first === 'object' && first !== null ? Reflect.get(first, 'rows') : undefined;
96
- const row = Array.isArray(rows) ? rows[0] : undefined;
97
- const cell = Array.isArray(row) ? row[0] : undefined;
98
- objects.set(key(address), typeof cell === 'number' ? cell : 0);
99
- return { result: { tables: count } };
26
+ case 'erased':
27
+ return { result: [...(erased.get(id) ?? [])] };
28
+ case 'wiped':
29
+ return { result: [...wiped] };
30
+ case 'fail-wipe':
31
+ if (typeof request.input !== 'boolean')
32
+ return invalidStoreInput('fail-wipe takes a boolean');
33
+ failWipe = request.input;
34
+ return { result: failWipe };
35
+ case 'substrat.wipe':
36
+ if (failWipe)
37
+ return { error: { code: 'unavailable', status: 503 } };
38
+ wiped.add(id);
39
+ return counter.invoke(address, request);
40
+ case 'substrat.erase': {
41
+ const answer = await counter.invoke(address, request);
42
+ if ('result' in answer) {
43
+ const { subjectId } = storeEraseInput.parse(request.input);
44
+ erased.set(id, (erased.get(id) ?? new Set()).add(subjectId));
45
+ }
46
+ return answer;
47
+ }
48
+ default:
49
+ return counter.invoke(address, request);
100
50
  }
101
- return { error: { code: 'unknown_operation', status: 400 } };
102
51
  },
103
52
  };
104
53
  }
@@ -117,7 +66,7 @@ export function scopeStoreContractSuite(adapterName, makeFixture) {
117
66
  const scopeB = scopeId.parse(ulid());
118
67
  const otherVertical = scopeId.parse(ulid());
119
68
  beforeAll(async () => {
120
- fixture = await makeFixture({ runners: [{ binding: 'GRAPH', runner: counterRunner() }] });
69
+ fixture = await makeFixture({ runners: [{ binding: 'GRAPH', runner: instrumentedCounterRunner() }] });
121
70
  host = fixture.host;
122
71
  await host.admin.createTenant(staff, { id: tenant, slug: `t-${tenant.toLowerCase()}`, name: 'T' });
123
72
  await host.provisionScope(staff, { tenantId: tenant, scopeId: scopeA, vertical: 'docs' });
@@ -149,7 +98,7 @@ export function scopeStoreContractSuite(adapterName, makeFixture) {
149
98
  release = resolve;
150
99
  });
151
100
  const slow = {
152
- kind: 'slow',
101
+ driver: 'slow',
153
102
  async start() {
154
103
  started += 1;
155
104
  await gate;
@@ -222,12 +171,15 @@ export function scopeStoreContractSuite(adapterName, makeFixture) {
222
171
  const refusal = await host.admin.scopeStoreSizes(staff, tenant, scopeId.parse(ulid())).then(() => undefined, (err) => err);
223
172
  expect(errorCodeOf(refusal)).toBe('not_found');
224
173
  });
225
- it('an erasure reaches the store, and a second one counts zero', async () => {
174
+ it("an erasure reaches the store and takes the subject's part, and a second one counts zero", async () => {
226
175
  const subject = ulid();
176
+ const store = await host.getScopeStore(principal, tenant, scopeA, 'GRAPH');
177
+ const before = Number(await store.invoke('read'));
178
+ await store.invoke('increment', { by: 2, subjectId: subject });
227
179
  const first = await host.admin.shredSubject(staff, tenant, scopeA, subject);
228
180
  expect(first.storeErasures).toEqual([{ binding: 'GRAPH', erased: 1 }]);
229
- const store = await host.getScopeStore(principal, tenant, scopeA, 'GRAPH');
230
181
  expect(await store.invoke('erased')).toEqual([subject]);
182
+ expect(await store.invoke('read')).toBe(before);
231
183
  const again = await host.admin.shredSubject(staff, tenant, scopeA, subject);
232
184
  expect(again.storeErasures).toEqual([{ binding: 'GRAPH', erased: 0 }]);
233
185
  });
@@ -246,7 +198,7 @@ export function scopeStoreContractSuite(adapterName, makeFixture) {
246
198
  await store.invoke('increment', { by: 4 });
247
199
  const dump = await host.admin.exportScope(staff, tenant, scopeB);
248
200
  expect(dump.stores).toEqual([
249
- expect.objectContaining({ binding: 'GRAPH', kind: 'counter' }),
201
+ expect.objectContaining({ binding: 'GRAPH', driver: 'counter' }),
250
202
  ]);
251
203
  await store.invoke('increment', { by: 1 });
252
204
  await host.restoreScope(staff, tenant, scopeB, dump);
@@ -262,16 +214,16 @@ export function scopeStoreContractSuite(adapterName, makeFixture) {
262
214
  const refusal = await host.restoreScope(staff, tenant, scopeB, {
263
215
  ...dump,
264
216
  tables: [...dump.tables, PROBE_TABLE],
265
- stores: [...dump.stores, { binding: 'MISSING', kind: 'counter', tables: [] }],
217
+ stores: [...dump.stores, { binding: 'MISSING', driver: 'counter', tables: [] }],
266
218
  }).then(() => undefined, (err) => err);
267
219
  expect(errorCodeOf(refusal)).toBe('not_found');
268
220
  // Neither the restore of the dump's GRAPH (before + 3) nor its tables were written.
269
221
  expect(await store.invoke('read')).toBe(before + 4);
270
222
  expect(await tableNames(scopeB)).not.toContain(PROBE_TABLE.name);
271
223
  });
272
- // L8: a store of another kind would be handed to this kind's import, and a destination
224
+ // L8: a store of another driver would be handed to this driver's import, and a destination
273
225
  // with no vertical has no store object to load into. Both refuse before any write.
274
- it('a restore whose store is of another kind writes nothing', async () => {
226
+ it('a restore whose store is of another driver writes nothing', async () => {
275
227
  const store = await host.getScopeStore(principal, tenant, scopeB, 'GRAPH');
276
228
  const before = Number(await store.invoke('read'));
277
229
  const dump = await host.admin.exportScope(staff, tenant, scopeB);
@@ -279,15 +231,15 @@ export function scopeStoreContractSuite(adapterName, makeFixture) {
279
231
  const refusal = await host.restoreScope(staff, tenant, scopeB, {
280
232
  ...dump,
281
233
  tables: [...dump.tables, PROBE_TABLE],
282
- stores: dump.stores.map((one) => ({ ...one, kind: 'other' })),
234
+ stores: dump.stores.map((one) => ({ ...one, driver: 'other' })),
283
235
  }).then(() => undefined, (err) => err);
284
236
  expect(errorCodeOf(refusal)).toBe('conflict');
285
237
  expect(await store.invoke('read')).toBe(before + 2);
286
238
  expect(await tableNames(scopeB)).not.toContain(PROBE_TABLE.name);
287
239
  });
288
240
  // CodeRabbit on #2244: the host does not parse the dump it is handed, so a store with no
289
- // kind skipped the kind comparison above and was imported.
290
- it('a restore whose store names no kind writes nothing', async () => {
241
+ // driver skipped the driver comparison above and was imported.
242
+ it('a restore whose store names no driver writes nothing', async () => {
291
243
  const store = await host.getScopeStore(principal, tenant, scopeB, 'GRAPH');
292
244
  const before = Number(await store.invoke('read'));
293
245
  const dump = await host.admin.exportScope(staff, tenant, scopeB);
@@ -336,7 +288,7 @@ export function scopeStoreContractSuite(adapterName, makeFixture) {
336
288
  tenantId: tenant,
337
289
  scopeId: dest,
338
290
  vertical: 'docs',
339
- }, { ...dump, stores: [...dump.stores, { binding: 'MISSING', kind: 'counter', tables: [] }] }).then(() => undefined, (err) => err);
291
+ }, { ...dump, stores: [...dump.stores, { binding: 'MISSING', driver: 'counter', tables: [] }] }).then(() => undefined, (err) => err);
340
292
  expect(errorCodeOf(refusal)).toBe('not_found');
341
293
  expect(await host.admin.getScopeRecord(staff, tenant, dest)).toBeUndefined();
342
294
  });
@@ -383,7 +335,7 @@ export function scopeStoreContractSuite(adapterName, makeFixture) {
383
335
  expect(refusal).toBeDefined();
384
336
  expect((await host.admin.getScopeRecord(staff, tenant, doomed))?.status).toBe('archived');
385
337
  const dump = await host.admin.exportScope(staff, tenant, doomed);
386
- expect(dump.stores.find((one) => one.binding === 'GRAPH')?.tables[0]?.rows).toEqual([[7]]);
338
+ expect(dump.stores.find((one) => one.binding === 'GRAPH')?.tables[0]?.rows).toEqual([[null, 7]]);
387
339
  expect(dump.tables.map((table) => table.name)).toContain(PROBE_TABLE.name);
388
340
  }
389
341
  finally {
@@ -423,7 +375,7 @@ export function scopeStoreContractSuite(adapterName, makeFixture) {
423
375
  expect(await loud.invoke('read')).toEqual(expect.any(Number));
424
376
  });
425
377
  it('with no observer, a call lands on the request now running; a passed one wins (#2245)', async () => {
426
- // The platform entry installs this slot (vertical-host's `runRecordingStoreCalls`);
378
+ // The platform entry installs this slot (vertical-host's `runRecording`);
427
379
  // here a fixed tally stands in for the request's record.
428
380
  const key = Symbol.for(STORE_CALL_SINK_KEY);
429
381
  const g = globalThis;