@substrat-run/contract-tests 0.82.0 → 0.84.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.
@@ -144,6 +144,29 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
144
144
  expect(new Date(row.occurred_at).getTime()).not.toBeNaN();
145
145
  expect(row.pii_class).toBe('none');
146
146
  });
147
+ it('ctx.now() is one instant for the whole operation, and the envelope agrees (#812)', async () => {
148
+ const stub = await host.getScope(alice, t1, s1);
149
+ const { first, second } = await stub.invoke('test/now');
150
+ // A valid instant, and the SAME one on both reads. Two rows written in one
151
+ // transaction cannot disagree about when they were written — that is the
152
+ // promise a frozen clock rests on, not an implementation detail.
153
+ expect(instant.safeParse(first).success).toBe(true);
154
+ expect(second).toBe(first);
155
+ // And the event announcing the work carries the same instant as the work.
156
+ const rows = await stub.invoke('test/read-outbox');
157
+ const row = rows[rows.length - 1];
158
+ expect(row.occurred_at).toBe(first);
159
+ });
160
+ it('ctx.now() advances between operations — stable is not frozen (#812)', async () => {
161
+ const stub = await host.getScope(alice, t1, s1);
162
+ const a = await stub.invoke('test/now');
163
+ await new Promise((r) => setTimeout(r, 5));
164
+ const b = await stub.invoke('test/now');
165
+ // Held within an operation, re-read for the next one. A host that stamped
166
+ // once per process would pass every assertion above and be useless.
167
+ expect(Date.parse(b.first)).toBeGreaterThanOrEqual(Date.parse(a.first));
168
+ expect(b.first).not.toBe(a.first);
169
+ });
147
170
  it('rejects PII-classed events without a subjectId (§6.1)', async () => {
148
171
  const stub = await host.getScope(alice, t1, s1);
149
172
  await expect(stub.invoke('test/emit-unclassified-pii')).rejects.toThrow(/subjectId/);
@@ -246,6 +269,44 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
246
269
  expect((await host.listPlatformRequestHistory(t1, s1, { status: 'failed' })).every((r) => r.status === 'failed')).toBe(true);
247
270
  expect((await host.listPlatformRequestHistory(t1, s1, { limit: 1 })).length).toBe(1);
248
271
  });
272
+ /**
273
+ * #841. `lastError` says WHAT happened; this says WHO refused — and until it existed the
274
+ * dashboard assumed the provider every time, so a `permission denied: protocol:read`
275
+ * raised by our own check, before a byte left the platform, was rendered as the provider's
276
+ * own words. Pinned in the shared suite because both adapters store it in their own spine
277
+ * table, and a read-back that disagreed between them would be worse than no read at all.
278
+ */
279
+ it('journals WHO refused beside what was refused, and survives the round trip (#841)', async () => {
280
+ const stub = await host.getScope(alice, t1, s1);
281
+ const id = await stub.invoke('platform/request', { kind: 'connector:test', payload: { doc: 9 } });
282
+ const row = (await host.listPlatformRequests(t1, s1)).find((r) => r.id === id);
283
+ await host.settlePlatformRequest(t1, s1, row.id, {
284
+ status: 'failed',
285
+ lastError: 'permission denied: protocol:read',
286
+ failure: { origin: 'platform', code: 'permission_denied', permission: 'protocol:read' },
287
+ });
288
+ const settled = (await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' })).find((r) => r.id === id);
289
+ expect(settled.failure).toEqual({
290
+ origin: 'platform',
291
+ code: 'permission_denied',
292
+ permission: 'protocol:read',
293
+ });
294
+ });
295
+ /**
296
+ * A caller that does not attribute leaves the column NULL, and NULL must read back as
297
+ * "nobody classified this" rather than as an origin nobody decided. This is the shape a
298
+ * control plane older than the field produces, and the drawer renders it by saying
299
+ * nothing about origin at all.
300
+ */
301
+ it('leaves attribution null when the settling caller did not attribute (#841)', async () => {
302
+ const stub = await host.getScope(alice, t1, s1);
303
+ const id = await stub.invoke('platform/request', { kind: 'connector:test', payload: { doc: 10 } });
304
+ const row = (await host.listPlatformRequests(t1, s1)).find((r) => r.id === id);
305
+ await host.settlePlatformRequest(t1, s1, row.id, { status: 'failed', lastError: 'something broke' });
306
+ const settled = (await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' })).find((r) => r.id === id);
307
+ expect(settled.lastError).toBe('something broke');
308
+ expect(settled.failure).toBeNull();
309
+ });
249
310
  it('newest first, so a limit is a recency window rather than an arbitrary page (#618)', async () => {
250
311
  const stub = await host.getScope(alice, t1, s1);
251
312
  const first = await stub.invoke('platform/request', { kind: 'order-check', payload: { n: 1 } });
@@ -1583,6 +1644,93 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
1583
1644
  expect(await host.admin.listConnectionGrants(staff, gt)).toEqual([]);
1584
1645
  });
1585
1646
  });
