@substrat-run/contract-tests 0.102.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,29 @@ scopeHostContractSuite('adapter-yours', async () => {
42
42
  The suite grows with the kernel (migration journal, crash-mid-migration,
43
43
  duplicate-delivery harnesses); adapters inherit new checks by upgrading.
44
44
 
45
+ ## Suites gated by mounting, not by a flag
46
+
47
+ Most suites take a `ScopeHostFixture` and every adapter mounts every one. A few need
48
+ something only some hosts can provide, and those are separate exports an adapter
49
+ mounts only when it can satisfy the fixture. There is deliberately no capability flag
50
+ that makes a shared suite skip on one adapter: a skipped test reads as coverage the
51
+ adapter does not have.
52
+
53
+ - **`grantExpiryContractSuite`** — the fixture returns `{ host, clock, cleanup }` where
54
+ `clock` is the `ManualClock` (from `@substrat-run/kernel`) the host was constructed
55
+ with. The suite grants a permission with an `expiresAt` an hour ahead, asserts it is
56
+ live, moves the clock past it and asserts the denial — the transition the wall clock
57
+ cannot show in a test. Mounted by `adapter-sqlite` (`new SqliteScopeHost({ dir,
58
+ clock: clock.read })`).
59
+
60
+ **Parity gap, stated:** `adapter-cloudflare` does not mount it. The Durable Object
61
+ reads the wall clock inside the DO, which workerd constructs — an option on
62
+ `CloudflareScopeHost` never reaches it, because the host only holds a stub. Getting a
63
+ clock there is a decision (caller-supplied `now` per invocation, a test-only seam, or
64
+ neither), tracked on #956. Until one is taken, the DO's checker runs the same
65
+ `expires_at` predicate against a `now` it reads itself, and the pure host is the one
66
+ held to the contract by this suite.
67
+
45
68
  ## Status
46
69
 
47
70
  Pre-release (0.x): the suite expands as kernel contracts land.
@@ -0,0 +1,14 @@
1
+ import { type ManualClock, type ScopeHost } from '@substrat-run/kernel';
2
+ /**
3
+ * What the fixture hands the suite. `clock` is the SAME `ManualClock` whose
4
+ * `read` the host was constructed with — the suite advances it and expects the
5
+ * host to notice. A fixture that built the host on a different clock would pass
6
+ * the "allowed" half and fail every "denied" one, which is the honest failure.
7
+ */
8
+ export interface GrantExpiryFixture {
9
+ host: ScopeHost;
10
+ clock: ManualClock;
11
+ cleanup: () => Promise<void>;
12
+ }
13
+ export declare function grantExpiryContractSuite(adapterName: string, makeFixture: () => Promise<GrantExpiryFixture>): void;
14
+ //# sourceMappingURL=grant-expiry-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grant-expiry-suite.d.ts","sourceRoot":"","sources":["../src/grant-expiry-suite.ts"],"names":[],"mappings":"AAgDA,OAAO,EAAQ,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAM9E;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAKD,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,GAC7C,IAAI,CAoJN"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Contract suite for grant expiry under an injected clock (#956).
3
+ *
4
+ * What it pins is one sentence: **a grant with an `expiresAt` stops granting the
5
+ * moment the HOST's clock passes it — not the wall clock, the host's.**
6
+ *
7
+ * `permissionContractSuite` already proves an ALREADY-expired grant is refused
8
+ * (dave's, dated 2000). What it cannot prove is the transition: a grant that is
9
+ * live now and dead an hour later, because the only way to get there against the
10
+ * wall clock is to wait. The pure host takes a `clock` (#812), and since #1160 its
11
+ * checker judges `expires_at` against that clock rather than `new Date()` — this
12
+ * suite is what holds it there. Without it, a refactor that quietly reverted the
13
+ * checker to the wall clock would pass every other suite: the dead grant of 2000
14
+ * is dead on both clocks.
15
+ *
16
+ * **Mounted, not flagged.** The fixture returns a `ManualClock` the suite moves,
17
+ * so only an adapter that can hand its host a clock can mount it. That is the
18
+ * pure SQLite host today. It is deliberately NOT a capability flag on the shared
19
+ * fixture that skips on the other adapter: a skipped test reads as coverage the
20
+ * adapter does not have.
21
+ *
22
+ * **Why the Cloudflare adapter does not mount it (the parity gap, written down).**
23
+ * The Durable Object host reads the wall clock inside the DO
24
+ * (`packages/adapter-cloudflare/src/scope-do.ts`, the `at` read in `invoke`, and
25
+ * the entitlement / system-grant reads beside it). The DO is constructed by
26
+ * workerd, not by `CloudflareScopeHost`, so an option on the host factory never
27
+ * reaches it — the host only holds a stub. The three ways a clock could get there
28
+ * were each a decision rather than a patch, and none was taken unattended:
29
+ * (a) the host sends the instant with each invocation, which makes `now`
30
+ * caller-supplied — a different trust story; (b) a test-only seam (an env flag
31
+ * or a DO method a test calls), honest but explicitly not the production path;
32
+ * (c) leave the fact untestable there and say so. This suite is (c): the pure
33
+ * host is held to the contract, the DO's checker runs the same SQL predicate
34
+ * (`expires_at IS NULL OR expires_at > ?`) against a `now` it reads itself, and
35
+ * the day the DO can take a clock, mounting this suite is the whole change.
36
+ */
37
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
38
+ import { instant, permissionKey, platformActorId, principalId, scopeId, tenantId, } from '@substrat-run/contracts';
39
+ import { ulid } from '@substrat-run/kernel';
40
+ import { permMod } from './modules.js';
41
+ const PERM_READ = permissionKey.parse('perm:read');
42
+ const PERM_USE = permissionKey.parse('perm:use');
43
+ const START = '2026-03-01T09:00:00.000Z';
44
+ const HOUR = 3_600_000;
45
+ export function grantExpiryContractSuite(adapterName, makeFixture) {
46
+ describe(`grant expiry under an injected clock: ${adapterName}`, () => {
47
+ let fixture;
48
+ let host;
49
+ let clock;
50
+ const t1 = tenantId.parse(ulid());
51
+ const s1 = scopeId.parse(ulid());
52
+ const alice = principalId.parse(ulid()); // grantedBy
53
+ const nora = principalId.parse(ulid()); // node-level, timed
54
+ const eve = principalId.parse(ulid()); // entity-narrowed, timed
55
+ const staff = platformActorId.parse(ulid());
56
+ const box = { entityType: 'box', entityId: 'b1' };
57
+ const probe = async (who, permission, entity) => {
58
+ const stub = await host.getScope(who, t1, s1);
59
+ const decision = await stub.invoke('perm/probe', { permission, entity });
60
+ return decision.allowed;
61
+ };
62
+ const at = (ms) => instant.parse(new Date(Date.parse(START) + ms).toISOString());
63
+ beforeAll(async () => {
64
+ fixture = await makeFixture();
65
+ host = fixture.host;
66
+ clock = fixture.clock;
67
+ clock.set(START);
68
+ host.registerModule(permMod);
69
+ await host.admin.createTenant(staff, { id: t1, slug: 'expiry-tenant', name: 'Expiry Tenant' });
70
+ await host.admin.grantEntitlement(staff, t1, 'perm');
71
+ await host.provisionScope(staff, { tenantId: t1, scopeId: s1, vertical: 'expiry-vertical' });
72
+ await host.admin.activateScope(staff, t1, s1);
73
+ // Both shapes a timed grant takes: node-level, and narrowed to one entity.
74
+ await host.admin.grant(staff, {
75
+ principalId: nora,
76
+ permission: PERM_READ,
77
+ node: { tenantId: t1, scopeId: s1 },
78
+ expiresAt: at(HOUR),
79
+ grantedBy: alice,
80
+ });
81
+ await host.admin.grant(staff, {
82
+ principalId: eve,
83
+ permission: PERM_READ,
84
+ node: { tenantId: t1, scopeId: s1 },
85
+ entity: box,
86
+ expiresAt: at(HOUR),
87
+ grantedBy: alice,
88
+ });
89
+ });
90
+ afterAll(async () => {
91
+ await fixture.cleanup();
92
+ });
93
+ it('a node-level grant is live before its expiresAt', async () => {
94
+ expect(await probe(nora, PERM_READ)).toBe(true);
95
+ });
96
+ it('an entity-narrowed grant is live before its expiresAt', async () => {
97
+ expect(await probe(eve, PERM_READ, box)).toBe(true);
98
+ });
99
+ /**
100
+ * The transition the wall clock cannot show. Nothing is revoked and nothing is
101
+ * swept: the only thing that changes is what time the host thinks it is.
102
+ */
103
+ it('the node-level grant stops granting once the clock passes expiresAt', async () => {
104
+ clock.set(at(HOUR + 60_000));
105
+ expect(await probe(nora, PERM_READ)).toBe(false);
106
+ });
107
+ it('the entity-narrowed grant stops granting once the clock passes expiresAt', async () => {
108
+ clock.set(at(HOUR + 60_000));
109
+ expect(await probe(eve, PERM_READ, box)).toBe(false);
110
+ });
111
+ /**
112
+ * The boundary is part of the contract, not an adapter detail: the predicate is
113
+ * `expires_at > now`, so at the expiry instant itself the grant is already gone.
114
+ * A `>=` would pass every other test in this suite and silently widen every
115
+ * timed grant on that adapter by one instant — so the suite pins both sides of
116
+ * the line, for both shapes of grant.
117
+ */
118
+ it('is gone at the expiry instant itself, not one past it', async () => {
119
+ clock.set(at(HOUR - 1));
120
+ expect(await probe(nora, PERM_READ)).toBe(true);
121
+ expect(await probe(eve, PERM_READ, box)).toBe(true);
122
+ clock.set(at(HOUR));
123
+ expect(await probe(nora, PERM_READ)).toBe(false);
124
+ expect(await probe(eve, PERM_READ, box)).toBe(false);
125
+ });
126
+ /**
127
+ * Expiry is judged on every check, not cached at grant time — so the same
128
+ * grant reads as live again if the clock is moved back before it. This is
129
+ * what distinguishes "the checker consults the clock" from "the grant was
130
+ * tombstoned when the clock passed it". The case sets up its own expiry first
131
+ * rather than inheriting the clock an earlier case left behind, so it proves
132
+ * the rollback when run alone too.
133
+ */
134
+ it('is judged at check time — the same grant is live again with the clock before expiresAt', async () => {
135
+ clock.set(at(HOUR + 60_000));
136
+ expect(await probe(nora, PERM_READ)).toBe(false);
137
+ expect(await probe(eve, PERM_READ, box)).toBe(false);
138
+ clock.set(at(HOUR - 60_000));
139
+ expect(await probe(nora, PERM_READ)).toBe(true);
140
+ expect(await probe(eve, PERM_READ, box)).toBe(true);
141
+ clock.set(at(HOUR + 60_000));
142
+ expect(await probe(nora, PERM_READ)).toBe(false);
143
+ expect(await probe(eve, PERM_READ, box)).toBe(false);
144
+ });
145
+ /**
146
+ * Expiry is not sticky: a later re-grant of the same (principal, permission,
147
+ * node) carries its own `expiresAt`, and the checker judges that one. How the
148
+ * adapter stores it is its own business — the SQLite host keys tuples on
149
+ * (subject, relation, object) and the re-grant REPLACES the expired row, so
150
+ * there is never a dead row beside a live one there. What the contract holds is
151
+ * only the outcome: the expired grant does not shadow the new one, and the new
152
+ * one dies on its own clock.
153
+ */
154
+ it('a re-grant after expiry grants again, on its own expiresAt', async () => {
155
+ clock.set(at(HOUR + 60_000));
156
+ expect(await probe(nora, PERM_READ)).toBe(false);
157
+ await host.admin.grant(staff, {
158
+ principalId: nora,
159
+ permission: PERM_READ,
160
+ node: { tenantId: t1, scopeId: s1 },
161
+ expiresAt: at(3 * HOUR),
162
+ grantedBy: alice,
163
+ });
164
+ expect(await probe(nora, PERM_READ)).toBe(true);
165
+ clock.set(at(3 * HOUR + 60_000));
166
+ expect(await probe(nora, PERM_READ)).toBe(false);
167
+ });
168
+ /** An untimed grant is unaffected by any clock — the control for the whole suite. */
169
+ it('a grant with no expiresAt is not touched by the clock', async () => {
170
+ await host.admin.grant(staff, {
171
+ principalId: nora,
172
+ permission: PERM_USE,
173
+ node: { tenantId: t1, scopeId: s1 },
174
+ grantedBy: alice,
175
+ });
176
+ clock.set(at(365 * 24 * HOUR));
177
+ expect(await probe(nora, PERM_USE)).toBe(true);
178
+ });
179
+ });
180
+ }
181
+ //# sourceMappingURL=grant-expiry-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grant-expiry-suite.js","sourceRoot":"","sources":["../src/grant-expiry-suite.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export { listContractSuite } from './list-suite.js';
12
12
  export { scheduleContractSuite } from './schedule-suite.js';
13
13
  export { inputParseContractSuite } from './input-parse-suite.js';
14
14
  export { spineGuardContractSuite } from './spine-guard-suite.js';
15
+ export { grantExpiryContractSuite } from './grant-expiry-suite.js';
16
+ export type { GrantExpiryFixture } from './grant-expiry-suite.js';
15
17
  export { entityCheckConformanceSuite, planEntityCheckCoverage } from './entity-check-suite.js';
16
18
  export type { EntityCheckFixture, EntityCheckSuiteOptions, PlannedCheck } from './entity-check-suite.js';
17
19
  export { nodeOnlySuite, declaredNodeOnlySuite } from './node-only-suite.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACzG,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC5E,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACxF,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,OAAO,EACP,WAAW,EACX,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACnE,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACzG,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC5E,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACxF,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,OAAO,EACP,WAAW,EACX,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { listContractSuite } from './list-suite.js';
11
11
  export { scheduleContractSuite } from './schedule-suite.js';
12
12
  export { inputParseContractSuite } from './input-parse-suite.js';
13
13
  export { spineGuardContractSuite } from './spine-guard-suite.js';
14
+ export { grantExpiryContractSuite } from './grant-expiry-suite.js';
14
15
  export { entityCheckConformanceSuite, planEntityCheckCoverage } from './entity-check-suite.js';
15
16
  export { nodeOnlySuite, declaredNodeOnlySuite } from './node-only-suite.js';
16
17
  export { declareEntityChecks, declareNodeOnly, assertNodeOnly } from './conformance.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAE/F,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOxF,OAAO,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,OAAO,EACP,WAAW,EACX,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,GAEpB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAEnE,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAE/F,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOxF,OAAO,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,OAAO,EACP,WAAW,EACX,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,GAEpB,MAAM,wBAAwB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAuBA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAW9B,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,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,CAk2JN"}
1
+ {"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AAuBA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAW9B,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,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,CA22JN"}
@@ -1694,25 +1694,31 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
1694
1694
  expect(new Set(exemplars.map((r) => r.fingerprint)).size).toBe(1);
1695
1695
  });
1696
1696
  it('walks the lifecycle: resolved regresses on a fresh arrival, ignored stays ignored', async () => {
1697
- const record = () => host.admin.recordOpsFailure({
1697
+ const record = (version) => host.admin.recordOpsFailure({
1698
1698
  actor: staff,
1699
1699
  operation: 'deploy.upload',
1700
1700
  stage: 'lifecycle-check',
1701
1701
  origin: 'platform',
1702
1702
  code: 'unavailable',
1703
+ version: version ?? null,
1703
1704
  message: 'upstream unavailable',
1704
1705
  });
1705
- await record();
1706
+ await record('01JVERSIONAAAAAAAAAAAAAAA1');
1706
1707
  const [issue] = await host.admin.listIssues(staff, { operation: 'deploy.upload', code: 'unavailable' });
1707
1708
  const resolved = await host.admin.setIssueStatus(staff, issue.fingerprint, 'resolved');
1708
1709
  expect(resolved?.status).toBe('resolved');
1709
1710
  expect(resolved?.resolvedAt).not.toBeNull();
1711
+ // The verdict freezes what it resolved (#1236): the newest version at that moment.
1712
+ expect(resolved?.resolvedVersion).toBe('01JVERSIONAAAAAAAAAAAAAAA1');
1710
1713
  // Sentry's best retention trick, minus the release half: seen again after
1711
1714
  // resolved = regressed, back into attention without anyone re-filing it.
1712
- await record();
1715
+ await record('01JVERSIONAAAAAAAAAAAAAAA2');
1713
1716
  const [regressed] = await host.admin.listIssues(staff, { operation: 'deploy.upload', code: 'unavailable' });
1714
1717
  expect(regressed.status).toBe('regressed');
1715
1718
  expect(regressed.count).toBe(2);
1719
+ // The regression names its pair: resolved under A1, seen again under A2.
1720
+ expect(regressed.resolvedVersion).toBe('01JVERSIONAAAAAAAAAAAAAAA1');
1721
+ expect(regressed.lastVersion).toBe('01JVERSIONAAAAAAAAAAAAAAA2');
1716
1722
  // The resolution timestamp survives the regression: "when somebody last
1717
1723
  // thought this was over" is part of the regression's story.
1718
1724
  expect(regressed.resolvedAt).not.toBeNull();
@@ -1722,10 +1728,13 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
1722
1728
  const [ignored] = await host.admin.listIssues(staff, { operation: 'deploy.upload', code: 'unavailable' });
1723
1729
  expect(ignored.status).toBe('ignored');
1724
1730
  expect(ignored.count).toBe(3);
1731
+ // The unstamped third arrival did not erase what the stamped one said.
1732
+ expect(ignored.lastVersion).toBe('01JVERSIONAAAAAAAAAAAAAAA2');
1725
1733
  // Reopening clears the resolution; an unknown fingerprint is undefined, never an invented row.
1726
1734
  const reopened = await host.admin.setIssueStatus(staff, issue.fingerprint, 'new');
1727
1735
  expect(reopened?.status).toBe('new');
1728
1736
  expect(reopened?.resolvedAt).toBeNull();
1737
+ expect(reopened?.resolvedVersion).toBeNull();
1729
1738
  expect(await host.admin.setIssueStatus(staff, 'no-such-fingerprint', 'resolved')).toBeUndefined();
1730
1739
  });
1731
1740
  });