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
27 changes: 24 additions & 3 deletions scripts/generate-design-system-adoption.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -418,15 +418,33 @@ function validateCandidateSourceBinding(candidateSourceHead, { root, policy }) {

const candidateAwaiting = candidateSuite === null ? null : visualBaselineAwaitingIds(candidateSuite, visualSuiteFile);
const currentAwaiting = currentSuite === null ? null : visualBaselineAwaitingIds(currentSuite, visualSuiteFile);
// Two legitimate shapes, and the second is what makes updating the design possible.
//
// FIRST ADOPTION — the capture head still declares the canonical six as awaiting,
// and the adopting commit empties that list.
// REFRESH — the list is already empty at both ends. Deliberately re-shooting a
// surface produces exactly this, and it is the ORDINARY case once baselines
// exist: the pixels move, the goldens are replaced, and the suite itself does
// not change at all.
//
// Requiring the six-to-empty transition alone made this binding satisfiable
// exactly once. After the first adoption no commit declares the six again, so no
// capture head could ever qualify and every later refresh was unprovable — the
// goldens would go red on the first intentional design change with no supported
// way to re-adopt them.
const candidateAwaitingCanonical =
candidateAwaiting?.valid && JSON.stringify(candidateAwaiting.ids) === JSON.stringify(canonicalIds);
const candidateAwaitingEmpty = candidateAwaiting?.valid && candidateAwaiting.ids.length === 0;
const candidateAwaitingAdoptable = candidateAwaitingCanonical || candidateAwaitingEmpty;
const currentAwaitingEmpty = currentAwaiting?.valid && currentAwaiting.ids.length === 0;
if (!candidateAwaiting?.valid) {
failures.push(
`candidateSourceHead AWAITING_BASELINE must be a static literal Set: ${candidateAwaiting?.failure ?? "missing suite"}`,
);
} else if (!candidateAwaitingCanonical) {
failures.push("candidateSourceHead AWAITING_BASELINE must contain exactly the canonical six ids");
} else if (!candidateAwaitingAdoptable) {
failures.push(
"candidateSourceHead AWAITING_BASELINE must be either the canonical six ids (first adoption) or empty (refresh)",
);
}
if (!currentAwaiting?.valid) {
failures.push(
Expand All @@ -444,8 +462,11 @@ function validateCandidateSourceBinding(candidateSourceHead, { root, policy }) {
currentSuite === null || currentAwaiting === null
? null
: normalizeVisualSuiteAwaitingValues(currentSuite, currentAwaiting);
// Normalisation blanks the AWAITING_BASELINE values on both sides, so this stays
// exact for a refresh (where the suite is byte-identical) as well as for the
// first adoption (where only those values moved).
const visualSuiteOnlyChangedAwaiting =
candidateAwaitingCanonical &&
candidateAwaitingAdoptable &&
currentAwaitingEmpty &&
normalizedCandidateSuite !== null &&
normalizedCandidateSuite === normalizedCurrentSuite;
Expand Down
34 changes: 30 additions & 4 deletions tests/design-system-adoption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,15 +831,25 @@ describe("design-system adoption manifest", () => {
}
});

it("requires the exact static AWAITING_BASELINE transition from canonical six to empty", { timeout: 90_000 }, () => {
it("accepts a first adoption or a refresh, and nothing else", { timeout: 90_000 }, () => {
const exactRoot = fs.mkdtempSync(path.join(os.tmpdir(), "design-system-awaiting-exact-"));
const refreshRoot = fs.mkdtempSync(path.join(os.tmpdir(), "design-system-awaiting-refresh-"));
const retainedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "design-system-awaiting-retained-"));
const missingRoot = fs.mkdtempSync(path.join(os.tmpdir(), "design-system-awaiting-missing-"));
const extraRoot = fs.mkdtempSync(path.join(os.tmpdir(), "design-system-awaiting-extra-"));
const dynamicRoot = fs.mkdtempSync(path.join(os.tmpdir(), "design-system-awaiting-dynamic-"));
const spreadRoot = fs.mkdtempSync(path.join(os.tmpdir(), "design-system-awaiting-spread-"));
const duplicateRoot = fs.mkdtempSync(path.join(os.tmpdir(), "design-system-awaiting-duplicate-"));
const fixtureRoots = [exactRoot, retainedRoot, missingRoot, extraRoot, dynamicRoot, spreadRoot, duplicateRoot];
const fixtureRoots = [
exactRoot,
refreshRoot,
retainedRoot,
missingRoot,
extraRoot,
dynamicRoot,
spreadRoot,
duplicateRoot,
];
try {
initialiseCandidateRepository(exactRoot, canonicalAwaitingValues);
setCurrentAwaitingValues(exactRoot, "");
Expand All @@ -848,6 +858,18 @@ describe("design-system adoption manifest", () => {
validateLinuxVisualBaselineSet(exact.paths, { root: exactRoot, trackedFiles: exact.trackedFiles }),
).toEqual([]);

// A REFRESH binds too: empty at both ends, suite byte-identical. This is the
// ordinary case once baselines exist — a surface is deliberately re-shot and
// its goldens replaced. Without it the six-to-empty transition was satisfiable
// exactly once, so the first intentional design change would have left the
// goldens red with no supported way to re-adopt them.
initialiseCandidateRepository(refreshRoot, "");
setCurrentAwaitingValues(refreshRoot, "");
const refresh = writeBaselineSet(refreshRoot);
expect(
validateLinuxVisualBaselineSet(refresh.paths, { root: refreshRoot, trackedFiles: refresh.trackedFiles }),
).toEqual([]);

initialiseCandidateRepository(retainedRoot, canonicalAwaitingValues);
setCurrentAwaitingValues(retainedRoot, JSON.stringify("dashboard-shell"));
const retained = writeBaselineSet(retainedRoot);
Expand All @@ -872,14 +894,18 @@ describe("design-system adoption manifest", () => {
root: missingRoot,
trackedFiles: missing.trackedFiles,
}),
).toContain("candidateSourceHead AWAITING_BASELINE must contain exactly the canonical six ids");
).toContain(
"candidateSourceHead AWAITING_BASELINE must be either the canonical six ids (first adoption) or empty (refresh)",
);

initialiseCandidateRepository(extraRoot, `${canonicalAwaitingValues}, "extra-target"`);
setCurrentAwaitingValues(extraRoot, "");
const extra = writeBaselineSet(extraRoot);
expect(
validateLinuxVisualBaselineSet(extra.paths, { root: extraRoot, trackedFiles: extra.trackedFiles }),
).toContain("candidateSourceHead AWAITING_BASELINE must contain exactly the canonical six ids");
).toContain(
"candidateSourceHead AWAITING_BASELINE must be either the canonical six ids (first adoption) or empty (refresh)",
);

initialiseCandidateRepository(dynamicRoot, "BASELINE_IDS");
setCurrentAwaitingValues(dynamicRoot, "");
Expand Down
Loading