@substrat-run/contract-tests 0.144.0 → 0.146.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 (61) hide show
  1. package/dist/async-log-suite.d.ts.map +1 -1
  2. package/dist/async-log-suite.js +5 -0
  3. package/dist/async-log-suite.js.map +1 -1
  4. package/dist/bind-module.d.ts +22 -1
  5. package/dist/bind-module.d.ts.map +1 -1
  6. package/dist/bind-module.js +32 -1
  7. package/dist/bind-module.js.map +1 -1
  8. package/dist/binding-invite-suite.d.ts.map +1 -1
  9. package/dist/binding-invite-suite.js +53 -0
  10. package/dist/binding-invite-suite.js.map +1 -1
  11. package/dist/derived-module.d.ts +72 -1
  12. package/dist/derived-module.d.ts.map +1 -1
  13. package/dist/derived-module.js +31 -3
  14. package/dist/derived-module.js.map +1 -1
  15. package/dist/derived-suite.d.ts.map +1 -1
  16. package/dist/derived-suite.js +91 -0
  17. package/dist/derived-suite.js.map +1 -1
  18. package/dist/entity-state-module.d.ts +16 -0
  19. package/dist/entity-state-module.d.ts.map +1 -1
  20. package/dist/entity-trash-module.d.ts +80 -0
  21. package/dist/entity-trash-module.d.ts.map +1 -1
  22. package/dist/entity-trash-module.js +73 -0
  23. package/dist/entity-trash-module.js.map +1 -1
  24. package/dist/entity-trash-suite.d.ts.map +1 -1
  25. package/dist/entity-trash-suite.js +94 -2
  26. package/dist/entity-trash-suite.js.map +1 -1
  27. package/dist/erasure-module.d.ts +8 -0
  28. package/dist/erasure-module.d.ts.map +1 -1
  29. package/dist/findings-suite.d.ts.map +1 -1
  30. package/dist/findings-suite.js +55 -1
  31. package/dist/findings-suite.js.map +1 -1
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/module-log-suite.d.ts.map +1 -1
  37. package/dist/module-log-suite.js +12 -0
  38. package/dist/module-log-suite.js.map +1 -1
  39. package/dist/modules.d.ts +253 -1
  40. package/dist/modules.d.ts.map +1 -1
  41. package/dist/modules.js +17 -5
  42. package/dist/modules.js.map +1 -1
  43. package/dist/peer-suite.d.ts.map +1 -1
  44. package/dist/peer-suite.js +75 -1
  45. package/dist/peer-suite.js.map +1 -1
  46. package/dist/scope-host-suite.d.ts.map +1 -1
  47. package/dist/scope-host-suite.js +501 -0
  48. package/dist/scope-host-suite.js.map +1 -1
  49. package/dist/stamps-module.d.ts +462 -0
  50. package/dist/stamps-module.d.ts.map +1 -0
  51. package/dist/stamps-module.js +242 -0
  52. package/dist/stamps-module.js.map +1 -0
  53. package/dist/stamps-suite.d.ts +3 -0
  54. package/dist/stamps-suite.d.ts.map +1 -0
  55. package/dist/stamps-suite.js +170 -0
  56. package/dist/stamps-suite.js.map +1 -0
  57. package/dist/vertical-events-suite.d.ts +16 -0
  58. package/dist/vertical-events-suite.d.ts.map +1 -1
  59. package/dist/vertical-events-suite.js +30 -3
  60. package/dist/vertical-events-suite.js.map +1 -1
  61. package/package.json +3 -3
@@ -5953,6 +5953,507 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
5953
5953
  await host.admin.promoteVersion(staff, 'callout', 'prod', v, { permissionChange: true, migrationChange: true });
5954
5954
  expect((await host.admin.listChannels(staff, 'callout')).find((c) => c.channel === 'prod')?.versionId).toBe(v);
5955
5955
  });
