// scripts/e2e-stamp.mjs — the start-mode build stamp. Every case runs over a // throwaway git checkout, so nothing here reads or builds the real tree. import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { after, test } from "node:test"; import { STAMP_VERSION, computeFingerprint, decide, ensurePackage, readStamp, resolveE2EMode, writeStamp, } from "./e2e-stamp.mjs"; const SPEC = { dir: "app", distDir: ".next/e2e", sources: ["app", "common", "pnpm-lock.yaml"], exclude: [":(exclude)app/e2e", ":(exclude,glob)**/*.test.ts"], }; const made = []; after(() => { for (const dir of made) fs.rmSync(dir, { recursive: true, force: true }); }); function repo() { const root = fs.mkdtempSync(path.join(os.tmpdir(), "e2e-stamp-test-")); made.push(root); const git = (...args) => execFileSync("git", args, { cwd: root, stdio: "pipe" }); git("init", "-q"); git("config", "user.email", "test@example.invalid"); git("config", "user.name", "test"); git("config", "commit.gpgsign", "false"); const write = (rel, text) => { fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), text); }; write(".gitignore", "**/.next/\n"); write("app/page.tsx", "export default 1;\n"); write("app/lib/a.ts", "export const a = 1;\n"); write("app/e2e/x.spec.ts", "// a spec\n"); write("app/lib/a.test.ts", "// a unit test\n"); write("common/lib/b.ts", "export const b = 2;\n"); write("pnpm-lock.yaml", "lockfileVersion: '9.0'\n"); write("plans/notes.md", "a plan\n"); git("add", "-A"); git("commit", "-q", "-m", "one"); return { root, git, write }; } const fp = (root) => computeFingerprint({ root, spec: SPEC }); test("the same tree gives the same stamp, and the stamp is fresh", () => { const { root } = repo(); const a = fp(root); const b = fp(root); assert.equal(a.key, b.key); assert.equal(a.version, STAMP_VERSION); assert.deepEqual(a.dirty, []); assert.deepEqual(decide({ recorded: a, current: b }), { fresh: true }); }); test("a dirty edit under common/ changes the stamp and names where", () => { const { root, write } = repo(); const built = fp(root); write("common/lib/b.ts", "export const b = 3;\n"); const now = fp(root); assert.notEqual(now.key, built.key); assert.deepEqual(now.dirty, ["common/lib/b.ts"]); const d = decide({ recorded: built, current: now }); assert.equal(d.fresh, false); assert.match(d.reason, /^common changed since the build \(1 uncommitted file there, e\.g\. common\/lib\/b\.ts\)$/); }); test("committing the edit keeps the new stamp (content, not HEAD)", () => { const { root, git, write } = repo(); write("app/lib/a.ts", "export const a = 9;\n"); const dirty = fp(root); git("commit", "-qam", "two"); const committed = fp(root); assert.equal(committed.key, dirty.key); assert.deepEqual(committed.dirty, []); }); test("an untracked file and a deletion count; a staged edit counts", () => { const { root, git, write } = repo(); const built = fp(root); write("app/lib/new.ts", "export {};\n"); assert.notEqual(fp(root).key, built.key); fs.rmSync(path.join(root, "app/lib/new.ts")); assert.equal(fp(root).key, built.key); fs.rmSync(path.join(root, "app/lib/a.ts")); const gone = fp(root); assert.notEqual(gone.key, built.key); assert.deepEqual(gone.dirty, ["app/lib/a.ts"]); git("checkout", "--", "app/lib/a.ts"); write("app/page.tsx", "export default 2;\n"); git("add", "app/page.tsx"); assert.notEqual(fp(root).key, built.key); }); test("what no build reads moves nothing: specs, unit tests, plans, ignored output", () => { const { root, git, write } = repo(); const built = fp(root); write("app/e2e/x.spec.ts", "// edited\n"); write("app/lib/a.test.ts", "// edited\n"); write("plans/notes.md", "edited\n"); write("app/.next/e2e/BUILD_ID", "abc\n"); assert.equal(fp(root).key, built.key); git("commit", "-qam", "specs only"); assert.equal(fp(root).key, built.key); }); test("a missing, garbled or foreign-version stamp means rebuild", () => { const { root } = repo(); const current = fp(root); const file = path.join(root, "stamp.json"); assert.equal(readStamp(file), null); assert.deepEqual(decide({ recorded: readStamp(file), current }), { fresh: false, reason: "no build stamp here yet", }); fs.writeFileSync(file, "{not json"); assert.deepEqual(decide({ recorded: readStamp(file), current }), { fresh: false, reason: "the build stamp is unreadable", }); fs.writeFileSync(file, JSON.stringify({ hello: 1 })); assert.equal(decide({ recorded: readStamp(file), current }).fresh, false); writeStamp(file, { ...current, version: STAMP_VERSION + 1 }); assert.match(decide({ recorded: readStamp(file), current }).reason, /^the build stamp is version/); }); test("a stamp written for this tree reads back fresh; a build dir without BUILD_ID does not", () => { const { root } = repo(); const current = fp(root); const file = path.join(root, "stamp.json"); writeStamp(file, current, { buildSeconds: 1 }); const recorded = readStamp(file); assert.equal(recorded.buildSeconds, 1); assert.deepEqual(decide({ recorded, current }), { fresh: true }); assert.deepEqual(decide({ recorded, current, buildIdPresent: false }), { fresh: false, reason: "the stamped build directory has no BUILD_ID", }); }); test("E2E_MODE: start by default, dev on request, anything else refused", () => { assert.equal(resolveE2EMode({}), "start"); assert.equal(resolveE2EMode({ E2E_MODE: "" }), "start"); assert.equal(resolveE2EMode({ E2E_MODE: "start" }), "start"); assert.equal(resolveE2EMode({ E2E_MODE: "DEV" }), "dev"); assert.throws(() => resolveE2EMode({ E2E_MODE: "prod" }), /not a mode/); }); test("ensure under E2E_MODE=dev builds nothing and says so", () => { const lines = []; assert.equal(ensurePackage("editor", { env: { E2E_MODE: "dev" }, log: (l) => lines.push(l) }), true); assert.deepEqual(lines, ["e2e build: E2E_MODE=dev — editor runs under next dev, no build"]); });