@substrat-run/contract-tests 0.119.0 → 0.120.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 +1 @@
1
- {"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AA4BA,OAAO,EAOL,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;AA6DD,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,CAu0NN"}
1
+ {"version":3,"file":"scope-host-suite.d.ts","sourceRoot":"","sources":["../src/scope-host-suite.ts"],"names":[],"mappings":"AA4BA,OAAO,EAOL,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;AA6DD,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,CAw6NN"}
@@ -4424,6 +4424,54 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
4424
4424
  await host.admin.promoteVersion(staff, 'tenant-a/crm', 'prod', v3);
4425
4425
  expect((await host.admin.getScopeRecord(staff, t2, appScope))?.verticalVersionId).toBe(v3);
4426
4426
  });
4427
+ it('embedded prod promotion leaves previews and real forks intact beside advancing installs (#1724)', async () => {
4428
+ const slug = 'tenant-a/preview-prevention';
4429
+ await host.admin.registerVertical(staff, { slug, name: 'Preview prevention', source: 'cli', ownerTenant: t2 });
4430
+ const first = await publishPrivate(slug, '1.0.0');
4431
+ const next = await publishPrivate(slug, '1.0.1');
4432
+ const make = async (kind, tenant = t2, active = true) => {
4433
+ const id = scopeId.parse(ulid());
4434
+ await host.provisionScope(staff, { tenantId: tenant, scopeId: id, vertical: slug, kind });
4435
+ if (active)
4436
+ await host.admin.activateScope(staff, tenant, id);
4437
+ await host.admin.bindScopeVersion(staff, tenant, id, first);
4438
+ return id;
4439
+ };
4440
+ const install = await make('production');
4441
+ const clean = await make('preview');
4442
+ const pinned = await make('preview');
4443
+ await host.admin.setScopeServingRef(staff, t2, pinned, 'historical-serving-script');
4444
+ const suspended = await make('production');
4445
+ await host.admin.suspendScope(staff, t2, suspended);
4446
+ const provisioning = await make('production', t2, false);
4447
+ const foreign = await make('production', t1);
4448
+ await (await host.getScope(alice, t2, clean)).invoke('test/write-marker', { v: 'preview-data' });
4449
+ // An actual snapshot copies data and records forkedFrom, rather than naming a fork in the title.
4450
+ const fork = await host.snapshotScope(staff, t2, clean);
4451
+ expect((await host.admin.getScopeRecord(staff, t2, fork))?.forkedFrom).toBe(clean);
4452
+ const frozen = [clean, pinned, fork, suspended, provisioning];
4453
+ const before = await Promise.all(frozen.map((id) => host.admin.getScopeRecord(staff, t2, id)));
4454
+ const dataBefore = await host.admin.readScopeTable(staff, t2, clean, { table: 'marker', limit: 200, offset: 0 });
4455
+ const hostname = `preview-${clean.toLowerCase()}.example.test`;
4456
+ await host.admin.bindHostname(staff, { hostname, tenantId: t2, scopeId: clean, surface: 'app', region: null, canonical: false });
4457
+ await host.admin.setHostnameStatus(staff, hostname, 'active');
4458
+ const routeBefore = await host.admin.resolveHostname(hostname);
4459
+ expect(routeBefore).toMatchObject({ scopeId: clean, deploymentRef: null });
4460
+ for (let retry = 0; retry < 2; retry++) {
4461
+ await host.admin.promoteVersion(staff, slug, 'prod', next);
4462
+ expect((await host.admin.getScopeRecord(staff, t2, install))?.verticalVersionId).toBe(next);
4463
+ expect(await Promise.all(frozen.map((id) => host.admin.getScopeRecord(staff, t2, id)))).toEqual(before);
4464
+ expect((await host.admin.getScopeRecord(staff, t1, foreign))?.verticalVersionId).toBe(first);
4465
+ expect(await host.admin.resolveHostname(hostname)).toEqual(routeBefore);
4466
+ expect(await host.admin.readScopeTable(staff, t2, clean, { table: 'marker', limit: 200, offset: 0 })).toEqual(dataBefore);
4467
+ expect(await host.admin.readScopeTable(staff, t2, fork, { table: 'marker', limit: 200, offset: 0 })).toEqual(dataBefore);
4468
+ }
4469
+ // Listing retains the existing opt-in update behavior for ordinary installs too.
4470
+ await host.admin.admitVersion(staff, next);
4471
+ await host.admin.setVerticalListed(staff, slug, true);
4472
+ await host.admin.promoteVersion(staff, slug, 'prod', first);
4473
+ expect((await host.admin.getScopeRecord(staff, t2, install))?.verticalVersionId).toBe(next);
4474
+ });
4427
4475
  it('publish refuses an auto-admitted prod version until a staff admit vouches for it', async () => {
4428
4476
  // Listing is the moment OTHER tenants start trusting this code, so the version
4429
4477
  // they would install needs a recorded human decision — the auto-admission note
@@ -4491,6 +4539,53 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
4491
4539
  // -- the hostname map (K-26) ---------------------------------------------
4492
4540
  // §4.2 provisions a scope; this is what finally gives it a URL. The router
4493
4541
  // resolves against these rows before dispatching to a vertical's worker.
4542
+ it('gates hostname routing on scope and tenant lifecycle without changing bindings (#1713)', async () => {
4543
+ const tenant = tenantId.parse(ulid());
4544
+ const otherTenant = tenantId.parse(ulid());
4545
+ const target = scopeId.parse(ulid());
4546
+ const sibling = scopeId.parse(ulid());
4547
+ const other = scopeId.parse(ulid());
4548
+ for (const id of [tenant, otherTenant]) {
4549
+ await host.admin.createTenant(staff, { id, slug: `route-${id.toLowerCase()}`, name: 'Routing' });
4550
+ }
4551
+ for (const [t, sc] of [[tenant, target], [tenant, sibling], [otherTenant, other]]) {
4552
+ await host.provisionScope(staff, { tenantId: t, scopeId: sc });
4553
+ await host.admin.bindHostname(staff, {
4554
+ hostname: `${sc.toLowerCase()}.example.com`, tenantId: t, scopeId: sc,
4555
+ surface: 'app', region: null, canonical: true,
4556
+ });
4557
+ await host.admin.setHostnameStatus(staff, `${sc.toLowerCase()}.example.com`, 'active');
4558
+ }
4559
+ const resolve = (sc) => host.admin.resolveHostname(`${sc.toLowerCase()}.example.com`);
4560
+ const binding = () => host.admin.listHostnames(staff, { scopeId: target });
4561
+ const original = await binding();
4562
+ expect(original[0]?.status).toBe('active');
4563
+ expect(await resolve(target)).toBeUndefined(); // provisioning, despite active binding
4564
+ for (const [t, sc] of [[tenant, target], [tenant, sibling], [otherTenant, other]]) {
4565
+ await host.admin.activateScope(staff, t, sc);
4566
+ }
4567
+ const active = await resolve(target);
4568
+ expect(active).toMatchObject({ tenantId: tenant, scopeId: target, deploymentRef: null });
4569
+ await host.admin.suspendScope(staff, tenant, target);
4570
+ expect(await resolve(target)).toBeUndefined();
4571
+ expect(await resolve(sibling)).toBeDefined();
4572
+ expect(await resolve(other)).toBeDefined();
4573
+ expect(await binding()).toEqual(original);
4574
+ await host.admin.unsuspendScope(staff, tenant, target);
4575
+ expect(await resolve(target)).toEqual(active);
4576
+ for (const status of ['suspended', 'deleting']) {
4577
+ await host.admin.setTenantStatus(staff, tenant, status);
4578
+ expect(await resolve(target)).toBeUndefined();
4579
+ expect(await resolve(sibling)).toBeUndefined();
4580
+ expect(await resolve(other)).toBeDefined();
4581
+ expect(await binding()).toEqual(original);
4582
+ await host.admin.setTenantStatus(staff, tenant, 'active');
4583
+ expect(await resolve(target)).toEqual(active);
4584
+ }
4585
+ await host.admin.archiveScope(staff, tenant, target);
4586
+ expect(await resolve(target)).toBeUndefined();
4587
+ expect(await binding()).toEqual(original);
4588
+ });
4494
4589
  it('binds a hostname as pending, and resolves nothing until it is active', async () => {
4495
4590
  await host.admin.bindHostname(staff, {
4496
4591
  hostname: 'acme.example.com',
@@ -4516,7 +4611,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
4516
4611
  // A hostname routes to exactly one place, so a rebind that would move another
4517
4612
  // scope's traffic is refused — unless the holder is archived or reaped, which is
4518
4613
  // how a deleted app's name becomes claimable again. Both adapters read the
4519
- // holder's scope status to decide, and neither reads it on the router's path.
4614
+ // holder's scope status to decide; routing independently requires active lifecycle.
4520
4615
  const holder = scopeId.parse(ulid());
4521
4616
  const claimant = scopeId.parse(ulid());
4522
4617
  for (const sc of [holder, claimant]) {
@@ -4547,6 +4642,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
4547
4642
  // the row, then the read-back 400d at the Zod boundary and the app got no URL.
4548
4643
  const sc = scopeId.parse(ulid());
4549
4644
  await host.provisionScope(staff, { tenantId: t1, scopeId: sc, vertical: 't-acme/crm', jurisdiction: 'eu' });
4645
+ await host.admin.activateScope(staff, t1, sc);
4550
4646
  await host.admin.bindHostname(staff, {
4551
4647
  hostname: 'crm-acme.global.example.com',
4552
4648
  tenantId: t1,