Skip to content

Commit 1c761c0

Browse files
hotlongclaude
andauthored
test(create-objectstack,metadata): make two version-sensitive tests hold across the 17.5.0 version pass (#20565)
Fixes #20560 Unblocks the 17.5.0 Version Packages PR #17076 (`changeset-release/main`). Its required `Test Core (1/6)` is red on head `de35e567` (CI run 36510180528): ``` AssertionError: scaffolded Dockerfile pins :17.4.2 but the scaffolded package.json asks for "^17.5.0" — the generated app would run a runtime image that is not the CLI building its artifact (#9017): expected false to be true ``` Both changes are test-only. No runtime code moves, and neither file ships: both packages publish only `dist`, `README.md` and `CHANGELOG.md`, and neither file is a tsup entry. So there is no changeset and the PR carries `skip-changeset`. ## 1. `create-objectstack`: the runtime-image fixture (the reported failure) `runtime-image.test.ts` planted the npm-resolved `@objectstack/cli` as the literal `${major}.4.2`. The scaffolder writes a caret range on its own version, so that literal is inside the range only while the minor is 4 or lower. At 17.5.0 it falls below the floor, and the agreement test blamed the scaffolder for what was a fixture defect. - `resolved` is now derived from the package's own version: same major, own minor + 1, patch 2 (17.4.0 gives 17.5.2, 17.5.0 gives 17.6.2). It is always inside the caret range and always above its floor, which is what the fixture's own comment says it is for. - A new precondition test, `plants a resolved version inside the scaffolded range and above its floor`, makes future fixture drift point at the fixture rather than at the scaffolder. I checked the other create-objectstack tests for a current-minor assumption. They key on the major only (`template-consistency`), use fixed literals unrelated to the package version (the `readResolvedCliVersion` cases, `banner-version`), or carry a fixture version of their own (`template-version-stamps`). The whole suite passes on the versioned tree (table below). ## 2. `metadata`: artifact-door notice wording (found by running the unreached shard-1 packages on the versioned tree) Turbo stopped shard 1 at the failure above, so ten scheduled packages never ran against 17.5.0. Running them found one more failure of the same class: ``` FAIL src/plugin-artifact-forward-conversion-retired-after.test.ts > [#20390] artifact door — a 17.4.0-built artifact boots on unreleased main > logs one conversion summary per retired entry it replayed, naming the site count AssertionError: expected '[MetadataPlugin] artifact \'forward-p…' not to contain 'predates this runtime\'s spec' ``` The fixture is a 17.4.0-built artifact (`engines.protocol: ^17.4.0`). While `packages/spec` carries the 17.4.0 label, the floor is not below the label. The per-entry half of the window opens (`converted-retired-after`), and its clause deliberately avoids "predates". The version pass moves the label to 17.5.0 and the floor falls below it. The label half opens instead (`converted-forward`), and the door correctly prints `predates this runtime's spec (authored engines.protocol floor 17.4.0, runtime spec 17.5.0)`. The runtime behaves as designed; the assertion had the pre-release label built in. The assertion now reads the running spec label the same way the door does, with `resolveInstalledSpecVersion()`. `plugin-artifact-forward-conversion.test.ts` already uses that pattern. The test then pins the exact clause of whichever half that label selects. Both halves were exercised: the per-entry clause on `main` (label 17.4.0), and the `predates` clause with the exact floor and runtime version on the versioned tree (label 17.5.0). ## Verification on the versioned tree I made a throwaway worktree at `origin/changeset-release/main` = `de35e567`, the #17076 head that the failing run tested, and copied both fixes in. Then: 1. `pnpm install --frozen-lockfile`. 2. `pnpm turbo run build` over each package's dependency closure (`--filter='PKG^...'`; 61 tasks, 0 cached). 3. Each package's own script via `pnpm --filter PKG run SCRIPT`, with the env Test Core sets (`OS_TEST_TIERS=queue`, `CI=true`). | Package | Script | Result on de35e56 + this PR | |---|---|---| | create-objectstack | test | pass: 16 files / 234 tests. Without the fix: 1 failed, the assertion quoted above | | @objectstack/spec | test | pass: 573 / 16846, 1 todo | | @objectstack/spec | test:repo | pass: 40 / 716 | | @objectstack/verify | test | pass: 15 / 116 | | @objectstack/metadata | test | pass: 55 / 826. Without the fix: 1 failed, the assertion quoted in section 2 | | @objectstack/plugin-audit | test | pass: 25 / 363 | | @objectstack/example-todo | test | pass: 7 / 238 | | @objectstack/trigger-record-change | test | pass: 10 / 101 | | @objectstack/formula | test | pass: 42 / 1227 | | @objectstack/downstream-contract | test | pass: 3 / 31 | | @objectstack/service-cluster-redis | test | pass: 3 / 37 | | @objectstack/knowledge-ragflow | test | pass: 1 / 10 | Of shard 1's other scheduled packages, `@objectstack/types` had already passed in that run, and shards 2–6 were green. So every package that run scheduled has now passed against the `de35e567` tree. ## Checks on this branch (base `b0574343`) - `pnpm --filter create-objectstack test`: 16 files / 234 tests pass; `typecheck` clean. - `pnpm --filter @objectstack/metadata test`: 55 / 826 pass (the per-entry half); `typecheck` clean. - `eslint` on both changed files: clean. `main` has since moved to `f6ceddc3`, six commits on. None of them touches `packages/create-objectstack`, `packages/metadata` or `packages/metadata-core`. A wider gap is tracked separately in #20562 and not addressed here: a version-sensitive test is only exercised after `pnpm run version`. --- _Generated by [Claude Code](https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dee9b26 commit 1c761c0

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

‎packages/create-objectstack/src/runtime-image.test.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,14 @@ describe('scaffolded Dockerfile runtime image tag (#9017)', () => {
9595
// The version npm resolved is deliberately NOT the range's floor: that is
9696
// the normal case (`^17.0.0` installs the newest 17.x) and it is the case
9797
// that would go unnoticed if the tag were taken from package.json instead.
98-
const resolved = `${String(ownVersion).split('.')[0]}.4.2`;
98+
//
99+
// It is DERIVED from the scaffolder's own version — same major, next minor
100+
// — never spelled as a literal. The scaffolder writes `^<ownVersion>`, so a
101+
// literal `<major>.4.2` lies inside that range only while ownVersion's minor
102+
// is at most 4; the version pass to 17.5.0 put it BELOW the floor, and the
103+
// agreement test went red on a fixture defect while blaming the scaffolder.
104+
const [ownMajor, ownMinor] = String(ownVersion).split('.').map(Number);
105+
const resolved = `${ownMajor}.${ownMinor + 1}.2`;
99106

100107
beforeEach(() => {
101108
scaffold(dir);
@@ -104,6 +111,19 @@ describe('scaffolded Dockerfile runtime image tag (#9017)', () => {
104111
expect(result.pinned, 'pinRuntimeImage refused the scaffolded Dockerfile').toBe(true);
105112
});
106113

114+
// A precondition on the fixture, not on the scaffolder: should the planted
115+
// version ever leave the range (or land on its floor), this names the
116+
// fixture as the fault instead of the agreement test below reporting a
117+
// runtime-image mismatch the scaffolder never produced.
118+
it('plants a resolved version inside the scaffolded range and above its floor', () => {
119+
expect(resolved).not.toBe(ownVersion);
120+
expect(
121+
satisfiesCaret(`^${ownVersion}`, resolved),
122+
`fixture defect: the planted @objectstack/cli ${resolved} does not ` +
123+
`satisfy ^${ownVersion}, the range the scaffolder writes for its own version`,
124+
).toBe(true);
125+
});
126+
107127
it('pins the FROM tag to the @objectstack/cli the project resolved', () => {
108128
expect(runtimeTag(dir)).toBe(resolved);
109129
});

‎packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts‎

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { readFileSync } from 'node:fs';
3030
import { dirname, join } from 'node:path';
3131
import { fileURLToPath } from 'node:url';
3232
import { defineStack } from '@objectstack/spec';
33+
import { resolveInstalledSpecVersion } from '@objectstack/metadata-core';
3334
import { MetadataPlugin } from './plugin.js';
3435

3536
const HERE = dirname(fileURLToPath(import.meta.url));
@@ -58,6 +59,14 @@ function newPlugin(): any {
5859
return new MetadataPlugin({ watch: false, config: { bootstrap: 'lazy' } });
5960
}
6061

62+
/** Order two versions by their leading `x.y.z` (a prerelease tail is ignored). */
63+
function compareVersions(a: string, b: string): number {
64+
const triple = (v: string) => v.split(/[.-]/).slice(0, 3).map((n) => Number.parseInt(n, 10));
65+
const [x, y] = [triple(a), triple(b)];
66+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i]! < y[i]! ? -1 : 1;
67+
return 0;
68+
}
69+
6170
/** The conversion summary lines the door logged, keyed by conversion id. */
6271
function conversionWarns(ctx: any): Map<string, string[]> {
6372
const byId = new Map<string, string[]>();
@@ -111,10 +120,31 @@ describe('[#20390] artifact door — a 17.4.0-built artifact boots on unreleased
111120
expect(warns.get('dashboard-widget-chart-config-structure-removed')![0]).toContain('3 site(s)');
112121
expect(warns.get('page-assigned-profiles-removed')).toHaveLength(1);
113122
expect(warns.get('page-assigned-profiles-removed')![0]).toContain('1 site(s)');
114-
// Under the per-entry half the floor is not below the runtime's label,
115-
// so the line must not claim the artifact "predates" a runtime printed
116-
// at the same version — it names the retirement that opened it instead.
117-
for (const line of [...warns.values()].flat()) expect(line).not.toContain("predates this runtime's spec");
123+
// WHICH half of the window opened is decided by the running spec's
124+
// label, and the version pass moves that label — so the clause is
125+
// pinned per half, never as one literal. Under the per-entry half
126+
// (unreleased `main`, label still 17.4.0) the floor is not below the
127+
// label, so the line must not claim the artifact "predates" a runtime
128+
// printed at the same version — it names the retirement that opened it
129+
// instead. Once the label is past the floor (the 17.5.0 version pass
130+
// onward) the label half opened it, and "predates" is exactly the
131+
// claim the line owes, beside a runtime version that bears it out.
132+
const floor = String(loadFixture().manifest.engines.protocol).replace(/^\^/, '');
133+
const installed = resolveInstalledSpecVersion();
134+
expect(installed).toMatch(/^\d+\.\d+\.\d+/); // spec is always resolvable here
135+
const labelPastFloor = compareVersions(installed!, floor) > 0;
136+
const lines = [...warns.values()].flat();
137+
expect(lines).toHaveLength(2);
138+
for (const line of lines) {
139+
if (labelPastFloor) {
140+
expect(line).toContain(
141+
`predates this runtime's spec (authored engines.protocol floor ${floor}, runtime spec ${installed})`,
142+
);
143+
} else {
144+
expect(line).not.toContain("predates this runtime's spec");
145+
expect(line).toContain('was built on a surface that still accepted shapes this runtime has since retired');
146+
}
147+
}
118148
});
119149

120150
/**

0 commit comments

Comments
 (0)