1647
+ // -- the read-back: what the SCOPE says a connection may do (#726 gap 1) ----
1648
+ //
1649
+ // The grants above are the DIRECTORY's record. This is the other half, and the
1650
+ // one a deployment can reach: the delivered tuples, read from where they are
1651
+ // enforced. #716 found `protocol:attach` missing from a live connection after
1652
+ // months of silent failure precisely because nothing outside the control plane
1653
+ // could answer this question.
1654
+ //
1655
+ // The invariant that matters is agreement with the checker: whatever this
1656
+ // returns must be exactly what `check` would allow for that connection in that
1657
+ // scope. A read-back that disagreed would be worse than none, because it is the
1658
+ // read an operator would believe. That agreement is asserted against real
1659
+ // evaluation in `permission-suite` (which has the probe operation); what is
1660
+ // pinned here is the shape — liveness, inheritance, and per-connection narrowing.
1661
+ describe('connection grants, read back from the scope (#726)', () => {
1662
+ const rt = tenantId.parse(ulid());
1663
+ const rs = scopeId.parse(ulid());
1664
+ const rconn = connectionId.parse(ulid());
1665
+ const SCOPED = permissionKey.parse('protocol:record-signature');
1666
+ const INHERITED = permissionKey.parse('protocol:attach');
1667
+ const EXPIRED = permissionKey.parse('protocol:read');
1668
+ beforeAll(async () => {
1669
+ await host.admin.createTenant(staff, { id: rt, slug: 'tenant-readback', name: 'Readback' });
1670
+ await host.provisionScope(staff, { tenantId: rt, scopeId: rs, vertical: 'signing' });
1671
+ await host.admin.activateScope(staff, rt, rs);
1672
+ await host.admin.createConnection(staff, {
1673
+ id: rconn,
1674
+ tenantId: rt,
1675
+ vertical: 'signing',
1676
+ provider: 'signer',
1677
+ label: 'Signer',
1678
+ secret: { accessToken: 'tok-readback' },
1679
+ });
1680
+ // Scope-targeted, tenant-wide, and one already past its expiry.
1681
+ await host.admin.grantToConnection(staff, {
1682
+ connectionId: rconn,
1683
+ permission: SCOPED,
1684
+ node: { tenantId: rt, scopeId: rs },
1685
+ grantedBy: staff,
1686
+ });
1687
+ await host.admin.grantToConnection(staff, {
1688
+ connectionId: rconn,
1689
+ permission: INHERITED,
1690
+ node: { tenantId: rt, scopeId: null },
1691
+ grantedBy: staff,
1692
+ });
1693
+ await host.admin.grantToConnection(staff, {
1694
+ connectionId: rconn,
1695
+ permission: EXPIRED,
1696
+ node: { tenantId: rt, scopeId: rs },
1697
+ expiresAt: instant.parse('2020-01-01T00:00:00.000Z'),
1698
+ grantedBy: staff,
1699
+ });
1700
+ });
1701
+ it('answers with the live grants, tenant-wide ones included', async () => {
1702
+ const grants = await host.connectionGrantsInScope(rt, rs);
1703
+ expect(grants.map((g) => g.permission).sort()).toEqual([INHERITED, SCOPED].sort());
1704
+ expect(grants.every((g) => g.connectionId === rconn)).toBe(true);
1705
+ });
1706
+ it('omits an expired grant — it would not be enforced either', async () => {
1707
+ const grants = await host.connectionGrantsInScope(rt, rs);
1708
+ expect(grants.map((g) => g.permission)).not.toContain(EXPIRED);
1709
+ });
1710
+ it('is scoped to the connection — another connection in the tenant is not reported', async () => {
1711
+ const other = connectionId.parse(ulid());
1712
+ await host.admin.createConnection(staff, {
1713
+ id: other,
1714
+ tenantId: rt,
1715
+ vertical: 'signing',
1716
+ provider: 'other-signer',
1717
+ label: 'Other',
1718
+ secret: { accessToken: 'tok-other' },
1719
+ });
1720
+ await host.admin.grantToConnection(staff, {
1721
+ connectionId: other,
1722
+ permission: EXPIRED,
1723
+ node: { tenantId: rt, scopeId: rs },
1724
+ grantedBy: staff,
1725
+ });
1726
+ const byConnection = new Map();
1727
+ for (const g of await host.connectionGrantsInScope(rt, rs)) {
1728
+ byConnection.set(g.connectionId, [...(byConnection.get(g.connectionId) ?? []), g.permission]);
1729
+ }
1730
+ expect(byConnection.get(other)).toEqual([EXPIRED]);
1731
+ expect(byConnection.get(rconn)?.sort()).toEqual([INHERITED, SCOPED].sort());
1732
+ });
1733
+ });
1586
1734
  // -- connector state: a connector's own durable bookkeeping (#101 gap 3) ---
1587
1735
  describe('connector state', () => {
1588
1736
  it('round-trips arbitrary JSON keyed by (connection, key)', async () => {