@substrat-run/contract-tests 0.114.0 → 0.116.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.
@@ -1,8 +1,35 @@
1
1
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
2
  import { connectorCalls, connectorTestFetch, resetConnectorCalls } from './connector-fixture.js';
3
- import { connectionId, dataSubjectId, eventId, instant, moduleManifest, orgId, AUTO_ADMISSION_NOTE, permissionKey, platformActorId, principalId, scopeId, tenantId, SCOPE_QUERY_ROW_MAX, } from '@substrat-run/contracts';
3
+ import { connectionId, dataSubjectId, errorCodeOf, eventId, instant, moduleManifest, orgId, AUTO_ADMISSION_NOTE, permissionKey, platformActorId, principalId, scopeId, tenantId, SCOPE_QUERY_ROW_MAX, } from '@substrat-run/contracts';
4
4
  import { isSearchIndexTable, runPlatformSweep, ulid, } from '@substrat-run/kernel';
5
5
  import { billedMod, contractTestBareOps, contractTestInitialModules, gateModManifest, lateMod, testModManifest, victimModManifest, } from './modules.js';
6
+ /**
7
+ * Assert a refusal by the CODE it declared, not by the sentence it happens to carry
8
+ * (#113 phase 5).
9
+ *
10
+ * `errorCodeOf` is the same reading the control plane's `mapError` does, so what this
11
+ * pins and the problem document a transport renders agree by construction. A `/message/`
12
+ * match agreed with it only by coincidence — and that coincidence was load-bearing:
13
+ * `CODE_PATTERNS` in `packages/control-plane-api/src/errors.ts` guessed the code by
14
+ * regex, so rewording a throw silently changed a 404 into a 500, and the suite's own
15
+ * message assertion was the only thing standing in the way. Moving the assertion here
16
+ * makes the message free to change and the TYPE the thing that cannot.
17
+ *
18
+ * A promise that RESOLVES fails this too: `errorCodeOf(undefined)` is `undefined`, which
19
+ * is never a code — so it cannot pass by not throwing at all.
20
+ *
21
+ * `message` is for the minority of refusals where the sentence carries something the code
22
+ * cannot: WHICH branch fired, when two branches share one code. `deleteVertical` refuses a
23
+ * live scope with "delete or rebind" and an archived one with "reap or restore" — both
24
+ * `conflict`, and an adapter that answered the wrong one would send an operator to a button
25
+ * that is not there. Pass it only for that; a code is the contract everywhere else.
26
+ */
27
+ const expectRefusal = async (p, code, message) => {
28
+ const err = await p.then(() => undefined, (e) => e);
29
+ expect(errorCodeOf(err)).toBe(code);
30
+ if (message !== undefined)
31
+ expect(err?.message).toMatch(message);
32
+ };
6
33
  /**
7
34
  * The scope-host contract suite (design doc §11). Every adapter — pure SQLite,
8
35
  * Cloudflare, and any future one — must pass this unchanged (D-14). If an
@@ -451,6 +478,44 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
451
478
  // rebuild instant went to the new table, and reopening it would write it there twice.
452
479
  await expect(host.admin.redrainEvents(staff, t1, sDrain, { drainedBefore: stampedAt })).resolves.toBe(0);
453
480
  const justAfter = new Date(Date.parse(stampedAt) + 1).toISOString();
481
+ // COUNT ONLY (#1545) — the same window asked read-only, which is what a dry run needs
482
+ // and what nothing could answer before: the verb only ever reopened, so a caller could
483
+ // learn the number by changing the rows or not at all.
484
+ const redrainRows = async () => (await host.admin.auditLog(staff, { tenantId: t1 })).filter((r) => r.action === 'redrainEvents' && r.scopeId === sDrain).length;
485
+ const receiptsBefore = await redrainRows();
486
+ const undrainedBefore = await host.admin.readUndrainedEvents(staff, t1, sDrain, 200);
487
+ // Strictly before, on the same boundary the reopen holds: a count that included the
488
+ // instant would rehearse a different window from the one the real run touches.
489
+ await expect(host.admin.redrainEvents(staff, t1, sDrain, { drainedBefore: stampedAt, countOnly: true })).resolves.toBe(0);
490
+ // Asked TWICE on purpose: a "count" that reopened its own window would answer
491
+ // differently the second time, which is the one failure a rehearsal must not have.
492
+ await expect(host.admin.redrainEvents(staff, t1, sDrain, { drainedBefore: justAfter, countOnly: true })).resolves.toBe(first.length);
493
+ await expect(host.admin.redrainEvents(staff, t1, sDrain, { drainedBefore: justAfter, countOnly: true })).resolves.toBe(first.length);
494
+ // …and it moved nothing: the stamped rows are still stamped, so the drain has exactly
495
+ // the same work in front of it as before the count.
496
+ expect((await host.admin.readUndrainedEvents(staff, t1, sDrain, 200)).map((e) => e.id)).toEqual(undrainedBefore.map((e) => e.id));
497
+ // …and wrote no receipt. The intent row exists because a reopen that crashed before
498
+ // its outcome row would leave a second egress with no trace; a count egresses nothing,
499
+ // and a row claiming a redrain on a scope that was only counted is a false statement
500
+ // in the one log that is evidence.
501
+ expect(await redrainRows()).toBe(receiptsBefore);
502
+ // It is still a READ, and K-24 takes all reads rather than a chosen subset — so the
503
+ // access log carries a row per count, naming the window and the number it found.
504
+ // "Nobody can tell who counted every tenant's outbox" is the hole that would be.
505
+ const counts = (await host.admin.accessLog(staff, { tenantId: t1, method: 'redrainEvents' })).filter((r) => r.scopeId === sDrain);
506
+ expect(counts).toHaveLength(3);
507
+ expect(counts.map((r) => r.resultCount)).toEqual(expect.arrayContaining([0, first.length]));
508
+ // The future refusal is the verb's, not the reopen's alone — the window rule is
509
+ // checked before the two branches part.
510
+ await expect(host.admin.redrainEvents(staff, t1, sDrain, {
511
+ drainedBefore: new Date(Date.now() + 60_000).toISOString(),
512
+ countOnly: true,
513
+ })).rejects.toThrow(/future/);
514
+ // K-3 holds for the count as for the reopen: a foreign pair is refused, never answered
515
+ // 0 — "nothing to reopen" would read as a finished rehearsal of a scope never touched.
516
+ await expect(host.admin.redrainEvents(staff, t2, sDrain, { drainedBefore: justAfter, countOnly: true })).rejects.toThrow(/unknown scope/);
517
+ // The number the count gave IS the number the reopen changes — the property that makes
518
+ // a dry run a rehearsal rather than a second, differently-shaped question.
454
519
  await expect(host.admin.redrainEvents(staff, t1, sDrain, { drainedBefore: justAfter })).resolves.toBe(first.length);
455
520
  // The reopened rows come back through the ordinary read, unchanged — nothing about the
456
521
  // event moves except that it may leave again.
@@ -2950,8 +3015,8 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
2950
3015
  expect((await at('listtest'))?.publishRequestedAt).toBeTruthy();
2951
3016
  await host.admin.setVerticalListed(staff, 'listtest', true);
2952
3017
  expect((await at('listtest'))?.publishRequestedAt).toBeUndefined(); // cleared on review
2953
- await expect(host.admin.setVerticalListed(staff, 'no-such-vertical', true)).rejects.toThrow(/unknown vertical/);
2954
- await expect(host.admin.requestPublish(staff, 'no-such-vertical')).rejects.toThrow(/unknown vertical/);
3018
+ await expectRefusal(host.admin.setVerticalListed(staff, 'no-such-vertical', true), 'not_found');
3019
+ await expectRefusal(host.admin.requestPublish(staff, 'no-such-vertical'), 'not_found');
2955
3020
  });
2956
3021
  it('blocks new installs (setVerticalInstallsBlocked) — a provisioning gate, not a delete', async () => {
2957
3022
  const at = (slug) => host.admin.listVerticals(staff).then((vs) => vs.find((v) => v.slug === slug));
@@ -2964,7 +3029,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
2964
3029
  expect((await at('blocktest'))?.listed).toBe(false);
2965
3030
  await host.admin.setVerticalInstallsBlocked(staff, 'blocktest', false);
2966
3031
  expect((await at('blocktest'))?.installsBlocked).toBe(false);
2967
- await expect(host.admin.setVerticalInstallsBlocked(staff, 'no-such-vertical', true)).rejects.toThrow(/unknown vertical/);
3032
+ await expectRefusal(host.admin.setVerticalInstallsBlocked(staff, 'no-such-vertical', true), 'not_found');
2968
3033
  });
2969
3034
  it('grants the tenant-provisioner capability (setVerticalTenantProvisioner) — a staff grant a re-push cannot touch', async () => {
2970
3035
  const at = (slug) => host.admin.listVerticals(staff).then((vs) => vs.find((v) => v.slug === slug));
@@ -2979,7 +3044,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
2979
3044
  expect((await at('managertest'))?.tenantProvisioner).toBe(true);
2980
3045
  await host.admin.setVerticalTenantProvisioner(staff, 'managertest', false);
2981
3046
  expect((await at('managertest'))?.tenantProvisioner).toBe(false);
2982
- await expect(host.admin.setVerticalTenantProvisioner(staff, 'no-such-vertical', true)).rejects.toThrow(/unknown vertical/);
3047
+ await expectRefusal(host.admin.setVerticalTenantProvisioner(staff, 'no-such-vertical', true), 'not_found');
2983
3048
  });
2984
3049
  it('carries the declared provisioner intent (#455) — a refreshable request, orthogonal to the grant', async () => {
2985
3050
  const at = (slug) => host.admin.listVerticals(staff).then((vs) => vs.find((v) => v.slug === slug));
@@ -3030,12 +3095,13 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3030
3095
  expect((await at('mailer'))?.emailSender).toBe(true);
3031
3096
  await host.admin.setVerticalEmailSender(staff, 'mailer', false);
3032
3097
  expect((await at('mailer'))?.emailSender).toBe(false);
3033
- await expect(host.admin.setVerticalEmailSender(staff, 'no-such-vertical', true)).rejects.toThrow(/unknown vertical/);
3098
+ await expectRefusal(host.admin.setVerticalEmailSender(staff, 'no-such-vertical', true), 'not_found');
3034
3099
  });
3035
3100
  it('deletes a vertical — refused while a scope is bound, total once nothing is', async () => {
3036
3101
  // 'callout' still backs s1 (bound above): the refusal that stops a delete from
3037
- // stranding a live scope's version pin and routing.
3038
- await expect(host.admin.deleteVertical(staff, 'callout')).rejects.toThrow(/still backs/);
3102
+ // stranding a live scope's version pin and routing. Pinned by the CODE (#113
3103
+ // phase 5) — the sentence is free to change, the 409 is not.
3104
+ await expectRefusal(host.admin.deleteVertical(staff, 'callout'), 'conflict');
3039
3105
  expect((await host.admin.listVerticals(staff)).some((v) => v.slug === 'callout')).toBe(true);
3040
3106
  // A vertical nothing is bound to deletes totally: row, versions, channels.
3041
3107
  const versionId = ulid();
@@ -3059,7 +3125,55 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3059
3125
  await host.admin.registerVertical(staff, { slug: 'deletable', name: 'Deletable', source: 'cli', ownerTenant: t2 });
3060
3126
  expect(await host.admin.listVersions(staff, 'deletable')).toEqual([]); // no resurrected versions
3061
3127
  await host.admin.deleteVertical(staff, 'deletable');
3062
- await expect(host.admin.deleteVertical(staff, 'no-such-vertical')).rejects.toThrow(/unknown vertical/);
3128
+ await expectRefusal(host.admin.deleteVertical(staff, 'no-such-vertical'), 'not_found');
3129
+ });
3130
+ it('refuses the serving-state read and write for a vertical that does not exist', async () => {
3131
+ await expectRefusal(host.admin.verticalServing(staff, 'no-such-vertical'), 'not_found');
3132
+ await expectRefusal(host.admin.setVerticalServing(staff, 'no-such-vertical', {
3133
+ ref: 'serving-script', versionId: ulid(), doClasses: [], migrationTag: 'g1',
3134
+ }), 'not_found');
3135
+ });
3136
+ it('refuses every registry verb that addresses a version which does not exist (#113 phase 5)', async () => {
3137
+ // The five `unknown version …` throw sites, one per verb, on both adapters. Nothing
3138
+ // asserted them before this case: each was a bare `Error` whose 404 came from a
3139
+ // `/unknown version /` regex in the control plane, so a reword — or a regression to
3140
+ // `new Error` — changed the status with no test to notice. The code is the contract
3141
+ // now, and it is pinned here rather than in the transport.
3142
+ //
3143
+ // The id is well-formed and simply names nothing, which is what separates this from
3144
+ // a parse failure: `not_found`, never `validation_failed`.
3145
+ const ghost = ulid();
3146
+ await expectRefusal(host.admin.admitVersion(staff, ghost), 'not_found');
3147
+ await expectRefusal(host.admin.rejectVersion(staff, ghost, 'no such version'), 'not_found');
3148
+ await expectRefusal(host.admin.promoteVersion(staff, 'callout', 'prod', ghost), 'not_found');
3149
+ await expectRefusal(host.admin.bindScopeVersion(staff, t1, s1, ghost), 'not_found');
3150
+ await expectRefusal(host.admin.versionManifest(staff, 'callout', ghost), 'not_found');
3151
+ // versionManifest refuses on the PAIR — `!v || v.vertical_slug !== verticalSlug` —
3152
+ // so the absent id above only exercises HALF its guard. A version that really
3153
+ // exists, under a DIFFERENT vertical, must read as absent too: asking for it under
3154
+ // someone else's slug is not a way in. Without this second call an implementation
3155
+ // that dropped the ownership check would still pass the case, which is the same
3156
+ // shape of hole this whole change exists to close.
3157
+ await host.admin.registerVertical(staff, {
3158
+ slug: 'otherowner',
3159
+ name: 'OtherOwner',
3160
+ source: 'cli',
3161
+ ownerTenant: t1,
3162
+ });
3163
+ const elsewhere = ulid();
3164
+ await host.admin.publishVersion(staff, {
3165
+ id: elsewhere,
3166
+ verticalSlug: 'otherowner',
3167
+ version: '1.0.0',
3168
+ manifestDigest: 'm-other',
3169
+ permissionDigest: 'p-other',
3170
+ migrationDigest: 'g-other',
3171
+ deploymentRef: null,
3172
+ });
3173
+ // It resolves under its OWN vertical — so the refusal below is the pair check
3174
+ // firing, not the version failing to exist.
3175
+ await host.admin.versionManifest(staff, 'otherowner', elsewhere);
3176
+ await expectRefusal(host.admin.versionManifest(staff, 'callout', elsewhere), 'not_found');
3063
3177
  });
3064
3178
  it('an archived scope blocks the delete naming the reap step; a reaped tombstone never blocks', async () => {
3065
3179
  // A deleted app leaves an `archived` row (restorable via unarchive), then a
@@ -3070,9 +3184,14 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3070
3184
  const sRet = scopeId.parse(ulid());
3071
3185
  await host.admin.registerVertical(staff, { slug: 'retirable', name: 'Retirable', source: 'cli', ownerTenant: t2 });
3072
3186
  await host.provisionScope(staff, { tenantId: t2, scopeId: sRet, jurisdiction: 'eu', vertical: 'retirable' });
3073
- await expect(host.admin.deleteVertical(staff, 'retirable')).rejects.toThrow(/still backs 1 scope\(s\) — delete or rebind/);
3187
+ // Both halves are `conflict` and the sentence is what tells them apart, so both are
3188
+ // asserted (#113 phase 5). The CODE is what the transport renders the 409 from —
3189
+ // the archived half reached the control plane as a generic 500 until these throws
3190
+ // were typed, because the pattern row that carried the live one read
3191
+ // `/still backs \d+ scope\(s\)/` and the word `archived` sits in the middle of it.
3192
+ await expectRefusal(host.admin.deleteVertical(staff, 'retirable'), 'conflict', /still backs 1 scope\(s\) — delete or rebind/);
3074
3193
  await host.admin.archiveScope(staff, t2, sRet);
3075
- await expect(host.admin.deleteVertical(staff, 'retirable')).rejects.toThrow(/1 archived scope\(s\) — reap or restore/);
3194
+ await expectRefusal(host.admin.deleteVertical(staff, 'retirable'), 'conflict', /1 archived scope\(s\) — reap or restore/);
3076
3195
  await host.admin.reapScope(staff, t2, sRet);
3077
3196
  await host.admin.deleteVertical(staff, 'retirable');
3078
3197
  expect((await host.admin.listVerticals(staff)).some((v) => v.slug === 'retirable')).toBe(false);
@@ -3104,7 +3223,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3104
3223
  await host.admin.rejectVersion(staff, versionId, 'permission diff widened a role');
3105
3224
  await expect(host.admin.bindScopeVersion(staff, t1, s1, versionId)).rejects.toThrow(/rejected, not admitted/);
3106
3225
  // Terminal: a rejected version is not resurrected, a new one is published.
3107
- await expect(host.admin.admitVersion(staff, versionId)).rejects.toThrow(/was rejected/);
3226
+ await expectRefusal(host.admin.admitVersion(staff, versionId), 'conflict');
3108
3227
  const rejected = (await host.admin.listVersions(staff, 'callout')).find((v) => v.id === versionId);
3109
3228
  expect(rejected?.admissionNote).toContain('widened a role');
3110
3229
  });
@@ -3120,7 +3239,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3120
3239
  }
3121
3240
  });
3122
3241
  it('refuses a version for a vertical nobody registered', async () => {
3123
- await expect(host.admin.publishVersion(staff, {
3242
+ await expectRefusal(host.admin.publishVersion(staff, {
3124
3243
  id: ulid(),
3125
3244
  verticalSlug: 'ghost',
3126
3245
  version: '1.0.0',
@@ -3128,7 +3247,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3128
3247
  permissionDigest: 'p',
3129
3248
  migrationDigest: 'g',
3130
3249
  deploymentRef: null,
3131
- })).rejects.toThrow(/unknown vertical/);
3250
+ }), 'not_found');
3132
3251
  });
3133
3252
  // -- channels and promotion-time checkpoints (#31 step 2) ----------------
3134
3253
  // Promotion is the moment a change reaches anyone, so it is where §4's two
@@ -3871,6 +3990,124 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
3871
3990
  expect(reactions).toHaveLength(2);
3872
3991
  expect(new Set(reactions.map((r) => r.caused_by)).size).toBe(2);
3873
3992
  });
3993
+ // -- the invocation a DELIVERY belongs to (#1525) -------------------------
3994
+ //
3995
+ // #1237 put `invocation_id` on the outbox, which answers "which call emitted this
3996
+ // event". A delivery joins to that through `event_id`, so the tempting reading is
3997
+ // that deliveries needed nothing. What the event's id cannot say is which call
3998
+ // attempted the DELIVERY — and for an executor those come apart by design: attempt
3999
+ // one runs in the emitting call's post-commit tail, every retry after it in a drain
4000
+ // that is a different call or none at all.
4001
+ //
4002
+ // Each test takes its own scope: they assert over the whole delivery log they read.
4003
+ describe('the invocation a delivery belongs to (#1525)', () => {
4004
+ const flowScope = async () => {
4005
+ const s = scopeId.parse(ulid());
4006
+ await host.provisionScope(staff, { tenantId: t1, scopeId: s, vertical: 'flow-vertical' });
4007
+ await host.admin.activateScope(staff, t1, s);
4008
+ return s;
4009
+ };
4010
+ it('stamps a consumer delivery with the invocation the call carried', async () => {
4011
+ const s = await flowScope();
4012
+ const stub = await host.getScope(alice, t1, s);
4013
+ const call = ulid();
4014
+ await stub.invoke('flow/produce', undefined, { invocationId: call });
4015
+ // The VALUE, not merely the column: a delivery that recorded null here would
4016
+ // satisfy every "the column exists" assertion and join to nothing.
4017
+ const rows = (await stub.invoke('flow/deliveries'));
4018
+ expect(rows).toHaveLength(2);
4019
+ expect(rows.map((r) => r.invocation_id)).toEqual([call, call]);
4020
+ // A SECOND call is a second id. The field is cleared after each invocation
4021
+ // (#1237), so a leak would file this call's deliveries under the previous one
4022
+ // — which a single-call assertion cannot see.
4023
+ const next = ulid();
4024
+ await stub.invoke('flow/produce', undefined, { invocationId: next });
4025
+ const after = (await stub.invoke('flow/deliveries'));
4026
+ expect(after).toHaveLength(4);
4027
+ expect(after.filter((r) => r.invocation_id === call)).toHaveLength(2);
4028
+ expect(after.filter((r) => r.invocation_id === next)).toHaveLength(2);
4029
+ });
4030
+ it('records null when the caller carried no invocation, and still delivers', async () => {
4031
+ // A seed, a test, an internal call. Null is the honest answer — and the
4032
+ // delivery itself must still happen and still be journalled, since a drain that
4033
+ // failed because no id was carried is a far worse outcome than an unjoined row.
4034
+ // The positive twin above shares this mechanism, so breaking the write reddens
4035
+ // that one rather than leaving both green on a column nobody fills.
4036
+ const s = await flowScope();
4037
+ const stub = await host.getScope(alice, t1, s);
4038
+ await stub.invoke('flow/produce');
4039
+ const rows = (await stub.invoke('flow/deliveries'));
4040
+ expect(rows).toHaveLength(2);
4041
+ expect(rows.every((r) => r.invocation_id === null)).toBe(true);
4042
+ expect(rows.every((r) => r.error === null)).toBe(true);
4043
+ });
4044
+ it('carries the delivery\'s invocation out through the effects walk', async () => {
4045
+ // The column is written; this is what makes it reachable. `walkEventEffects`
4046
+ // is the read that renders a delivery, so a column added to the store and
4047
+ // missed there would leave the join invisible to every surface that shows one.
4048
+ const s = await flowScope();
4049
+ const stub = await host.getScope(alice, t1, s);
4050
+ const call = ulid();
4051
+ await stub.invoke('flow/produce', undefined, { invocationId: call });
4052
+ const rows = (await stub.invoke('flow/causes'));
4053
+ const step1 = rows.find((r) => r.type === 'flow.step1');
4054
+ const tree = await host.admin.eventEffects(staff, t1, s, {
4055
+ eventId: eventId.parse(step1.id),
4056
+ });
4057
+ expect(tree.root.deliveries).not.toHaveLength(0);
4058
+ expect(tree.root.deliveries.every((d) => d.invocationId === call)).toBe(true);
4059
+ });
4060
+ it('stamps an executor delivery with the call whose tail ran it', async () => {
4061
+ // Executors are the half the event's own id could never have covered. On the
4062
+ // hosted adapter this journal is written from the COORDINATOR over an RPC, so
4063
+ // there is no ambient field to read and the id has to survive the hop.
4064
+ const s = scopeId.parse(ulid());
4065
+ await host.provisionScope(staff, {
4066
+ tenantId: t1,
4067
+ scopeId: s,
4068
+ vertical: 'connector-vertical',
4069
+ });
4070
+ await host.admin.activateScope(staff, t1, s);
4071
+ const stub = await host.getScope(alice, t1, s);
4072
+ const call = ulid();
4073
+ await stub.invoke('connector/request-effect', { tag: 'inv-ok' }, { invocationId: call });
4074
+ const emitted = await host.admin.invocationEvents(staff, t1, s, { invocationId: call });
4075
+ const requested = emitted.events.find((e) => e.type === 'effect.requested');
4076
+ const tree = await host.admin.eventEffects(staff, t1, s, { eventId: requested.id });
4077
+ const delivery = tree.root.deliveries.find((d) => d.consumer === 'executor:flaky-effector');
4078
+ expect(delivery).toBeDefined();
4079
+ expect(delivery.state).toBe('delivered');
4080
+ expect(delivery.invocationId).toBe(call);
4081
+ });
4082
+ it('files a RETRY under the call that re-attempted it, not the one that emitted', async () => {
4083
+ // The test that proves this column is not a copy of the outbox's. A doomed
4084
+ // executor is attempted once in the emitting call's tail and once more on a
4085
+ // drain — and the drain is not a call. So the dead row's event still names the
4086
+ // request that produced it while the attempt that gave up names none, which is
4087
+ // exactly the pair `DeadLetter` could not previously tell apart.
4088
+ const s = scopeId.parse(ulid());
4089
+ await host.provisionScope(staff, {
4090
+ tenantId: t1,
4091
+ scopeId: s,
4092
+ vertical: 'connector-vertical',
4093
+ });
4094
+ await host.admin.activateScope(staff, t1, s);
4095
+ const stub = await host.getScope(alice, t1, s);
4096
+ const call = ulid();
4097
+ await stub.invoke('connector/request-doomed', { tag: 'inv-doomed' }, { invocationId: call });
4098
+ // maxAttempts 2: attempt one ran inline under `call`, attempt two runs here.
4099
+ expect((await host.drainDue(t1, s)).deadLettered).toBe(1);
4100
+ const page = await host.admin.deadLetters(staff, t1, s, {});
4101
+ const dead = page.entries.find((d) => d.consumer === 'executor:doomed-effector');
4102
+ expect(dead).toBeDefined();
4103
+ // The EVENT still joins to the request that emitted it…
4104
+ expect(dead.invocationId).toBe(call);
4105
+ // …and the ATTEMPT that gave up names no call, because a drain is not one. An
4106
+ // implementation that copied the event's id here would read `call` and claim a
4107
+ // request had done something it did not do.
4108
+ expect(dead.attemptInvocationId).toBeNull();
4109
+ });
4110
+ });
3874
4111
  it('creates a tenant record, idempotently; only real creates are audited', async () => {
3875
4112
  await host.admin.createTenant(staff, { id: t3, slug: 'acme-co', name: 'Acme Co' });
3876
4113
  await host.admin.createTenant(staff, { id: t3, slug: 'acme-co', name: 'Acme Co' }); // no-op