Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion packages/create-objectstack/src/runtime-image.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `^<ownVersion>`, so a
// literal `<major>.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);
Expand All @@ -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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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<string, string[]> {
const byId = new Map<string, string[]>();
Expand Down Expand Up @@ -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');
}
}
});

/**
Expand Down
Loading