@substrat-run/contract-tests 0.138.0 → 0.140.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 (113) hide show
  1. package/dist/attachment-text-fixtures.d.ts +17 -0
  2. package/dist/attachment-text-fixtures.d.ts.map +1 -1
  3. package/dist/attachment-text-fixtures.js +344 -4
  4. package/dist/attachment-text-fixtures.js.map +1 -1
  5. package/dist/attachment-text-suite.d.ts +16 -1
  6. package/dist/attachment-text-suite.d.ts.map +1 -1
  7. package/dist/attachment-text-suite.js +212 -28
  8. package/dist/attachment-text-suite.js.map +1 -1
  9. package/dist/capability-suite.d.ts.map +1 -1
  10. package/dist/capability-suite.js +5 -0
  11. package/dist/capability-suite.js.map +1 -1
  12. package/dist/caused-by-module.d.ts +23 -0
  13. package/dist/caused-by-module.d.ts.map +1 -0
  14. package/dist/caused-by-module.js +56 -0
  15. package/dist/caused-by-module.js.map +1 -0
  16. package/dist/caused-by-suite.d.ts +28 -0
  17. package/dist/caused-by-suite.d.ts.map +1 -0
  18. package/dist/caused-by-suite.js +223 -0
  19. package/dist/caused-by-suite.js.map +1 -0
  20. package/dist/connect-link-suite.d.ts +19 -0
  21. package/dist/connect-link-suite.d.ts.map +1 -0
  22. package/dist/connect-link-suite.js +215 -0
  23. package/dist/connect-link-suite.js.map +1 -0
  24. package/dist/entity-state-migration-suite.d.ts +16 -0
  25. package/dist/entity-state-migration-suite.d.ts.map +1 -0
  26. package/dist/entity-state-migration-suite.js +305 -0
  27. package/dist/entity-state-migration-suite.js.map +1 -0
  28. package/dist/entity-state-module.d.ts +317 -0
  29. package/dist/entity-state-module.d.ts.map +1 -0
  30. package/dist/entity-state-module.js +278 -0
  31. package/dist/entity-state-module.js.map +1 -0
  32. package/dist/entity-state-suite.d.ts +9 -0
  33. package/dist/entity-state-suite.d.ts.map +1 -0
  34. package/dist/entity-state-suite.js +557 -0
  35. package/dist/entity-state-suite.js.map +1 -0
  36. package/dist/entity-trash-module.d.ts +439 -0
  37. package/dist/entity-trash-module.d.ts.map +1 -0
  38. package/dist/entity-trash-module.js +287 -0
  39. package/dist/entity-trash-module.js.map +1 -0
  40. package/dist/entity-trash-suite.d.ts +59 -0
  41. package/dist/entity-trash-suite.d.ts.map +1 -0
  42. package/dist/entity-trash-suite.js +568 -0
  43. package/dist/entity-trash-suite.js.map +1 -0
  44. package/dist/erasure-module.d.ts +259 -0
  45. package/dist/erasure-module.d.ts.map +1 -0
  46. package/dist/erasure-module.js +251 -0
  47. package/dist/erasure-module.js.map +1 -0
  48. package/dist/findings-atomic-suite.d.ts +21 -0
  49. package/dist/findings-atomic-suite.d.ts.map +1 -0
  50. package/dist/findings-atomic-suite.js +131 -0
  51. package/dist/findings-atomic-suite.js.map +1 -0
  52. package/dist/findings-suite.d.ts +11 -0
  53. package/dist/findings-suite.d.ts.map +1 -0
  54. package/dist/findings-suite.js +391 -0
  55. package/dist/findings-suite.js.map +1 -0
  56. package/dist/index.d.ts +22 -3
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +20 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/list-suite.d.ts.map +1 -1
  61. package/dist/list-suite.js +101 -1
  62. package/dist/list-suite.js.map +1 -1
  63. package/dist/membership-executor-suite.d.ts +6 -2
  64. package/dist/membership-executor-suite.d.ts.map +1 -1
  65. package/dist/membership-executor-suite.js +318 -21
  66. package/dist/membership-executor-suite.js.map +1 -1
  67. package/dist/migration-comments-suite.d.ts +3 -0
  68. package/dist/migration-comments-suite.d.ts.map +1 -0
  69. package/dist/migration-comments-suite.js +92 -0
  70. package/dist/migration-comments-suite.js.map +1 -0
  71. package/dist/migration-comments.d.ts +3 -0
  72. package/dist/migration-comments.d.ts.map +1 -0
  73. package/dist/migration-comments.js +50 -0
  74. package/dist/migration-comments.js.map +1 -0
  75. package/dist/migration-digest-suite.d.ts +3 -0
  76. package/dist/migration-digest-suite.d.ts.map +1 -0
  77. package/dist/migration-digest-suite.js +175 -0
  78. package/dist/migration-digest-suite.js.map +1 -0
  79. package/dist/modules.d.ts +759 -0
  80. package/dist/modules.d.ts.map +1 -1
  81. package/dist/modules.js +158 -6
  82. package/dist/modules.js.map +1 -1
  83. package/dist/peer-suite.d.ts +2 -1
  84. package/dist/peer-suite.d.ts.map +1 -1
  85. package/dist/peer-suite.js +103 -1
  86. package/dist/peer-suite.js.map +1 -1
  87. package/dist/permission-suite.d.ts.map +1 -1
  88. package/dist/permission-suite.js +590 -2
  89. package/dist/permission-suite.js.map +1 -1
  90. package/dist/scope-host-suite.d.ts.map +1 -1
  91. package/dist/scope-host-suite.js +102 -1
  92. package/dist/scope-host-suite.js.map +1 -1
  93. package/dist/split-cases.d.ts +12 -0
  94. package/dist/split-cases.d.ts.map +1 -0
  95. package/dist/split-cases.js +59 -0
  96. package/dist/split-cases.js.map +1 -0
  97. package/dist/subject-erasure-suite.d.ts +6 -0
  98. package/dist/subject-erasure-suite.d.ts.map +1 -0
  99. package/dist/subject-erasure-suite.js +287 -0
  100. package/dist/subject-erasure-suite.js.map +1 -0
  101. package/dist/switch-audit-fault.d.ts +48 -0
  102. package/dist/switch-audit-fault.d.ts.map +1 -0
  103. package/dist/switch-audit-fault.js +80 -0
  104. package/dist/switch-audit-fault.js.map +1 -0
  105. package/dist/system-switch-suite.d.ts +2 -1
  106. package/dist/system-switch-suite.d.ts.map +1 -1
  107. package/dist/system-switch-suite.js +90 -0
  108. package/dist/system-switch-suite.js.map +1 -1
  109. package/dist/vertical-events-suite.d.ts +55 -0
  110. package/dist/vertical-events-suite.d.ts.map +1 -1
  111. package/dist/vertical-events-suite.js +55 -0
  112. package/dist/vertical-events-suite.js.map +1 -1
  113. package/package.json +3 -3
