@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.
Files changed (73) hide show
  1. package/dist/attachment-refusal-suite.d.ts +3 -0
  2. package/dist/attachment-refusal-suite.d.ts.map +1 -0
  3. package/dist/attachment-refusal-suite.js +128 -0
  4. package/dist/attachment-refusal-suite.js.map +1 -0
  5. package/dist/bind-module.d.ts +199 -0
  6. package/dist/bind-module.d.ts.map +1 -0
  7. package/dist/bind-module.js +104 -0
  8. package/dist/bind-module.js.map +1 -0
  9. package/dist/binding-invite-suite.d.ts +12 -0
  10. package/dist/binding-invite-suite.d.ts.map +1 -0
  11. package/dist/binding-invite-suite.js +507 -0
  12. package/dist/binding-invite-suite.js.map +1 -0
  13. package/dist/caused-by-module.d.ts.map +1 -1
  14. package/dist/caused-by-module.js +3 -2
  15. package/dist/caused-by-module.js.map +1 -1
  16. package/dist/derived-module.d.ts +690 -0
  17. package/dist/derived-module.d.ts.map +1 -0
  18. package/dist/derived-module.js +320 -0
  19. package/dist/derived-module.js.map +1 -0
  20. package/dist/derived-suite.d.ts +3 -0
  21. package/dist/derived-suite.d.ts.map +1 -0
  22. package/dist/derived-suite.js +481 -0
  23. package/dist/derived-suite.js.map +1 -0
  24. package/dist/entity-state-module.d.ts.map +1 -1
  25. package/dist/entity-state-module.js +12 -6
  26. package/dist/entity-state-module.js.map +1 -1
  27. package/dist/entity-trash-module.d.ts.map +1 -1
  28. package/dist/entity-trash-module.js +4 -4
  29. package/dist/entity-trash-module.js.map +1 -1
  30. package/dist/entity-trash-suite.d.ts.map +1 -1
  31. package/dist/entity-trash-suite.js +17 -8
  32. package/dist/entity-trash-suite.js.map +1 -1
  33. package/dist/erasure-module.d.ts.map +1 -1
  34. package/dist/erasure-module.js +3 -2
  35. package/dist/erasure-module.js.map +1 -1
  36. package/dist/index.d.ts +8 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +8 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/input-parse-suite.d.ts.map +1 -1
  41. package/dist/input-parse-suite.js +33 -2
  42. package/dist/input-parse-suite.js.map +1 -1
  43. package/dist/membership-module.d.ts.map +1 -1
  44. package/dist/membership-module.js +3 -2
  45. package/dist/membership-module.js.map +1 -1
  46. package/dist/migration-comments.d.ts.map +1 -1
  47. package/dist/migration-comments.js +3 -2
  48. package/dist/migration-comments.js.map +1 -1
  49. package/dist/modules.d.ts.map +1 -1
  50. package/dist/modules.js +78 -68
  51. package/dist/modules.js.map +1 -1
  52. package/dist/permission-suite.d.ts.map +1 -1
  53. package/dist/permission-suite.js +28 -0
  54. package/dist/permission-suite.js.map +1 -1
  55. package/dist/scope-host-suite.d.ts.map +1 -1
  56. package/dist/scope-host-suite.js +151 -31
  57. package/dist/scope-host-suite.js.map +1 -1
  58. package/dist/shape-backfill-ties.d.ts +29 -0
  59. package/dist/shape-backfill-ties.d.ts.map +1 -0
  60. package/dist/shape-backfill-ties.js +113 -0
  61. package/dist/shape-backfill-ties.js.map +1 -0
  62. package/dist/shape-reconcile-plan.d.ts +37 -0
  63. package/dist/shape-reconcile-plan.d.ts.map +1 -0
  64. package/dist/shape-reconcile-plan.js +143 -0
  65. package/dist/shape-reconcile-plan.js.map +1 -0
  66. package/dist/test-operations.d.ts +25 -0
  67. package/dist/test-operations.d.ts.map +1 -0
  68. package/dist/test-operations.js +22 -0
  69. package/dist/test-operations.js.map +1 -0
  70. package/dist/vertical-events-suite.d.ts.map +1 -1
  71. package/dist/vertical-events-suite.js +72 -7
  72. package/dist/vertical-events-suite.js.map +1 -1
  73. 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,EAkBf,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAeL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAY9B,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;AA2BD;;;;;;;;;;;;;;;;;;;;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,CAs+SN"}
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"}
@@ -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
- operations: { 'selfw/op': (() => 'x') },
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 () => {