@substrat-run/contract-tests 0.142.0 → 0.144.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attachment-refusal-suite.d.ts +3 -0
- package/dist/attachment-refusal-suite.d.ts.map +1 -0
- package/dist/attachment-refusal-suite.js +128 -0
- package/dist/attachment-refusal-suite.js.map +1 -0
- package/dist/bind-module.d.ts +199 -0
- package/dist/bind-module.d.ts.map +1 -0
- package/dist/bind-module.js +104 -0
- package/dist/bind-module.js.map +1 -0
- package/dist/binding-invite-suite.d.ts +12 -0
- package/dist/binding-invite-suite.d.ts.map +1 -0
- package/dist/binding-invite-suite.js +507 -0
- package/dist/binding-invite-suite.js.map +1 -0
- package/dist/caused-by-module.d.ts.map +1 -1
- package/dist/caused-by-module.js +3 -2
- package/dist/caused-by-module.js.map +1 -1
- package/dist/derived-module.d.ts +690 -0
- package/dist/derived-module.d.ts.map +1 -0
- package/dist/derived-module.js +320 -0
- package/dist/derived-module.js.map +1 -0
- package/dist/derived-suite.d.ts +3 -0
- package/dist/derived-suite.d.ts.map +1 -0
- package/dist/derived-suite.js +481 -0
- package/dist/derived-suite.js.map +1 -0
- package/dist/entity-state-module.d.ts.map +1 -1
- package/dist/entity-state-module.js +12 -6
- package/dist/entity-state-module.js.map +1 -1
- package/dist/entity-trash-module.d.ts.map +1 -1
- package/dist/entity-trash-module.js +4 -4
- package/dist/entity-trash-module.js.map +1 -1
- package/dist/entity-trash-suite.d.ts.map +1 -1
- package/dist/entity-trash-suite.js +17 -8
- package/dist/entity-trash-suite.js.map +1 -1
- package/dist/erasure-module.d.ts.map +1 -1
- package/dist/erasure-module.js +3 -2
- package/dist/erasure-module.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/input-parse-suite.d.ts.map +1 -1
- package/dist/input-parse-suite.js +33 -2
- package/dist/input-parse-suite.js.map +1 -1
- package/dist/membership-module.d.ts.map +1 -1
- package/dist/membership-module.js +3 -2
- package/dist/membership-module.js.map +1 -1
- package/dist/migration-comments.d.ts.map +1 -1
- package/dist/migration-comments.js +3 -2
- package/dist/migration-comments.js.map +1 -1
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +78 -68
- package/dist/modules.js.map +1 -1
- package/dist/permission-suite.d.ts.map +1 -1
- package/dist/permission-suite.js +28 -0
- package/dist/permission-suite.js.map +1 -1
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +151 -31
- package/dist/scope-host-suite.js.map +1 -1
- package/dist/shape-backfill-ties.d.ts +29 -0
- package/dist/shape-backfill-ties.d.ts.map +1 -0
- package/dist/shape-backfill-ties.js +113 -0
- package/dist/shape-backfill-ties.js.map +1 -0
- package/dist/shape-reconcile-plan.d.ts +37 -0
- package/dist/shape-reconcile-plan.d.ts.map +1 -0
- package/dist/shape-reconcile-plan.js +143 -0
- package/dist/shape-reconcile-plan.js.map +1 -0
- package/dist/test-operations.d.ts +25 -0
- package/dist/test-operations.d.ts.map +1 -0
- package/dist/test-operations.js +22 -0
- package/dist/test-operations.js.map +1 -0
- package/dist/vertical-events-suite.d.ts.map +1 -1
- package/dist/vertical-events-suite.js +72 -7
- package/dist/vertical-events-suite.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAqBL,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAqBL,KAAK,SAAS,EAqBf,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAeL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAa9B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAuCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,aAAa,MACrB,OAAO,CAAC,OAAO,CAAC,QACb,SAAS,YACL,MAAM,KACf,OAAO,CAAC,IAAI,CAOd,CAAC;AAEF,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EAC5C,IAAI,GAAE,qBAA0B,GAC/B,IAAI,CAslTN"}
|
package/dist/scope-host-suite.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
import { connectorCalls, connectorTestFetch, resetConnectorCalls } from './connector-fixture.js';
|
|
3
|
-
import { connectionId, dataSubjectId, domainEvent, errorCodeOf, moduleId, SCOPE_GATE_REASONS, eventId, instant, moduleManifest, orgId, AUTO_ADMISSION_NOTE, permissionKey, platformActorId, platformRequestId, principalId, scopeId, tenantId, SCOPE_QUERY_ROW_MAX, KERNEL_AUTHORED_EVENT_TYPES, EMAIL_SENT, PLATFORM_REQUEST_ENTITY_TYPE, SEND_EMAIL_KIND, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, } from '@substrat-run/contracts';
|
|
3
|
+
import { connectionId, dataSubjectId, domainEvent, errorCodeOf, moduleId, SCOPE_GATE_REASONS, eventId, instant, moduleManifest, orgId, AUTO_ADMISSION_NOTE, permissionKey, platformActorId, platformRequestId, principalId, scopeId, tenantId, SCOPE_QUERY_ROW_MAX, KERNEL_AUTHORED_EVENT_TYPES, EMAIL_SENT, PLATFORM_REQUEST_ENTITY_TYPE, SEND_EMAIL_KIND, isUndecodablePlatformRequest, platformRequestEntry, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, } from '@substrat-run/contracts';
|
|
4
4
|
import { INERT_SCOPE_REASON, BACKFILL_MOVE_ID, SCOPE_COPY_LEASE_MS, isSearchIndexTable, platformIntentFailureMessage, REDACTED_DELIVERY_NOTE, REDACTED_FAILURE_NOTE, REDACTED_INTENT_MARKER, runPlatformSweep, startPlatformSweeper, createUlid, ulid, } from '@substrat-run/kernel';
|
|
5
5
|
import { billedMod, contractTestBareOps, contractTestInitialModules, gateModManifest, lateMod, TEST_LIFECYCLE, testModManifest, victimModManifest, } from './modules.js';
|
|
6
|
+
import { testOperations } from './test-operations.js';
|
|
7
|
+
/**
|
|
8
|
+
* A journal read whose every row is whole. A test that planted nothing has no business meeting
|
|
9
|
+
* an `UndecodablePlatformRequest` (#1637), so one fails here, naming why, rather than being
|
|
10
|
+
* narrowed away — the read a vertical makes is held to the same union, and must narrow too.
|
|
11
|
+
*/
|
|
12
|
+
function whole(entries) {
|
|
13
|
+
return entries.map((e) => {
|
|
14
|
+
if (isUndecodablePlatformRequest(e))
|
|
15
|
+
throw new Error(`unexpected undecodable intent row: ${e.decodeError}`);
|
|
16
|
+
return e;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
6
19
|
/**
|
|
7
20
|
* Assert a refusal by the CODE it declared, not by the sentence it happens to carry
|
|
8
21
|
* (#113 phase 5).
|
|
@@ -258,7 +271,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
258
271
|
it('the platform lists pending intents and settles them done (drain surface, Phase B)', async () => {
|
|
259
272
|
const stub = await host.getScope(alice, t1, s1);
|
|
260
273
|
const id = await stub.invoke('platform/request', { kind: 'provision-sibling', payload: { slug: 'padel' } });
|
|
261
|
-
const pending = await host.listPlatformRequests(t1, s1);
|
|
274
|
+
const pending = whole(await host.listPlatformRequests(t1, s1));
|
|
262
275
|
const mine = pending.find((r) => r.id === id);
|
|
263
276
|
expect(mine).toBeDefined();
|
|
264
277
|
expect(mine.kind).toBe('provision-sibling');
|
|
@@ -266,7 +279,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
266
279
|
expect(mine.payload).toEqual({ slug: 'padel' }); // JSON parsed back to the contract shape
|
|
267
280
|
await host.settlePlatformRequest(t1, s1, mine.id, { status: 'done', result: { scopeId: 'NEWSITE' } });
|
|
268
281
|
// A settled intent drops out of the pending list…
|
|
269
|
-
expect((await host.listPlatformRequests(t1, s1)).some((r) => r.id === id)).toBe(false);
|
|
282
|
+
expect((whole(await host.listPlatformRequests(t1, s1))).some((r) => r.id === id)).toBe(false);
|
|
270
283
|
// …and the row now reads done with its handler result recorded.
|
|
271
284
|
const rows = await stub.invoke('platform/read-requests');
|
|
272
285
|
const settled = rows.find((r) => r.id === id);
|
|
@@ -297,7 +310,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
297
310
|
// another request. The row stays exactly as it was.
|
|
298
311
|
await expect(host.settlePlatformRequest(t1, s1, platformRequestId.parse(id), { status: 'pending', event: event(id) })).rejects.toThrow();
|
|
299
312
|
await expect(host.settlePlatformRequest(t1, s1, platformRequestId.parse(id), { status: 'done', event: event(ulid()) })).rejects.toThrow();
|
|
300
|
-
expect((await host.listPlatformRequests(t1, s1)).find((r) => r.id === id)?.attempts).toBe(0);
|
|
313
|
+
expect((whole(await host.listPlatformRequests(t1, s1))).find((r) => r.id === id)?.attempts).toBe(0);
|
|
301
314
|
expect(await mine()).toEqual([]);
|
|
302
315
|
await host.settlePlatformRequest(t1, s1, platformRequestId.parse(id), { status: 'done', event: event(id) });
|
|
303
316
|
const written = await mine();
|
|
@@ -315,10 +328,10 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
315
328
|
it('a transient failure keeps the intent pending and preserves a two-phase result (retry)', async () => {
|
|
316
329
|
const stub = await host.getScope(alice, t1, s1);
|
|
317
330
|
const id = await stub.invoke('platform/request', { kind: 'provision-sibling', payload: {} });
|
|
318
|
-
const mine = (await host.listPlatformRequests(t1, s1)).find((r) => r.id === id);
|
|
331
|
+
const mine = (whole(await host.listPlatformRequests(t1, s1))).find((r) => r.id === id);
|
|
319
332
|
// Two-phase: record a minted id but keep the intent pending (crash-safe pre-write).
|
|
320
333
|
await host.settlePlatformRequest(t1, s1, mine.id, { status: 'pending', result: { scopeId: 'MINTED' } });
|
|
321
|
-
const still = (await host.listPlatformRequests(t1, s1)).find((r) => r.id === id);
|
|
334
|
+
const still = (whole(await host.listPlatformRequests(t1, s1))).find((r) => r.id === id);
|
|
322
335
|
expect(still.status).toBe('pending'); // still drainable
|
|
323
336
|
expect(still.result).toEqual({ scopeId: 'MINTED' });
|
|
324
337
|
expect(still.attempts).toBe(1);
|
|
@@ -332,15 +345,15 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
332
345
|
it('reads back settled intents — the journal both the platform and the vertical see (#618)', async () => {
|
|
333
346
|
const stub = await host.getScope(alice, t1, s1);
|
|
334
347
|
const id = await stub.invoke('platform/request', { kind: 'connector:test', payload: { doc: 1 } });
|
|
335
|
-
const row = (await host.listPlatformRequests(t1, s1)).find((r) => r.id === id);
|
|
348
|
+
const row = (whole(await host.listPlatformRequests(t1, s1))).find((r) => r.id === id);
|
|
336
349
|
// The failure this issue is about: a provider's own sentence, journaled and then
|
|
337
350
|
// unreachable from anywhere a builder would look.
|
|
338
351
|
const refusal = 'HTTP 409 requires valid personal number field';
|
|
339
352
|
await host.settlePlatformRequest(t1, s1, row.id, { status: 'failed', lastError: refusal });
|
|
340
353
|
// Settled, so it is gone from the DRAIN's read…
|
|
341
|
-
expect((await host.listPlatformRequests(t1, s1)).some((r) => r.id === id)).toBe(false);
|
|
354
|
+
expect((whole(await host.listPlatformRequests(t1, s1))).some((r) => r.id === id)).toBe(false);
|
|
342
355
|
// …and present, with the whole error, in the JOURNAL's.
|
|
343
|
-
const history = await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' });
|
|
356
|
+
const history = whole(await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' }));
|
|
344
357
|
const settled = history.find((r) => r.id === id);
|
|
345
358
|
expect(settled.status).toBe('failed');
|
|
346
359
|
expect(settled.lastError).toBe(refusal); // verbatim, never truncated
|
|
@@ -348,10 +361,10 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
348
361
|
expect(settled.settledAt).not.toBeNull();
|
|
349
362
|
// `kind` is an exact match, so one provider's traffic reads alone.
|
|
350
363
|
await stub.invoke('platform/request', { kind: 'provision-sibling', payload: {} });
|
|
351
|
-
expect((await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' })).every((r) => r.kind === 'connector:test')).toBe(true);
|
|
364
|
+
expect((whole(await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' }))).every((r) => r.kind === 'connector:test')).toBe(true);
|
|
352
365
|
// …as does `status`, and `limit` bounds the window.
|
|
353
|
-
expect((await host.listPlatformRequestHistory(t1, s1, { status: 'failed' })).every((r) => r.status === 'failed')).toBe(true);
|
|
354
|
-
expect((await host.listPlatformRequestHistory(t1, s1, { limit: 1 })).length).toBe(1);
|
|
366
|
+
expect((whole(await host.listPlatformRequestHistory(t1, s1, { status: 'failed' }))).every((r) => r.status === 'failed')).toBe(true);
|
|
367
|
+
expect((whole(await host.listPlatformRequestHistory(t1, s1, { limit: 1 }))).length).toBe(1);
|
|
355
368
|
});
|
|
356
369
|
/**
|
|
357
370
|
* #841. `lastError` says WHAT happened; this says WHO refused — and until it existed the
|
|
@@ -363,13 +376,13 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
363
376
|
it('journals WHO refused beside what was refused, and survives the round trip (#841)', async () => {
|
|
364
377
|
const stub = await host.getScope(alice, t1, s1);
|
|
365
378
|
const id = await stub.invoke('platform/request', { kind: 'connector:test', payload: { doc: 9 } });
|
|
366
|
-
const row = (await host.listPlatformRequests(t1, s1)).find((r) => r.id === id);
|
|
379
|
+
const row = (whole(await host.listPlatformRequests(t1, s1))).find((r) => r.id === id);
|
|
367
380
|
await host.settlePlatformRequest(t1, s1, row.id, {
|
|
368
381
|
status: 'failed',
|
|
369
382
|
lastError: 'permission denied: protocol:read',
|
|
370
383
|
failure: { origin: 'platform', code: 'permission_denied', permission: 'protocol:read' },
|
|
371
384
|
});
|
|
372
|
-
const settled = (await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' })).find((r) => r.id === id);
|
|
385
|
+
const settled = (whole(await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' }))).find((r) => r.id === id);
|
|
373
386
|
expect(settled.failure).toEqual({
|
|
374
387
|
origin: 'platform',
|
|
375
388
|
code: 'permission_denied',
|
|
@@ -385,9 +398,9 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
385
398
|
it('leaves attribution null when the settling caller did not attribute (#841)', async () => {
|
|
386
399
|
const stub = await host.getScope(alice, t1, s1);
|
|
387
400
|
const id = await stub.invoke('platform/request', { kind: 'connector:test', payload: { doc: 10 } });
|
|
388
|
-
const row = (await host.listPlatformRequests(t1, s1)).find((r) => r.id === id);
|
|
401
|
+
const row = (whole(await host.listPlatformRequests(t1, s1))).find((r) => r.id === id);
|
|
389
402
|
await host.settlePlatformRequest(t1, s1, row.id, { status: 'failed', lastError: 'something broke' });
|
|
390
|
-
const settled = (await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' })).find((r) => r.id === id);
|
|
403
|
+
const settled = (whole(await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' }))).find((r) => r.id === id);
|
|
391
404
|
expect(settled.lastError).toBe('something broke');
|
|
392
405
|
expect(settled.failure).toBeNull();
|
|
393
406
|
});
|
|
@@ -395,9 +408,9 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
395
408
|
const stub = await host.getScope(alice, t1, s1);
|
|
396
409
|
const first = await stub.invoke('platform/request', { kind: 'order-check', payload: { n: 1 } });
|
|
397
410
|
const second = await stub.invoke('platform/request', { kind: 'order-check', payload: { n: 2 } });
|
|
398
|
-
const rows = await host.listPlatformRequestHistory(t1, s1, { kind: 'order-check' });
|
|
411
|
+
const rows = whole(await host.listPlatformRequestHistory(t1, s1, { kind: 'order-check' }));
|
|
399
412
|
expect(rows.map((r) => r.id)).toEqual([second, first]);
|
|
400
|
-
expect((await host.listPlatformRequestHistory(t1, s1, { kind: 'order-check', limit: 1 }))[0].id).toBe(second);
|
|
413
|
+
expect((whole(await host.listPlatformRequestHistory(t1, s1, { kind: 'order-check', limit: 1 })))[0].id).toBe(second);
|
|
401
414
|
});
|
|
402
415
|
it('a vertical reads the outcome of its OWN intents in scope — ctx.platformRequests (#618)', async () => {
|
|
403
416
|
const stub = await host.getScope(alice, t1, s1);
|
|
@@ -425,8 +438,8 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
425
438
|
describe("a copy never runs the source's pending intents (#1686)", () => {
|
|
426
439
|
let source;
|
|
427
440
|
let id;
|
|
428
|
-
const pendingAt = async (s) => (await host.listPlatformRequests(t1, s)).map((r) => r.id);
|
|
429
|
-
const historyAt = async (s) => (await host.listPlatformRequestHistory(t1, s)).find((r) => r.id === id);
|
|
441
|
+
const pendingAt = async (s) => (whole(await host.listPlatformRequests(t1, s))).map((r) => r.id);
|
|
442
|
+
const historyAt = async (s) => (whole(await host.listPlatformRequestHistory(t1, s))).find((r) => r.id === id);
|
|
430
443
|
/** The copy: the intent is not drainable there, and its journal says why. */
|
|
431
444
|
const notCarriedAt = async (copy) => {
|
|
432
445
|
expect(await pendingAt(copy)).not.toContain(id);
|
|
@@ -466,7 +479,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
466
479
|
});
|
|
467
480
|
it('a restore into the scope the backup came from keeps it pending, payload and all', async () => {
|
|
468
481
|
await host.restoreScope(staff, t1, source, await host.admin.exportScope(staff, t1, source));
|
|
469
|
-
const back = (await host.listPlatformRequests(t1, source)).find((r) => r.id === id);
|
|
482
|
+
const back = (whole(await host.listPlatformRequests(t1, source))).find((r) => r.id === id);
|
|
470
483
|
expect(back).toMatchObject({ status: 'pending', payload: { slug: 'copied' }, lastError: null, settledAt: null });
|
|
471
484
|
});
|
|
472
485
|
});
|
|
@@ -551,7 +564,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
551
564
|
expect(count('copy-unattempted')).toBe(1);
|
|
552
565
|
// The consumer never ran on the copied event.
|
|
553
566
|
expect(await logOf(fork)).toEqual([]);
|
|
554
|
-
expect(await host.listPlatformRequests(t1, fork)).toEqual([]);
|
|
567
|
+
expect(whole(await host.listPlatformRequests(t1, fork))).toEqual([]);
|
|
555
568
|
// The retry reads settled, saying why, rather than vanishing.
|
|
556
569
|
const settled = (await host.executorDeadLetters(t1, fork)).find((d) => d.executorId === 'flaky-effector');
|
|
557
570
|
expect(settled?.error).toMatch(new RegExp(`^not carried: copied from scope ${source}`));
|
|
@@ -735,8 +748,8 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
735
748
|
};
|
|
736
749
|
/** All three reads, so each of the three decode sites is on the hook. */
|
|
737
750
|
const readAll = async (s) => ({
|
|
738
|
-
pending: await host.listPlatformRequests(t1, s),
|
|
739
|
-
history: await host.listPlatformRequestHistory(t1, s),
|
|
751
|
+
pending: whole(await host.listPlatformRequests(t1, s)),
|
|
752
|
+
history: whole(await host.listPlatformRequestHistory(t1, s)),
|
|
740
753
|
inScope: await (await host.getScope(alice, t1, s)).invoke('platform/intents'),
|
|
741
754
|
});
|
|
742
755
|
const byId = (rows, id) => rows.find((r) => r.id === id);
|
|
@@ -749,6 +762,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
749
762
|
for (const row of [...pending, ...history, ...inScope]) {
|
|
750
763
|
// ABSENT rather than null, so a healthy list is the shape it always was.
|
|
751
764
|
expect(row).not.toHaveProperty('decodeError');
|
|
765
|
+
expect(row).not.toHaveProperty('undecodable');
|
|
752
766
|
}
|
|
753
767
|
expect(byId(history, healthySettled.id)).toMatchObject({
|
|
754
768
|
payload: { doc: 2 },
|
|
@@ -802,14 +816,120 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
802
816
|
lastError: refusal,
|
|
803
817
|
failure: { origin: 'platform', code: 'validation_failed', permission: null },
|
|
804
818
|
});
|
|
805
|
-
expect((await host.listPlatformRequests(t1, s)).map((r) => r.id)).toEqual([healthyPending.id]);
|
|
806
|
-
const after = byId(await host.listPlatformRequestHistory(t1, s), brokenPendingId);
|
|
819
|
+
expect((whole(await host.listPlatformRequests(t1, s))).map((r) => r.id)).toEqual([healthyPending.id]);
|
|
820
|
+
const after = byId(whole(await host.listPlatformRequestHistory(t1, s)), brokenPendingId);
|
|
807
821
|
expect(after.status).toBe('failed');
|
|
808
822
|
expect(after.lastError).toBe(refusal);
|
|
809
823
|
expect(after.failure).toEqual({ origin: 'platform', code: 'validation_failed', permission: null });
|
|
810
824
|
// The stored payload is untouched by the settle, so the row still says it is not whole.
|
|
811
825
|
expect(after.decodeError).toMatch(/^payload: /);
|
|
812
826
|
});
|
|
827
|
+
/**
|
|
828
|
+
* #1637: the columns #1588 could not tolerate — id, kind, status, attempts, requested_at —
|
|
829
|
+
* have no value the contract accepts that would not be fabricated, so such a row still
|
|
830
|
+
* threw, and took every read of the journal with it. It now reads as the OTHER half of
|
|
831
|
+
* the entry: those five columns as stored, as text, and why. One column at a time, each
|
|
832
|
+
* beside a healthy twin, because a restored dump can only show a column at a time.
|
|
833
|
+
*/
|
|
834
|
+
describe('a row whose identity does not decode (#1637)', () => {
|
|
835
|
+
/** The healthy pending row's identity, as the variant names it: stored, as text. */
|
|
836
|
+
const asStored = {
|
|
837
|
+
undecodable: true,
|
|
838
|
+
id: brokenPendingId,
|
|
839
|
+
kind: 'connector:test',
|
|
840
|
+
status: 'pending',
|
|
841
|
+
attempts: '0',
|
|
842
|
+
requestedAt: at,
|
|
843
|
+
};
|
|
844
|
+
/** One broken column each: what is planted, and the field it reads back as — written out. */
|
|
845
|
+
const broken = [
|
|
846
|
+
{ column: 'id', over: { id: 'not-a-ulid' }, reads: { id: 'not-a-ulid' } },
|
|
847
|
+
{ column: 'id', over: { id: null }, reads: { id: null } },
|
|
848
|
+
// Oversized: read back capped, the way the decoder caps any value it quotes, so it cannot
|
|
849
|
+
// ride whole into the drain's report, the sweep log on every tick, or the dashboard card.
|
|
850
|
+
{ column: 'id', over: { id: 'x'.repeat(5000) }, reads: { id: `${'x'.repeat(200)}…` } },
|
|
851
|
+
{ column: 'kind', over: { kind: '' }, reads: { kind: '' } },
|
|
852
|
+
{ column: 'status', over: { status: 'queued' }, reads: { status: 'queued' } },
|
|
853
|
+
{ column: 'attempts', over: { attempts: -1 }, reads: { attempts: '-1' } },
|
|
854
|
+
{ column: 'attempts', over: { attempts: 'abc' }, reads: { attempts: 'abc' } },
|
|
855
|
+
// An epoch, the shape a dump from another world would carry — as text, since a restore
|
|
856
|
+
// binds a JSON number as REAL and TEXT affinity would store it as `1756684800.0`.
|
|
857
|
+
{ column: 'requested_at', over: { requested_at: '1756684800' }, reads: { requestedAt: '1756684800' } },
|
|
858
|
+
];
|
|
859
|
+
/** The raw reads — no `whole`, since meeting the variant is the point. */
|
|
860
|
+
const readEntries = async (s) => ({
|
|
861
|
+
pending: await host.listPlatformRequests(t1, s),
|
|
862
|
+
history: await host.listPlatformRequestHistory(t1, s),
|
|
863
|
+
inScope: await (await host.getScope(alice, t1, s)).invoke('platform/intents'),
|
|
864
|
+
});
|
|
865
|
+
for (const { column, over, reads } of broken) {
|
|
866
|
+
it(`${column} = ${JSON.stringify(over[column])}: named as stored, beside the row that is whole`, async () => {
|
|
867
|
+
const clean = await readAll(await restoredWith([healthyPending]));
|
|
868
|
+
const row = { ...healthyPending, id: brokenPendingId, ...over };
|
|
869
|
+
const { pending, history, inScope } = await readEntries(await restoredWith([row, healthyPending]));
|
|
870
|
+
const expected = {
|
|
871
|
+
...asStored,
|
|
872
|
+
...reads,
|
|
873
|
+
decodeError: expect.stringMatching(new RegExp(`^${column}: `)),
|
|
874
|
+
};
|
|
875
|
+
// A stored `queued` is not pending, so the drain's queue never meets that one.
|
|
876
|
+
const queued = column === 'status' ? [] : [expected];
|
|
877
|
+
for (const [read, entries, twin, also] of [
|
|
878
|
+
['pending', pending, clean.pending, queued],
|
|
879
|
+
['history', history, clean.history, [expected]],
|
|
880
|
+
['in-scope', inScope, clean.inScope, [expected]],
|
|
881
|
+
]) {
|
|
882
|
+
expect(entries, read).toHaveLength(1 + also.length);
|
|
883
|
+
// The healthy row reads EXACTLY as it does with no bad neighbour.
|
|
884
|
+
const healthy = entries.filter((e) => !isUndecodablePlatformRequest(e));
|
|
885
|
+
expect(healthy, read).toEqual(twin);
|
|
886
|
+
expect(entries.filter(isUndecodablePlatformRequest), read).toEqual(also);
|
|
887
|
+
// Every entry satisfies the published union — the variant claims no branded type.
|
|
888
|
+
for (const e of entries)
|
|
889
|
+
expect(() => platformRequestEntry.parse(e), read).not.toThrow();
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
it('several broken columns at once are all named, and the JSON beside them too', async () => {
|
|
894
|
+
const row = { ...healthyPending, id: 'not-a-ulid', status: 'queued', payload: 'nope' };
|
|
895
|
+
const { history } = await readEntries(await restoredWith([row, healthyPending]));
|
|
896
|
+
const variant = history.find(isUndecodablePlatformRequest);
|
|
897
|
+
expect(variant.decodeError.split('; ').map((part) => part.split(':')[0])).toEqual(['id', 'status', 'payload']);
|
|
898
|
+
// Named, never quoted: the payload's stored text does not travel in the reason.
|
|
899
|
+
expect(variant.decodeError).not.toContain('nope');
|
|
900
|
+
});
|
|
901
|
+
it('a clean journal carries no variant marker at all (the positive twin)', async () => {
|
|
902
|
+
const { pending, history, inScope } = await readEntries(await restoredWith([healthyPending, healthySettled]));
|
|
903
|
+
for (const e of [...pending, ...history, ...inScope]) {
|
|
904
|
+
expect(e).not.toHaveProperty('undecodable');
|
|
905
|
+
expect(isUndecodablePlatformRequest(e)).toBe(false);
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
it('a variant whose stored id is still an id can be refused: settled, out of the queue, still named', async () => {
|
|
909
|
+
// The drain's case (a) — tested there against a fake; what BOTH adapters owe it is that
|
|
910
|
+
// the settle lands on a row the read could not decode.
|
|
911
|
+
const s = await restoredWith([{ ...healthyPending, id: brokenPendingId, kind: '' }, healthyPending]);
|
|
912
|
+
const refusal = 'not executed: the intent row could not be decoded (kind: …)';
|
|
913
|
+
await host.settlePlatformRequest(t1, s, brokenPendingId, {
|
|
914
|
+
status: 'failed',
|
|
915
|
+
lastError: refusal,
|
|
916
|
+
failure: { origin: 'platform', code: 'validation_failed', permission: null },
|
|
917
|
+
});
|
|
918
|
+
const { pending, history } = await readEntries(s);
|
|
919
|
+
expect(pending.map((e) => e.id)).toEqual([healthyPending.id]);
|
|
920
|
+
// Settled — and the settle's `attempts + 1` left a number where text was — but the kind
|
|
921
|
+
// is still not a kind, so the row is still the variant, and says so.
|
|
922
|
+
expect(history.find(isUndecodablePlatformRequest)).toEqual({
|
|
923
|
+
undecodable: true,
|
|
924
|
+
id: brokenPendingId,
|
|
925
|
+
kind: '',
|
|
926
|
+
status: 'failed',
|
|
927
|
+
attempts: '1',
|
|
928
|
+
requestedAt: healthyPending.requested_at,
|
|
929
|
+
decodeError: expect.stringMatching(/^kind: /),
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
});
|
|
813
933
|
});
|
|
814
934
|
/**
|
|
815
935
|
* #1636 — #1588's shape everywhere else it lived. One outbox or denial row whose JSON
|
|
@@ -2467,7 +2587,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
2467
2587
|
});
|
|
2468
2588
|
return id;
|
|
2469
2589
|
};
|
|
2470
|
-
const journal = async (kind) => host.listPlatformRequestHistory(t1, s1, { kind });
|
|
2590
|
+
const journal = async (kind) => whole(await host.listPlatformRequestHistory(t1, s1, { kind }));
|
|
2471
2591
|
it('redacts the routed copy of the event, and leaves the neighbour intact', async () => {
|
|
2472
2592
|
// A distinct kind per test: this scope's journal is shared with every other
|
|
2473
2593
|
// suite that enqueues, and `connector:<provider>` IS a family of kinds.
|
|
@@ -2522,7 +2642,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
2522
2642
|
const kind = `connector:erasure-${ulid()}`;
|
|
2523
2643
|
const erased = dataSubjectId.parse(ulid());
|
|
2524
2644
|
const id = await routeIntent(kind, erased, 'Anna Ek');
|
|
2525
|
-
expect((await host.listPlatformRequests(t1, s1)).some((r) => r.id === id)).toBe(true);
|
|
2645
|
+
expect((whole(await host.listPlatformRequests(t1, s1))).some((r) => r.id === id)).toBe(true);
|
|
2526
2646
|
await host.admin.shredSubject(staff, t1, s1, erased);
|
|
2527
2647
|
// Redacting and leaving it pending would hand the drain a tombstone to execute;
|
|
2528
2648
|
// leaving it pending and intact would keep the name. `failed` is the truthful
|
|
@@ -2531,7 +2651,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
2531
2651
|
expect(settled.status).toBe('failed');
|
|
2532
2652
|
expect(settled.settledAt).not.toBeNull();
|
|
2533
2653
|
expect(settled.lastError).toContain('subject erasure');
|
|
2534
|
-
const stillPending = await host.listPlatformRequests(t1, s1);
|
|
2654
|
+
const stillPending = whole(await host.listPlatformRequests(t1, s1));
|
|
2535
2655
|
expect(stillPending.some((r) => r.id === id)).toBe(false);
|
|
2536
2656
|
});
|
|
2537
2657
|
it('redacts what the provider said back, not only what we sent', async () => {
|
|
@@ -3041,7 +3161,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
3041
3161
|
});
|
|
3042
3162
|
await host.settlePlatformRequest(t1, s1, template, { status: 'done' });
|
|
3043
3163
|
const queued = async () => {
|
|
3044
|
-
const rows = await host.listPlatformRequestHistory(t1, s, { kind: SWEEP_RUNS_KIND });
|
|
3164
|
+
const rows = whole(await host.listPlatformRequestHistory(t1, s, { kind: SWEEP_RUNS_KIND }));
|
|
3045
3165
|
return { mine: rows.find((r) => r.id === ids.mine), theirs: rows.find((r) => r.id === ids.theirs) };
|
|
3046
3166
|
};
|
|
3047
3167
|
const before = await queued();
|
|
@@ -6420,7 +6540,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
6420
6540
|
permissions: [{ key: 'selfw:use', description: 'self' }],
|
|
6421
6541
|
withdraws: ['selfw/op'],
|
|
6422
6542
|
}),
|
|
6423
|
-
|
|
6543
|
+
...testOperations({ 'selfw/op': (() => 'x') }),
|
|
6424
6544
|
})).toThrow(/withdraws its own operation/);
|
|
6425
6545
|
});
|
|
6426
6546
|
it('links declared entity relations, idempotently (K-16)', async () => {
|