@@ -1,6 +1,6 @@
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, 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, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, } from '@substrat-run/contracts';
4
4
  import { INERT_SCOPE_REASON, 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
6
  /**
@@ -273,6 +273,45 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
273
273
  expect(settled.status).toBe('done');
274
274
  expect(JSON.parse(settled.result)).toEqual({ scopeId: 'NEWSITE' });
275
275
  });
276
+ it('a settle carrying an outcome event writes it once, as the kernel, with the settle (#2102)', async () => {
277
+ const stub = await host.getScope(alice, t1, s1);
278
+ const mail = { to: 'ada@example.com', subject: 'Hej', html: '<p>Hej</p>', text: 'Hej' };
279
+ const id = await stub.invoke('platform/request', { kind: SEND_EMAIL_KIND, payload: mail });
280
+ const mine = () => stub
281
+ .invoke('platform/outcome-events')
282
+ .then((rows) => rows.filter((r) => JSON.parse(r.payload).request === id));
283
+ const event = (request) => ({
284
+ type: EMAIL_SENT,
285
+ entity: { entityType: PLATFORM_REQUEST_ENTITY_TYPE, entityId: id },
286
+ payload: {
287
+ request: platformRequestId.parse(request),
288
+ about: null,
289
+ sender: 'platform',
290
+ messageId: 'msg-1',
291
+ attempts: 1,
292
+ code: null,
293
+ status: null,
294
+ },
295
+ });
296
+ // Refused before anything is written: an event on a `pending` settle, or one naming
297
+ // another request. The row stays exactly as it was.
298
+ await expect(host.settlePlatformRequest(t1, s1, platformRequestId.parse(id), { status: 'pending', event: event(id) })).rejects.toThrow();
299
+ 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);
301
+ expect(await mine()).toEqual([]);
302
+ await host.settlePlatformRequest(t1, s1, platformRequestId.parse(id), { status: 'done', event: event(id) });
303
+ const written = await mine();
304
+ expect(written).toHaveLength(1);
305
+ expect(written[0].type).toBe(EMAIL_SENT);
306
+ expect(JSON.parse(written[0].actor)).toEqual({ system: '@substrat-run/kernel' });
307
+ expect([written[0].entity_type, written[0].entity_id]).toEqual([PLATFORM_REQUEST_ENTITY_TYPE, id]);
308
+ expect(JSON.parse(written[0].payload)).toMatchObject({ request: id, messageId: 'msg-1', sender: 'platform' });
309
+ // A repeated settle finds the row terminal: it moves nothing, so it writes nothing.
310
+ await host.settlePlatformRequest(t1, s1, platformRequestId.parse(id), { status: 'failed', event: event(id) });
311
+ expect(await mine()).toHaveLength(1);
312
+ const row = (await stub.invoke('platform/read-requests')).find((r) => r.id === id);
313
+ expect(row.status).toBe('done');
314
+ });
276
315
  it('a transient failure keeps the intent pending and preserves a two-phase result (retry)', async () => {
277
316
  const stub = await host.getScope(alice, t1, s1);
278
317
  const id = await stub.invoke('platform/request', { kind: 'provision-sibling', payload: {} });
@@ -4400,6 +4439,20 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
4400
4439
  expect(rotated?.after).toMatchObject({ id: row.id, rotatedBy: officeAdmin });
4401
4440
  expect(JSON.stringify(log)).not.toContain('tok-relay-rotated');
4402
4441
  });
4442
+ it('a rotation sets the expiry, keeps it when omitted, and clears it with null (#2100)', async () => {
4443
+ const [row] = await host.admin.listConnections(staff, { tenantId: t1, vertical: 'callout' });
4444
+ const expiry = async () => (await host.admin.listConnections(staff, { tenantId: t1, vertical: 'callout' })).find((c) => c.id === row.id)
4445
+ .expiresAt;
4446
+ const at = '2027-10-07T12:00:00.000Z';
4447
+ await host.admin.updateConnectionSecret(staff, row.id, { accessToken: 'tok-dated' }, at);
4448
+ expect(await expiry()).toBe(at);
4449
+ await host.admin.updateConnectionSecret(staff, row.id, { accessToken: 'tok-kept' });
4450
+ expect(await expiry()).toBe(at);
4451
+ // The credential that had an end was replaced by one that has none (a certificate → a
4452
+ // client secret): the old end must not stay on the connection, reading as this one's.
4453
+ await host.admin.updateConnectionSecret(staff, row.id, { accessToken: 'tok-undated' }, null);
4454
+ expect(await expiry()).toBeNull();
4455
+ });
4403
4456
  it('records health, and a success clears a prior error', async () => {
4404
4457
  const [row] = await host.admin.listConnections(staff, {
4405
4458
  tenantId: t1,
@@ -6335,6 +6388,43 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
6335
6388
  ]);
6336
6389
  expect(await relinked(item('rl6'))).toHaveLength(2);
6337
6390
  });
6391
+ /**
6392
+ * The documented way to DETACH one parent of a multi-parent child (#2044): relink it
6393
+ * onto a parent it already has. ticket0 takes a conversation's public thread off a
6394
+ * widget session this way, so it is contract, not an accident of `writeEdge`. The
6395
+ * expiries are planted (only a restore can hold one) to pin what the verb does to
6396
+ * them: `to` is left permanent, as every edge a verb leaves live is, and a bystander
6397
+ * parent keeps its own.
6398
+ */
6399
+ it('relink onto a live parent detaches `from` only: `to` left live and permanent, others untouched', async () => {
6400
+ const FUTURE = '2999-01-01T00:00:00.000Z';
6401
+ await plant([
6402
+ { subject: 'item:rl-dt', object: 'box:rb1' }, // from
6403
+ { subject: 'item:rl-dt', object: 'box:rb2', expires_at: FUTURE }, // to, already live
6404
+ { subject: 'item:rl-dt', object: 'box:rb3', expires_at: FUTURE }, // a bystander
6405
+ ]);
6406
+ await move(item('rl-dt'), box('rb1'), box('rb2'));
6407
+ const table = (await host.admin.exportScope(staff, t1, s1)).tables.find((t) => t.name === '_substrat_tuples');
6408
+ const col = (name) => table.columns.indexOf(name);
6409
+ const rows = table.rows
6410
+ .filter((r) => r[col('subject')] === 'item:rl-dt')
6411
+ .map((r) => ({
6412
+ object: r[col('object')],
6413
+ revoked: r[col('revoked_at')] !== null,
6414
+ expires: r[col('expires_at')],
6415
+ }))
6416
+ .sort((x, y) => String(x.object).localeCompare(String(y.object)));
6417
+ expect(rows).toEqual([
6418
+ { object: 'box:rb1', revoked: true, expires: null },
6419
+ { object: 'box:rb2', revoked: false, expires: null },
6420
+ { object: 'box:rb3', revoked: false, expires: FUTURE },
6421
+ ]);
6422
+ // One move, recorded as one; the live `to` was neither revived nor re-announced.
6423
+ const events = await relinked(item('rl-dt'));
6424
+ expect(events).toHaveLength(1);
6425
+ expect(events[0]).toMatchObject({ payload: { child: item('rl-dt'), from: box('rb1'), to: box('rb2') } });
6426
+ expect(await relinked(item('rl-dt'), 'entity.linked')).toEqual([]);
6427
+ });
6338
6428
  it('link revives an edge relink moved away from — INSERT OR IGNORE would have kept it dead', async () => {
6339
6429
  await link(item('rl7'), box('rb1'));
6340
6430
  // The twin first: a first-time link records nothing, as it never has.
@@ -7070,6 +7160,15 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
7070
7160
  expect(await host.admin.listEntitlements(staff, t)).toHaveLength(1);
7071
7161
  expect((await host.admin.listOrgs(staff, t)).filter((o) => o.id === org)).toHaveLength(1);
7072
7162
  expect((await host.admin.resolveIdentity(t, `oidc:reap-${t.toLowerCase()}`, 'x1'))?.principal).toBe(person);
7163
+ // #1748: a finding and a suppress rule are the tenant's too.
7164
+ await host.admin.recordOpsFailure({ actor: staff, operation: 'reap.finding', tenantId: t, message: 'x' });
7165
+ await host.admin.createFindingRule(staff, t, {
7166
+ operation: 'reap.other',
7167
+ expiresAt: instant.parse(new Date(Date.now() + 86_400_000).toISOString()),
7168
+ reason: 'reap fixture',
7169
+ });
7170
+ expect(await host.admin.listFindings(staff, { tenantId: t })).toHaveLength(1);
7171
+ expect(await host.admin.listFindingRules(staff, t)).toHaveLength(1);
7073
7172
  // Reap only follows the reversible delete state.
7074
7173
  await expectRefusal(host.admin.reapTenant(staff, t), 'conflict', /not deleting/);
7075
7174
  // The caller reaps the scope first (the sweep/route orchestrates this above the
@@ -7088,6 +7187,8 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
7088
7187
  expect(await host.admin.listEntitlements(staff, t)).toHaveLength(0);
7089
7188
  expect((await host.admin.listOrgs(staff, t)).filter((o) => o.id === org)).toHaveLength(0);
7090
7189
  expect(await host.admin.resolveIdentity(t, `oidc:reap-${t.toLowerCase()}`, 'x1')).toBeUndefined();
7190
+ expect(await host.admin.listFindings(staff, { tenantId: t })).toHaveLength(0);
7191
+ expect(await host.admin.listFindingRules(staff, t)).toHaveLength(0);
7091
7192
  // The admin log is KEPT WHOLE — the witness outlives the tenant — and records the reap.
7092
7193
  const log = await host.admin.auditLog(staff, { tenantId: t });
7093
7194
  expect(log.some((r) => r.action === 'createTenant')).toBe(true);