Skip to content
Merged
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
74 changes: 67 additions & 7 deletions packages/spec/src/conversions/retired-after.census.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ import { ALL_CONVERSIONS } from './registry.js';
* version from the census's last release up to the label — it landed either
* before the bump or after it, and only the new tarball can say which.
* Refreshing the census after the publish closes the tolerance again.
*
* The label is read on its RELEASE TRIPLE. Changesets pre mode writes a
* prerelease label (`18.0.0-next.0`) into this package on the Version Packages
* PR, and onto `main` once that lands, so every comparison against the label
* uses `x.y.z` with the prerelease suffix dropped: `18.0.0-next.N` reads as
* `18.0.0`. That is the reading the artifact door gives the runtime label (it
* compares `x.y.z` triples and tolerates a suffix), and on a stable label it is
* the label itself, so no assertion changes meaning there. Under a prerelease
* label the census never records that label's tarball (it holds stable
* releases only), so the release-pending tolerance above stays open, spanning
* `[last censused release, release triple]`, until the stable release is
* published and censused, when the PUBLISHED rule re-judges every stamp. Which
* stamp a retirement landing under a prerelease label should carry is the
* artifact door's window question, and this file follows that rule rather
* than settling it. Only the label gets this reading: a census version and a
* `retiredAfter` stamp stay stable `x.y.z`, and a label that is neither
* `x.y.z` nor `x.y.z-PRERELEASE` still throws.
*/

type Triple = [number, number, number];
Expand Down Expand Up @@ -69,6 +86,22 @@ function cmp(a: string, b: string): number {
return 0;
}

/**
* The release triple of a package label: `x.y.z` as is, `x.y.z-PRERELEASE`
* with the suffix dropped (`18.0.0-next.0` reads as `18.0.0`). Anything else
* throws. See the file docblock for why the label alone is read this way.
*/
function releaseTriple(label: string): string {
const m = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(label);
if (!m) throw new Error(`not an x.y.z or x.y.z-prerelease package label: '${label}'`);
return m[1]!;
}

/** The label as every comparison below reads it. */
const RELEASE = releaseTriple(LABEL);
/** The label as a failure message names it. */
const LABEL_SHOWN = LABEL === RELEASE ? `'${LABEL}'` : `'${LABEL}' (release triple '${RELEASE}')`;

const LAST_CENSUSED = CENSUS.releases.at(-1)!.version;

/**
Expand All @@ -91,7 +124,7 @@ describe('retiredAfter census — every retired entry stamped from the published
for (let i = 1; i < versions.length; i++) {
expect(cmp(versions[i - 1]!, versions[i]!), `${versions[i - 1]} < ${versions[i]}`).toBe(-1);
}
expect(cmp(LAST_CENSUSED, LABEL), `census (${LAST_CENSUSED}) is ahead of the label (${LABEL})`).toBeLessThanOrEqual(0);
expect(cmp(LAST_CENSUSED, RELEASE), `census (${LAST_CENSUSED}) is ahead of the label ${LABEL_SHOWN}`).toBeLessThanOrEqual(0);
for (const r of CENSUS.releases) {
expect(r.integrity).toMatch(/^sha512-/);
expect([...r.retired].sort()).toEqual(r.retired);
Expand All @@ -102,7 +135,7 @@ describe('retiredAfter census — every retired entry stamped from the published
expect(RETIRED.length).toBeGreaterThan(0);
for (const c of RETIRED) {
expect(c.retiredAfter, c.id).toMatch(/^\d+\.\d+\.\d+$/);
expect(cmp(c.retiredAfter!, LABEL), `${c.id}: retiredAfter ${c.retiredAfter} > label ${LABEL}`).toBeLessThanOrEqual(0);
expect(cmp(c.retiredAfter!, RELEASE), `${c.id}: retiredAfter ${c.retiredAfter} > label ${LABEL_SHOWN}`).toBeLessThanOrEqual(0);
}
});

Expand All @@ -126,17 +159,44 @@ describe('retiredAfter census — every retired entry stamped from the published
});

it('every UNPUBLISHED entry carries the package label (the release-pending range while the census trails it)', () => {
const pending = cmp(LABEL, LAST_CENSUSED) > 0;
const pending = cmp(RELEASE, LAST_CENSUSED) > 0;
const wrong: string[] = [];
for (const c of RETIRED) {
if (publishedRetiredAfter(c.id) !== null) continue;
const v = c.retiredAfter!;
if (!pending && v !== LABEL) {
wrong.push(`${c.id}: retiredAfter '${v}', but no published tarball retires it, so it takes the label '${LABEL}'`);
} else if (pending && (cmp(v, LAST_CENSUSED) < 0 || cmp(v, LABEL) > 0)) {
wrong.push(`${c.id}: retiredAfter '${v}' is outside [${LAST_CENSUSED}, ${LABEL}] — refresh the census (${REGEN})`);
if (!pending && v !== RELEASE) {
wrong.push(`${c.id}: retiredAfter '${v}', but no published tarball retires it, so it takes the label ${LABEL_SHOWN}`);
} else if (pending && (cmp(v, LAST_CENSUSED) < 0 || cmp(v, RELEASE) > 0)) {
wrong.push(`${c.id}: retiredAfter '${v}' is outside [${LAST_CENSUSED}, ${RELEASE}] — refresh the census (${REGEN})`);
}
}
expect(wrong).toEqual([]);
});
});

describe('the package label is read on its release triple', () => {
it('a stable label is its own release triple', () => {
expect(releaseTriple('17.7.0')).toBe('17.7.0');
expect(releaseTriple('18.0.0')).toBe('18.0.0');
});

it('a prerelease label reads as its release triple', () => {
expect(releaseTriple('18.0.0-next.0')).toBe('18.0.0');
expect(releaseTriple('18.0.0-next.12')).toBe('18.0.0');
expect(releaseTriple('18.1.0-rc.1')).toBe('18.1.0');
// ...and so compares at, never above, the release it precedes.
expect(cmp('17.7.0', releaseTriple('18.0.0-next.0'))).toBe(-1);
expect(cmp('18.0.0', releaseTriple('18.0.0-next.0'))).toBe(0);
});

it('a malformed label still throws, naming the label', () => {
for (const bad of ['', 'next', '18', '18.0', 'v18.0.0', '18.0.0-', '18.0.0-next..0', '18.0.0.0', ' 18.0.0']) {
expect(() => releaseTriple(bad), JSON.stringify(bad)).toThrow(`'${bad}'`);
}
});

it('the release-triple reading is the label\'s alone: a census version or a stamp stays stable x.y.z', () => {
expect(() => triple('18.0.0-next.0')).toThrow(`'18.0.0-next.0'`);
expect(() => cmp('18.0.0-next.0', '17.7.0')).toThrow(`'18.0.0-next.0'`);
});
});
Loading