From dcb00916177b5ae66b88b01d96b9d8838a14e3ec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:32:41 +0000 Subject: [PATCH 1/2] test(create-objectstack): derive the runtime-image fixture's resolved CLI version from the scaffolder's own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "with dependencies installed" fixture planted `.4.2` as the npm-resolved @objectstack/cli. The scaffolder writes `^`, so that literal sits inside the range only while create-objectstack's minor is at most 4: the 17.5.0 version pass put it below the floor and the agreement test failed with "scaffolded Dockerfile pins :17.4.2 but the scaffolded package.json asks for ^17.5.0" — a fixture defect reported as a scaffolder one. The resolved version is now same major, next minor, patch 2 — always inside `^` and always above its floor, which is what the fixture's comment says it is for. A new precondition test asserts exactly that, so a future fixture drift names the fixture instead of the scaffolder. Claude-Session: https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL Co-authored-by: Claude --- .../src/runtime-image.test.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/create-objectstack/src/runtime-image.test.ts b/packages/create-objectstack/src/runtime-image.test.ts index 7522579f348..776a350c439 100644 --- a/packages/create-objectstack/src/runtime-image.test.ts +++ b/packages/create-objectstack/src/runtime-image.test.ts @@ -95,7 +95,14 @@ describe('scaffolded Dockerfile runtime image tag (#9017)', () => { // The version npm resolved is deliberately NOT the range's floor: that is // the normal case (`^17.0.0` installs the newest 17.x) and it is the case // that would go unnoticed if the tag were taken from package.json instead. - const resolved = `${String(ownVersion).split('.')[0]}.4.2`; + // + // It is DERIVED from the scaffolder's own version — same major, next minor + // — never spelled as a literal. The scaffolder writes `^`, so a + // literal `.4.2` lies inside that range only while ownVersion's minor + // is at most 4; the version pass to 17.5.0 put it BELOW the floor, and the + // agreement test went red on a fixture defect while blaming the scaffolder. + const [ownMajor, ownMinor] = String(ownVersion).split('.').map(Number); + const resolved = `${ownMajor}.${ownMinor + 1}.2`; beforeEach(() => { scaffold(dir); @@ -104,6 +111,19 @@ describe('scaffolded Dockerfile runtime image tag (#9017)', () => { expect(result.pinned, 'pinRuntimeImage refused the scaffolded Dockerfile').toBe(true); }); + // A precondition on the fixture, not on the scaffolder: should the planted + // version ever leave the range (or land on its floor), this names the + // fixture as the fault instead of the agreement test below reporting a + // runtime-image mismatch the scaffolder never produced. + it('plants a resolved version inside the scaffolded range and above its floor', () => { + expect(resolved).not.toBe(ownVersion); + expect( + satisfiesCaret(`^${ownVersion}`, resolved), + `fixture defect: the planted @objectstack/cli ${resolved} does not ` + + `satisfy ^${ownVersion}, the range the scaffolder writes for its own version`, + ).toBe(true); + }); + it('pins the FROM tag to the @objectstack/cli the project resolved', () => { expect(runtimeTag(dir)).toBe(resolved); }); From 76b0acbe9433a5b16db03b77d4f6459b82bab086 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 03:02:49 +0000 Subject: [PATCH 2/2] test(metadata): pin the artifact-door notice clause per window half, not as the pre-release literal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "logs one conversion summary per retired entry it replayed" asserted that no conversion notice says "predates this runtime's spec". That holds only while packages/spec still carries the 17.4.0 label: the 17.4.0-floored fixture then sits in the per-entry half of the window. The version pass moves the label to 17.5.0, the floor falls below it, the label half opens the window (verdict converted-forward), and the door correctly prints "predates this runtime's spec (authored engines.protocol floor 17.4.0, runtime spec 17.5.0)" — so the test went red on the versioned tree while the runtime behaved exactly as designed. The assertion now reads the running spec's label the way the door does (resolveInstalledSpecVersion) and pins the clause of whichever half that label selects: the per-entry "was built on a surface that still accepted shapes this runtime has since retired" clause below the floor, the "predates" clause with the exact floor and runtime version above it. Claude-Session: https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL Co-authored-by: Claude --- ...t-forward-conversion-retired-after.test.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts b/packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts index be1ca4f222b..7dec75cc875 100644 --- a/packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts +++ b/packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts @@ -30,6 +30,7 @@ import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineStack } from '@objectstack/spec'; +import { resolveInstalledSpecVersion } from '@objectstack/metadata-core'; import { MetadataPlugin } from './plugin.js'; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -58,6 +59,14 @@ function newPlugin(): any { return new MetadataPlugin({ watch: false, config: { bootstrap: 'lazy' } }); } +/** Order two versions by their leading `x.y.z` (a prerelease tail is ignored). */ +function compareVersions(a: string, b: string): number { + const triple = (v: string) => v.split(/[.-]/).slice(0, 3).map((n) => Number.parseInt(n, 10)); + const [x, y] = [triple(a), triple(b)]; + for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i]! < y[i]! ? -1 : 1; + return 0; +} + /** The conversion summary lines the door logged, keyed by conversion id. */ function conversionWarns(ctx: any): Map { const byId = new Map(); @@ -111,10 +120,31 @@ describe('[#20390] artifact door — a 17.4.0-built artifact boots on unreleased expect(warns.get('dashboard-widget-chart-config-structure-removed')![0]).toContain('3 site(s)'); expect(warns.get('page-assigned-profiles-removed')).toHaveLength(1); expect(warns.get('page-assigned-profiles-removed')![0]).toContain('1 site(s)'); - // Under the per-entry half the floor is not below the runtime's label, - // so the line must not claim the artifact "predates" a runtime printed - // at the same version — it names the retirement that opened it instead. - for (const line of [...warns.values()].flat()) expect(line).not.toContain("predates this runtime's spec"); + // WHICH half of the window opened is decided by the running spec's + // label, and the version pass moves that label — so the clause is + // pinned per half, never as one literal. Under the per-entry half + // (unreleased `main`, label still 17.4.0) the floor is not below the + // label, so the line must not claim the artifact "predates" a runtime + // printed at the same version — it names the retirement that opened it + // instead. Once the label is past the floor (the 17.5.0 version pass + // onward) the label half opened it, and "predates" is exactly the + // claim the line owes, beside a runtime version that bears it out. + const floor = String(loadFixture().manifest.engines.protocol).replace(/^\^/, ''); + const installed = resolveInstalledSpecVersion(); + expect(installed).toMatch(/^\d+\.\d+\.\d+/); // spec is always resolvable here + const labelPastFloor = compareVersions(installed!, floor) > 0; + const lines = [...warns.values()].flat(); + expect(lines).toHaveLength(2); + for (const line of lines) { + if (labelPastFloor) { + expect(line).toContain( + `predates this runtime's spec (authored engines.protocol floor ${floor}, runtime spec ${installed})`, + ); + } else { + expect(line).not.toContain("predates this runtime's spec"); + expect(line).toContain('was built on a surface that still accepted shapes this runtime has since retired'); + } + } }); /**