5956
+ // -- a version that strands people a link bound (#2178) -------------------
5957
+ // A binding shape's people keep it until they are released, and the levers need the shape:
5958
+ // an unbind needs it to bind, a retirement needs it to be a bootstrap shape, an erasure finds
5959
+ // them through the holder the scope recorded. So a version may drop or move a shape the
5960
+ // running one binds with only once it retires the keys, and no acknowledgement passes it.
5961
+ describe('a version that strands people a link bound (#2178)', () => {
5962
+ const HOLDER = { table: 'bind_records', idColumn: 'id', principalColumn: 'principal' };
5963
+ const KEY = 'contact:read-own';
5964
+ const binding = (over = {}) => ({
5965
+ entityType: 'contact', permissions: [KEY], bootstrap: true, holder: HOLDER, bind: { permission: 'contact:invite' }, ...over,
5966
+ });
5967
+ const shapes = {
5968
+ binds: [binding()],
5969
+ dropped: [],
5970
+ moved: [binding({ holder: { ...HOLDER, principalColumn: 'owner' } })],
5971
+ sharing: [{ entityType: 'contact', permissions: [KEY] }],
5972
+ unbound: [binding({ bind: undefined })],
5973
+ // Every key retired: nothing is left to strand, so the holder may move and the shape go.
5974
+ retired: [binding({ permissions: [], retired: [KEY], holder: { ...HOLDER, principalColumn: 'owner' } })],
5975
+ };
5976
+ // One digest pair for every version, so the permission and migration checkpoints never fire:
5977
+ // what refuses below is this gate alone.
5978
+ const publishShaped = async (slug, entityGrants, admit = true) => {
5979
+ const id = ulid();
5980
+ await host.admin.publishVersion(staff, {
5981
+ id, verticalSlug: slug, version: `3.0.${id.slice(-4).toLowerCase()}`, manifestDigest: `m-${id}`,
5982
+ permissionDigest: 'p', migrationDigest: 'g', deploymentRef: null,
5983
+ manifestJson: JSON.stringify({ registry: { permissions: [], roles: [], entityGrants } }),
5984
+ });
5985
+ if (admit)
5986
+ await host.admin.admitVersion(staff, id);
5987
+ return id;
5988
+ };
5989
+ const listed = async () => {
5990
+ const slug = `acme/bind-${ulid().slice(-8).toLowerCase()}`;
5991
+ await host.admin.registerVertical(staff, { slug, name: slug, source: 'cli' });
5992
+ const v = {};
5993
+ for (const k of Object.keys(shapes))
5994
+ v[k] = await publishShaped(slug, shapes[k]);
5995
+ return { slug, v };
5996
+ };
5997
+ const prodOf = async (slug) => (await host.admin.listChannels(staff, slug)).find((c) => c.channel === 'prod')?.versionId;
5998
+ const install = async (slug, version, extra = {}) => {
5999
+ const sc = scopeId.parse(ulid());
6000
+ await host.provisionScope(staff, { tenantId: t1, scopeId: sc, vertical: slug, ...extra });
6001
+ await host.admin.activateScope(staff, t1, sc);
6002
+ if (version)
6003
+ await host.admin.bindScopeVersion(staff, t1, sc, version);
6004
+ return sc;
6005
+ };
6006
+ const boundTo = async (sc) => (await host.admin.getScopeRecord(staff, t1, sc))?.verticalVersionId;
6007
+ const ALL_ACKS = { permissionChange: true, migrationChange: true, exportBreak: true };
6008
+ /**
6009
+ * #2185: a reconcile that answered it left no retirement open, and (#2184) that the scope has
6010
+ * bound through `contact`, as a reconcile of a version that binds it records.
6011
+ */
6012
+ const FINISHED = { retirementsFinished: true, boundShapes: ['contact'] };
6013
+ /** #2185: one that answered it gave a retirement up. */
6014
+ const LEFT_OPEN = { retirementsFinished: false, boundShapes: ['contact'] };
6015
+ /** #2184: what a refusal adds where the installs reported `contact` bound. */
6016
+ const BOUND = '(?:, where links have bound people to contact)?';
6017
+ it('a promote that drops, re-holds or un-bootstraps a shape the channel binds with is refused, whatever is acknowledged', async () => {
6018
+ const { slug, v } = await listed();
6019
+ await host.admin.promoteVersion(staff, slug, 'prod', v.binds);
6020
+ for (const [version, says] of [
6021
+ [v.dropped, /strand people bound to contact \(drops the shape; bound people keep contact:read-own\)/],
6022
+ [v.moved, /contact \(reads its holder from elsewhere; bound people keep contact:read-own\)/],
6023
+ [v.sharing, /contact \(makes it a sharing shape; bound people keep contact:read-own\)/],
6024
+ ]) {
6025
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', version), 'precondition_failed', says);
6026
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', version, ALL_ACKS), 'precondition_failed', /retire those keys first/i);
6027
+ expect(await prodOf(slug)).toBe(v.binds);
6028
+ }
6029
+ // The twins: the same shape, and one that only stops binding (its holder stays recorded and
6030
+ // it can still be retired), promote.
6031
+ await host.admin.promoteVersion(staff, slug, 'prod', await publishShaped(slug, shapes.binds));
6032
+ await host.admin.promoteVersion(staff, slug, 'prod', v.unbound);
6033
+ expect(await prodOf(slug)).toBe(v.unbound);
6034
+ });
6035
+ it('retiring every key is the way through: the holder may then move, and the shape go in a later version', async () => {
6036
+ const { slug, v } = await listed();
6037
+ await host.admin.promoteVersion(staff, slug, 'prod', v.binds);
6038
+ await host.admin.promoteVersion(staff, slug, 'prod', v.retired);
6039
+ expect(await prodOf(slug)).toBe(v.retired);
6040
+ await host.admin.promoteVersion(staff, slug, 'prod', v.dropped);
6041
+ expect(await prodOf(slug)).toBe(v.dropped);
6042
+ // Back again is a first appearance of the shape, not a strand.
6043
+ await host.admin.promoteVersion(staff, slug, 'prod', v.binds);
6044
+ expect(await prodOf(slug)).toBe(v.binds);
6045
+ });
6046
+ it('a bind is judged on the version the scope runs, forks and previews included; a first bind is not', async () => {
6047
+ const { slug, v } = await listed();
6048
+ const sc = await install(slug, v.binds);
6049
+ await expectRefusal(host.admin.bindScopeVersion(staff, t1, sc, v.dropped), 'precondition_failed', /drops the shape/);
6050
+ await expectRefusal(host.admin.bindScopeVersion(staff, t1, sc, v.moved, { acknowledge: { exportBreak: true } }), 'precondition_failed', /reads its holder/);
6051
+ // The read a caller asks before moving data refuses the same move.
6052
+ await expectRefusal(host.admin.bindingImpact(staff, t1, sc, v.dropped), 'precondition_failed', /drops the shape/);
6053
+ expect(await boundTo(sc)).toBe(v.binds);
6054
+ // People bound in a copy live in the copy's own spine.
6055
+ for (const extra of [{ kind: 'archive', forkedFrom: sc, forkedAt: new Date().toISOString() }, { kind: 'preview' }]) {
6056
+ const copy = await install(slug, v.binds, extra);
6057
+ await expectRefusal(host.admin.bindScopeVersion(staff, t1, copy, v.dropped), 'precondition_failed', /drops the shape/);
6058
+ }
6059
+ // The twins: a first bind ran nothing before it, and a retiring version moves the scope.
6060
+ const fresh = await install(slug);
6061
+ await host.admin.bindScopeVersion(staff, t1, fresh, v.dropped);
6062
+ expect(await boundTo(fresh)).toBe(v.dropped);
6063
+ await expect(host.admin.bindingImpact(staff, t1, sc, v.retired)).resolves.toEqual([]);
6064
+ await host.admin.bindScopeVersion(staff, t1, sc, v.retired);
6065
+ expect(await boundTo(sc)).toBe(v.retired);
6066
+ });
6067
+ it('routing a scope onto a serving script that runs a stranding version is refused like a bind', async () => {
6068
+ const { slug, v } = await listed();
6069
+ const sc = await install(slug, v.binds);
6070
+ const routeOf = async () => (await host.admin.getScopeRecord(staff, t1, sc))?.servingRef ?? null;
6071
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6072
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.dropped, doClasses: [], migrationTag: 'g' });
6073
+ await expectRefusal(host.admin.setScopeServingRef(staff, t1, sc, ref), 'precondition_failed', /drops the shape/);
6074
+ await expectRefusal(host.admin.bindingImpact(staff, t1, sc, v.binds, { servingRef: ref }), 'precondition_failed', /drops the shape/);
6075
+ expect(await routeOf()).toBeNull();
6076
+ // The twin: serving a version that retires the keys, the same route moves.
6077
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.retired, doClasses: [], migrationTag: 'g' });
6078
+ await host.admin.setScopeServingRef(staff, t1, sc, ref);
6079
+ expect(await routeOf()).toBe(ref);
6080
+ });
6081
+ it('a promote is judged from the version served in place too, and replacing what serves is judged like a promote (review of #2181)', async () => {
6082
+ const { slug, v } = await listed();
6083
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6084
+ await host.admin.promoteVersion(staff, slug, 'prod', v.binds);
6085
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.binds, doClasses: [], migrationTag: 'g' });
6086
+ // The retiring version promotes, but its serve has not happened: what serves still binds.
6087
+ await host.admin.promoteVersion(staff, slug, 'prod', v.retired);
6088
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v.dropped), 'precondition_failed', /drops the shape/);
6089
+ expect(await prodOf(slug)).toBe(v.retired);
6090
+ // Serving it straight onto the dropping version is the same move.
6091
+ await expectRefusal(host.admin.setVerticalServing(staff, slug, { ref, versionId: v.dropped, doClasses: [], migrationTag: 'g' }), 'precondition_failed', /drops the shape/);
6092
+ // The twins: the retiring version served, then the drop promotes and serves.
6093
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.retired, doClasses: [], migrationTag: 'g' });
6094
+ await host.admin.promoteVersion(staff, slug, 'prod', v.dropped);
6095
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.dropped, doClasses: [], migrationTag: 'g' });
6096
+ expect(await prodOf(slug)).toBe(v.dropped);
6097
+ });
6098
+ it('an owned scope on the serving script is judged from what it runs, not its lagging pointer (review of #2181)', async () => {
6099
+ const slug = `tenant-a/bind-${ulid().slice(-8).toLowerCase()}`;
6100
+ await host.admin.registerVertical(staff, { slug, name: slug, source: 'cli', ownerTenant: t1 });
6101
+ const [plain, binds, dropped] = [
6102
+ await publishShaped(slug, shapes.dropped, false),
6103
+ await publishShaped(slug, shapes.binds, false),
6104
+ await publishShaped(slug, shapes.dropped, false),
6105
+ ];
6106
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6107
+ await host.admin.promoteVersion(staff, slug, 'prod', plain);
6108
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: plain, doClasses: [], migrationTag: 'g' });
6109
+ // Born on the serving script, its pointer names a binding version it has never run.
6110
+ const sc = await install(slug, binds);
6111
+ expect((await host.admin.getScopeRecord(staff, t1, sc))?.servingRef).toBe(ref);
6112
+ await host.admin.promoteVersion(staff, slug, 'prod', dropped);
6113
+ expect(await prodOf(slug)).toBe(dropped);
6114
+ expect(await boundTo(sc)).toBe(dropped);
6115
+ });
6116
+ it('a retirement counts for a scope only once it has reconciled the retiring version (review of #2181)', async () => {
6117
+ const { slug, v } = await listed();
6118
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6119
+ await host.admin.promoteVersion(staff, slug, 'prod', v.binds);
6120
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.binds, doClasses: [], migrationTag: 'g' });
6121
+ // One install on the serving script, one bound by pointer: both reconciled the binding version.
6122
+ const routed = await install(slug, v.binds);
6123
+ const pinned = await install(slug, v.binds);
6124
+ await host.admin.setScopeServingRef(staff, t1, pinned, null);
6125
+ // A version that retires nothing is written finished by every caller that reconciles it (#2185).
6126
+ for (const sc of [routed, pinned])
6127
+ await host.admin.markScopeProvisioned(staff, t1, sc, v.binds, FINISHED);
6128
+ // The retiring version promotes, serves and is bound; no install has reconciled it yet.
6129
+ await host.admin.promoteVersion(staff, slug, 'prod', v.retired);
6130
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.retired, doClasses: [], migrationTag: 'g' });
6131
+ await host.admin.bindScopeVersion(staff, t1, pinned, v.retired);
6132
+ // So the drop is refused at each door: the promote, the serve, and the pinned install's bind,
6133
+ // each naming the install it waits for.
6134
+ const waitsFor = (sc) => new RegExp(`drops the shape.*Judged from 1 install\\(s\\) that last reconciled ${v.binds}${BOUND} \\(one is ${sc}\\)`);
6135
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v.dropped), 'precondition_failed', waitsFor(routed));
6136
+ await expectRefusal(host.admin.setVerticalServing(staff, slug, { ref, versionId: v.dropped, doClasses: [], migrationTag: 'g' }), 'precondition_failed', waitsFor(routed));
6137
+ await expectRefusal(host.admin.bindScopeVersion(staff, t1, pinned, v.dropped), 'precondition_failed', waitsFor(pinned));
6138
+ // A copy never reconciles, so its receipt is not read: its bind is judged on what it runs.
6139
+ const copy = await install(slug, v.binds, { kind: 'archive', forkedFrom: pinned, forkedAt: new Date().toISOString() });
6140
+ await host.admin.markScopeProvisioned(staff, t1, copy, v.binds);
6141
+ await host.admin.bindScopeVersion(staff, t1, copy, v.retired);
6142
+ await host.admin.bindScopeVersion(staff, t1, copy, v.dropped);
6143
+ // The twin: once every install has reconciled the retiring version, and its retirements
6144
+ // finished (#2185), the drop passes everywhere.
6145
+ for (const sc of [routed, pinned])
6146
+ await host.admin.markScopeProvisioned(staff, t1, sc, v.retired, FINISHED);
6147
+ await host.admin.promoteVersion(staff, slug, 'prod', v.dropped);
6148
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.dropped, doClasses: [], migrationTag: 'g' });
6149
+ await host.admin.bindScopeVersion(staff, t1, pinned, v.dropped);
6150
+ expect(await prodOf(slug)).toBe(v.dropped);
6151
+ expect(await boundTo(pinned)).toBe(v.dropped);
6152
+ });
6153
+ it('an install with no receipt counts as not reconciled, but only for a move that would strand otherwise (review of #2181, round 2)', async () => {
6154
+ const { slug, v } = await listed();
6155
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6156
+ await host.admin.promoteVersion(staff, slug, 'prod', v.binds);
6157
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.binds, doClasses: [], migrationTag: 'g' });
6158
+ const routed = await install(slug, v.binds);
6159
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.binds, FINISHED);
6160
+ await host.admin.promoteVersion(staff, slug, 'prod', v.retired);
6161
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.retired, doClasses: [], migrationTag: 'g' });
6162
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.retired, FINISHED);
6163
+ // Its receipt cleared (a rewind here; a switch does the same on the hosted adapter): it may
6164
+ // not have run the retirement, so the drop waits for it, and says so.
6165
+ await host.admin.markScopeProvisioned(staff, t1, routed, null);
6166
+ const noReceipt = new RegExp(`drops the shape.*Judged from 1 install\\(s\\) with no reconcile receipt.*\\(one is ${routed}\\)`);
6167
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v.dropped), 'precondition_failed', noReceipt);
6168
+ // A move that strands nobody even with the retirement unrun is not refused for it: a key added.
6169
+ const added = await publishShaped(slug, [binding({ permissions: ['contact:other'], retired: [KEY], holder: { ...HOLDER, principalColumn: 'owner' } })]);
6170
+ await host.admin.promoteVersion(staff, slug, 'prod', added);
6171
+ const retiredBoth = await publishShaped(slug, [binding({ permissions: [], retired: [KEY, 'contact:other'], holder: { ...HOLDER, principalColumn: 'owner' } })]);
6172
+ await host.admin.promoteVersion(staff, slug, 'prod', retiredBoth);
6173
+ // The twin: served, reconciled again with its retirements finished, the drop passes. (Review of
6174
+ // #2220: a receipt counts only for the version the install runs, as the sweep writes it.)
6175
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: retiredBoth, doClasses: [], migrationTag: 'g' });
6176
+ await host.admin.markScopeProvisioned(staff, t1, routed, retiredBoth, FINISHED);
6177
+ await host.admin.promoteVersion(staff, slug, 'prod', v.dropped);
6178
+ expect(await prodOf(slug)).toBe(v.dropped);
6179
+ });
6180
+ /**
6181
+ * #2185: a receipt says a reconcile ran, not that it finished the version's retirements: one
6182
+ * that gave a retirement up (`retirementsLeftOpen`) still writes it. So the directory also
6183
+ * records the version whose retirements FINISHED, and an install whose receipt's version has
6184
+ * not is judged as still holding the keys that version retires — limitation (a) of #2181.
6185
+ */
6186
+ describe('the retirement finished, not only reconciled (#2185)', () => {
6187
+ const unfinished = (version, sc) => new RegExp(`drops the shape.*Judged from 1 install\\(s\\) that reconciled ${version} with its retirements not finished${BOUND} \\(one is ${sc}\\); the drop waits for their retirements to finish`);
6188
+ const retiringFleet = async () => {
6189
+ const { slug, v } = await listed();
6190
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6191
+ await host.admin.promoteVersion(staff, slug, 'prod', v.binds);
6192
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.binds, doClasses: [], migrationTag: 'g' });
6193
+ const routed = await install(slug, v.binds);
6194
+ const pinned = await install(slug, v.binds);
6195
+ await host.admin.setScopeServingRef(staff, t1, pinned, null);
6196
+ for (const sc of [routed, pinned])
6197
+ await host.admin.markScopeProvisioned(staff, t1, sc, v.binds, FINISHED);
6198
+ await host.admin.promoteVersion(staff, slug, 'prod', v.retired);
6199
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.retired, doClasses: [], migrationTag: 'g' });
6200
+ await host.admin.bindScopeVersion(staff, t1, pinned, v.retired);
6201
+ return { slug, v, ref, routed, pinned };
6202
+ };
6203
+ it('records the finished version beside the receipt, and a receipt written without it clears it', async () => {
6204
+ const { v, routed } = await retiringFleet();
6205
+ const record = async () => {
6206
+ const r = await host.admin.getScopeRecord(staff, t1, routed);
6207
+ return { provisioned: r?.provisionedVersionId, finished: r?.retirementsFinishedVersionId ?? null };
6208
+ };
6209
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.retired, FINISHED);
6210
+ expect(await record()).toEqual({ provisioned: v.retired, finished: v.retired });
6211
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.retired, LEFT_OPEN);
6212
+ expect(await record()).toEqual({ provisioned: v.retired, finished: null });
6213
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.retired, FINISHED);
6214
+ // A caller that cannot say (an old vertical-host's answer) clears it.
6215
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.retired);
6216
+ expect(await record()).toEqual({ provisioned: v.retired, finished: null });
6217
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.retired, FINISHED);
6218
+ // A cleared receipt (a rewind) clears it too.
6219
+ await host.admin.markScopeProvisioned(staff, t1, routed, null, FINISHED);
6220
+ expect(await record()).toEqual({ provisioned: null, finished: null });
6221
+ });
6222
+ it('(a) a reconcile that left a retirement open blocks the drop at every door, naming the install; finished, the drop passes', async () => {
6223
+ const { slug, v, ref, routed, pinned } = await retiringFleet();
6224
+ // Both installs reconciled the retiring version, and each gave a retirement up.
6225
+ for (const sc of [routed, pinned])
6226
+ await host.admin.markScopeProvisioned(staff, t1, sc, v.retired, LEFT_OPEN);
6227
+ // The promote and the serve move the routed install; the pinned one moves at its own bind.
6228
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v.dropped), 'precondition_failed', unfinished(v.retired, routed));
6229
+ await expectRefusal(host.admin.setVerticalServing(staff, slug, { ref, versionId: v.dropped, doClasses: [], migrationTag: 'g' }), 'precondition_failed', unfinished(v.retired, routed));
6230
+ await expectRefusal(host.admin.bindScopeVersion(staff, t1, pinned, v.dropped), 'precondition_failed', unfinished(v.retired, pinned));
6231
+ expect(await prodOf(slug)).toBe(v.retired);
6232
+ // The twins: once each install's retirements finish, its door opens, and only its own.
6233
+ await host.admin.markScopeProvisioned(staff, t1, pinned, v.retired, FINISHED);
6234
+ await host.admin.bindScopeVersion(staff, t1, pinned, v.dropped);
6235
+ expect(await boundTo(pinned)).toBe(v.dropped);
6236
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v.dropped), 'precondition_failed', unfinished(v.retired, routed));
6237
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.retired, FINISHED);
6238
+ await host.admin.promoteVersion(staff, slug, 'prod', v.dropped);
6239
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.dropped, doClasses: [], migrationTag: 'g' });
6240
+ expect(await prodOf(slug)).toBe(v.dropped);
6241
+ });
6242
+ it('refuses only a move that would strand with the retirement unrun: a key added, a key dropped and retired still move', async () => {
6243
+ const { slug, v, ref, routed, pinned } = await retiringFleet();
6244
+ for (const sc of [routed, pinned])
6245
+ await host.admin.markScopeProvisioned(staff, t1, sc, v.retired, LEFT_OPEN);
6246
+ const added = await publishShaped(slug, [binding({ permissions: ['contact:other'], retired: [KEY], holder: { ...HOLDER, principalColumn: 'owner' } })]);
6247
+ await host.admin.promoteVersion(staff, slug, 'prod', added);
6248
+ const retiredBoth = await publishShaped(slug, [binding({ permissions: [], retired: [KEY, 'contact:other'], holder: { ...HOLDER, principalColumn: 'owner' } })]);
6249
+ await host.admin.promoteVersion(staff, slug, 'prod', retiredBoth);
6250
+ expect(await prodOf(slug)).toBe(retiredBoth);
6251
+ // And the version retiring every key, retire-all, then the drop: refused until finished.
6252
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v.dropped), 'precondition_failed', /retirements not finished/);
6253
+ // Served and bound, then reconciled: a receipt counts only for what the install runs (review of #2220).
6254
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: retiredBoth, doClasses: [], migrationTag: 'g' });
6255
+ await host.admin.bindScopeVersion(staff, t1, pinned, retiredBoth);
6256
+ for (const sc of [routed, pinned])
6257
+ await host.admin.markScopeProvisioned(staff, t1, sc, retiredBoth, FINISHED);
6258
+ await host.admin.promoteVersion(staff, slug, 'prod', v.dropped);
6259
+ expect(await prodOf(slug)).toBe(v.dropped);
6260
+ });
6261
+ it('a version that retires nothing is judged the same finished or not: an unfinished answer blocks no other change', async () => {
6262
+ const { slug, v } = await listed();
6263
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6264
+ await host.admin.promoteVersion(staff, slug, 'prod', v.binds);
6265
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.binds, doClasses: [], migrationTag: 'g' });
6266
+ const routed = await install(slug, v.binds);
6267
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.binds, LEFT_OPEN);
6268
+ // A key added promotes; the drop is refused for the shape, as it is with every install finished.
6269
+ const more = await publishShaped(slug, [binding({ permissions: [KEY, 'contact:other'] })]);
6270
+ await host.admin.promoteVersion(staff, slug, 'prod', more);
6271
+ expect(await prodOf(slug)).toBe(more);
6272
+ });
6273
+ it('(b) a copy is still not judged from its record: its bind is judged on what it runs, as before', async () => {
6274
+ const { slug, v, pinned } = await retiringFleet();
6275
+ const fork = await install(slug, v.binds, {
6276
+ kind: 'archive',
6277
+ forkedFrom: pinned,
6278
+ forkedAt: new Date().toISOString(),
6279
+ });
6280
+ await host.admin.markScopeProvisioned(staff, t1, fork, v.retired, LEFT_OPEN);
6281
+ await host.admin.bindScopeVersion(staff, t1, fork, v.retired);
6282
+ await host.admin.bindScopeVersion(staff, t1, fork, v.dropped);
6283
+ expect(await boundTo(fork)).toBe(v.dropped);
6284
+ });
6285
+ });
6286
+ /**
6287
+ * #2184: dropping `bind` stops new links and leaves the people bound holding the shape. Judged
6288
+ * from the registry alone, a later version could then strand them, since no version binds any
6289
+ * more. So each install's reconcile reports the types it has ever bound (`boundShapes`, paired
6290
+ * with its receipt), and the gate counts a table-held bootstrap shape as binding wherever an
6291
+ * install bound it, or did not report.
6292
+ */
6293
+ describe('a shape bound before its `bind` was dropped (#2184)', () => {
6294
+ const reported = (types) => ({ retirementsFinished: true, boundShapes: types });
6295
+ const unbound = (over = {}) => binding({ bind: undefined, ...over });
6296
+ const boundBy = (version, sc, types = 'contact') => new RegExp(`Judged from 1 install\\(s\\) that last reconciled ${version}, where links have bound people to ${types} \\(one is ${sc}\\)`);
6297
+ /** A fleet that bound through `contact`, then took `bind` away: every install reconciled that. */
6298
+ const unboundFleet = async () => {
6299
+ const { slug, v } = await listed();
6300
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6301
+ await host.admin.promoteVersion(staff, slug, 'prod', v.binds);
6302
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.binds, doClasses: [], migrationTag: 'g' });
6303
+ const routed = await install(slug, v.binds);
6304
+ const pinned = await install(slug, v.binds);
6305
+ await host.admin.setScopeServingRef(staff, t1, pinned, null);
6306
+ for (const sc of [routed, pinned])
6307
+ await host.admin.markScopeProvisioned(staff, t1, sc, v.binds, reported(['contact']));
6308
+ // Dropping `bind` strands no one, so it moves at every door.
6309
+ await host.admin.promoteVersion(staff, slug, 'prod', v.unbound);
6310
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.unbound, doClasses: [], migrationTag: 'g' });
6311
+ await host.admin.bindScopeVersion(staff, t1, pinned, v.unbound);
6312
+ for (const sc of [routed, pinned])
6313
+ await host.admin.markScopeProvisioned(staff, t1, sc, v.unbound, reported(['contact']));
6314
+ return { slug, v, ref, routed, pinned };
6315
+ };
6316
+ it('records the bound shapes beside the receipt, paired with it; a receipt written without them, or none, clears them', async () => {
6317
+ const { v, routed } = await unboundFleet();
6318
+ const record = async () => (await host.admin.getScopeRecord(staff, t1, routed))?.boundShapes ?? null;
6319
+ expect(await record()).toEqual({ versionId: v.unbound, types: ['contact'] });
6320
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.unbound, { retirementsFinished: true, boundShapes: [] });
6321
+ expect(await record()).toEqual({ versionId: v.unbound, types: [] });
6322
+ // A caller that cannot say (packages that predate the report) clears them.
6323
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.unbound, { retirementsFinished: true });
6324
+ expect(await record()).toBeNull();
6325
+ await host.admin.markScopeProvisioned(staff, t1, routed, v.unbound, reported(['contact']));
6326
+ // A cleared receipt (a rewind, a restore) clears them too.
6327
+ await host.admin.markScopeProvisioned(staff, t1, routed, null, reported(['contact']));
6328
+ expect(await record()).toBeNull();
6329
+ });
6330
+ it('refuses dropping the shape after `bind` was dropped, at every door, naming the type and the install', async () => {
6331
+ const { slug, v, ref, routed, pinned } = await unboundFleet();
6332
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v.dropped), 'precondition_failed', boundBy(v.unbound, routed));
6333
+ await expectRefusal(host.admin.setVerticalServing(staff, slug, { ref, versionId: v.dropped, doClasses: [], migrationTag: 'g' }), 'precondition_failed', boundBy(v.unbound, routed));
6334
+ await expectRefusal(host.admin.bindScopeVersion(staff, t1, pinned, v.dropped), 'precondition_failed', boundBy(v.unbound, pinned));
6335
+ await expectRefusal(host.admin.bindingImpact(staff, t1, pinned, v.dropped), 'precondition_failed', /drops the shape/);
6336
+ // So is moving its holder, making it a sharing shape, or dropping a key unretired.
6337
+ const kept = await publishShaped(slug, [unbound({ permissions: [] })]);
6338
+ for (const [version, says] of [
6339
+ [await publishShaped(slug, [unbound({ holder: { ...HOLDER, principalColumn: 'owner' } })]), /reads its holder from elsewhere/],
6340
+ [v.sharing, /makes it a sharing shape/],
6341
+ [kept, /drops keys from it without retiring them/],
6342
+ ]) {
6343
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', version), 'precondition_failed', says);
6344
+ }
6345
+ expect(await prodOf(slug)).toBe(v.unbound);
6346
+ });
6347
+ it('lets the same fleet through a legitimate evolution: a key added, then every key retired, then the drop', async () => {
6348
+ const { slug, v, ref, routed, pinned } = await unboundFleet();
6349
+ const added = await publishShaped(slug, [unbound({ permissions: [KEY, 'contact:other'] })]);
6350
+ await host.admin.promoteVersion(staff, slug, 'prod', added);
6351
+ const retiredAll = await publishShaped(slug, [unbound({ permissions: [], retired: [KEY, 'contact:other'] })]);
6352
+ await host.admin.promoteVersion(staff, slug, 'prod', retiredAll);
6353
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: retiredAll, doClasses: [], migrationTag: 'g' });
6354
+ await host.admin.bindScopeVersion(staff, t1, pinned, retiredAll);
6355
+ // Until both installs reconcile the retirement, the drop waits for them.
6356
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v.dropped), 'precondition_failed', boundBy(v.unbound, routed));
6357
+ for (const sc of [routed, pinned])
6358
+ await host.admin.markScopeProvisioned(staff, t1, sc, retiredAll, reported(['contact']));
6359
+ await host.admin.promoteVersion(staff, slug, 'prod', v.dropped);
6360
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: v.dropped, doClasses: [], migrationTag: 'g' });
6361
+ await host.admin.bindScopeVersion(staff, t1, pinned, v.dropped);
6362
+ expect(await prodOf(slug)).toBe(v.dropped);
6363
+ });
6364
+ it('a vertical whose table-held shape never bound: refused only while its installs have not reported, then free', async () => {
6365
+ const slug = `acme/plain-${ulid().slice(-8).toLowerCase()}`;
6366
+ await host.admin.registerVertical(staff, { slug, name: slug, source: 'cli' });
6367
+ const plain = await publishShaped(slug, [unbound()]);
6368
+ const dropped = await publishShaped(slug, []);
6369
+ await host.admin.promoteVersion(staff, slug, 'prod', plain);
6370
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6371
+ await host.admin.setVerticalServing(staff, slug, { ref, versionId: plain, doClasses: [], migrationTag: 'g' });
6372
+ const routed = await install(slug, plain);
6373
+ // A reconcile answered by packages that predate the report: unknown, so judged strictly.
6374
+ await host.admin.markScopeProvisioned(staff, t1, routed, plain, { retirementsFinished: true });
6375
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', dropped), 'precondition_failed', new RegExp(`Judged from 1 install\\(s\\) that reconciled ${plain} without reporting which binding shapes links have bound \\(one is ${routed}\\), ` +
6376
+ `so every bootstrap shape of ${plain} with a table holder counts as bound; the drop waits for their report`));
6377
+ // Reported: it bound nothing, so the drop passes.
6378
+ await host.admin.markScopeProvisioned(staff, t1, routed, plain, reported([]));
6379
+ await host.admin.promoteVersion(staff, slug, 'prod', dropped);
6380
+ expect(await prodOf(slug)).toBe(dropped);
6381
+ });
6382
+ /**
6383
+ * Review of #2220: a receipt behind what the installs run speaks for none of it. V2 adds
6384
+ * `bind`, and its reconcile commits in the scope and people claim, but its receipt never lands
6385
+ * (the re-assert after it failed, or the answer was lost); V3 drops `bind`; V4 removes the
6386
+ * shape. Judged from the V1 receipt's `[]` and V3's registry, V4 passed and stranded them.
6387
+ */
6388
+ it("judges a lagging receipt's running version as unreported: V1 [] → V2 binds, no receipt → V3 drops bind → V4 removes", async () => {
6389
+ const slug = `acme/lag-${ulid().slice(-8).toLowerCase()}`;
6390
+ await host.admin.registerVertical(staff, { slug, name: slug, source: 'cli' });
6391
+ const [v1, v2, v3, v4] = [await publishShaped(slug, [unbound()]), await publishShaped(slug, [binding()]), await publishShaped(slug, [unbound()]), await publishShaped(slug, [])];
6392
+ const ref = `serving-${ulid().slice(-8).toLowerCase()}`;
6393
+ const serve = (versionId) => host.admin.setVerticalServing(staff, slug, { ref, versionId, doClasses: [], migrationTag: 'g' });
6394
+ await host.admin.promoteVersion(staff, slug, 'prod', v1);
6395
+ await serve(v1);
6396
+ const routed = await install(slug, v1);
6397
+ const pinned = await install(slug, v1);
6398
+ await host.admin.setScopeServingRef(staff, t1, pinned, null);
6399
+ for (const sc of [routed, pinned])
6400
+ await host.admin.markScopeProvisioned(staff, t1, sc, v1, reported([]));
6401
+ // V2 and V3 reach both installs; no receipt is written for either.
6402
+ for (const v of [v2, v3]) {
6403
+ await host.admin.promoteVersion(staff, slug, 'prod', v);
6404
+ await serve(v);
6405
+ await host.admin.bindScopeVersion(staff, t1, pinned, v);
6406
+ }
6407
+ // A legitimate evolution that strands nothing still moves while the receipts lag: a key added.
6408
+ const more = await publishShaped(slug, [unbound({ permissions: [KEY, 'contact:other'] })]);
6409
+ await host.admin.promoteVersion(staff, slug, 'prod', more);
6410
+ await host.admin.promoteVersion(staff, slug, 'prod', v3);
6411
+ // The drop is refused at every door, naming the receipt and what the install runs.
6412
+ const lags = (sc) => new RegExp(`drops the shape.*Judged from 1 install\\(s\\) that last reconciled ${v1} \\(one is ${sc}\\), and run ${v3}, which that receipt does not speak for`);
6413
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v4), 'precondition_failed', lags(routed));
6414
+ await expectRefusal(serve(v4), 'precondition_failed', lags(routed));
6415
+ await expectRefusal(host.admin.bindScopeVersion(staff, t1, pinned, v4), 'precondition_failed', lags(pinned));
6416
+ // Reconciled: a scope that reports `contact` bound stays held, now from its own report…
6417
+ for (const sc of [routed, pinned])
6418
+ await host.admin.markScopeProvisioned(staff, t1, sc, v3, reported(['contact']));
6419
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', v4), 'precondition_failed', boundBy(v3, routed));
6420
+ // …and the twin: a receipt that is what the installs run, reporting nothing bound, is trusted.
6421
+ for (const sc of [routed, pinned])
6422
+ await host.admin.markScopeProvisioned(staff, t1, sc, v3, reported([]));
6423
+ await host.admin.promoteVersion(staff, slug, 'prod', v4);
6424
+ await serve(v4);
6425
+ await host.admin.bindScopeVersion(staff, t1, pinned, v4);
6426
+ expect(await prodOf(slug)).toBe(v4);
6427
+ });
6428
+ it('(b) a copy is judged on what it runs, as before: its record is not read', async () => {
6429
+ const { slug, v, pinned } = await unboundFleet();
6430
+ const fork = await install(slug, v.unbound, { kind: 'archive', forkedFrom: pinned, forkedAt: new Date().toISOString() });
6431
+ await host.admin.markScopeProvisioned(staff, t1, fork, v.unbound, reported(['contact']));
6432
+ await host.admin.bindScopeVersion(staff, t1, fork, v.dropped);
6433
+ expect(await boundTo(fork)).toBe(v.dropped);
6434
+ });
6435
+ });
6436
+ it("a private promote judges each owned scope it re-points from the scope's own version, before anything moves", async () => {
6437
+ const slug = `tenant-a/bind-${ulid().slice(-8).toLowerCase()}`;
6438
+ await host.admin.registerVertical(staff, { slug, name: slug, source: 'cli', ownerTenant: t1 });
6439
+ const [plain, binds, dropped, retired] = [
6440
+ await publishShaped(slug, shapes.dropped, false),
6441
+ await publishShaped(slug, shapes.binds, false),
6442
+ await publishShaped(slug, shapes.dropped, false),
6443
+ await publishShaped(slug, shapes.retired, false),
6444
+ ];
6445
+ await host.admin.promoteVersion(staff, slug, 'prod', plain);
6446
+ // An owned scope lagging on a binding version the channel never served.
6447
+ const sc = await install(slug, binds);
6448
+ await expectRefusal(host.admin.promoteVersion(staff, slug, 'prod', dropped, ALL_ACKS), 'precondition_failed', /drops the shape/);
6449
+ expect(await prodOf(slug)).toBe(plain);
6450
+ expect(await boundTo(sc)).toBe(binds);
6451
+ // The twin: a version that retires the keys promotes and re-points it.
6452
+ await host.admin.promoteVersion(staff, slug, 'prod', retired);
6453
+ expect(await prodOf(slug)).toBe(retired);
6454
+ expect(await boundTo(sc)).toBe(retired);
6455
+ });
6456
+ });
5956
6457
  // -- private verticals: self-serve prod (builder-plane.md §4-revised) ----
5957
6458
  // A private vertical's blast radius is its own tenant — dev/staging already
5958
6459
  // run the same bundle in the same sandbox — so its versions self-admit on