231 lines
13 KiB
TypeScript
231 lines
13 KiB
TypeScript
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 test from "node:test";
|
|
import { BuildInputLoader } from "./buildInputLoader";
|
|
import { BuildEngine, BUILDER_VERSION, renderMarkdown } from "./buildEngine";
|
|
|
|
const git = (root: string, args: string[]): string => execFileSync("git", ["-C", root, ...args], {
|
|
encoding: "utf8",
|
|
env: { ...process.env, GIT_AUTHOR_DATE: "2026-01-01T00:00:00Z", GIT_COMMITTER_DATE: "2026-01-01T00:00:00Z" },
|
|
}).trim();
|
|
|
|
const createRepository = (): string => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "labyricorn-git-fixture-"));
|
|
fs.mkdirSync(path.join(root, "packages"), { recursive: true });
|
|
fs.cpSync(path.resolve("packages/site-definition"), path.join(root, "packages/site-definition"), { recursive: true });
|
|
fs.cpSync(path.resolve("packages/content-models"), path.join(root, "packages/content-models"), { recursive: true });
|
|
git(root, ["init", "-b", "main"]);
|
|
git(root, ["config", "user.name", "Fixture"]);
|
|
git(root, ["config", "user.email", "[email protected]"]);
|
|
git(root, ["add", "."]);
|
|
git(root, ["commit", "-m", "fixture"]);
|
|
return root;
|
|
};
|
|
|
|
const commit = (root: string, message: string): string => {
|
|
git(root, ["add", "-A"]);
|
|
git(root, ["commit", "-m", message]);
|
|
return git(root, ["rev-parse", "HEAD"]);
|
|
};
|
|
|
|
const load = (root: string, runId: string) => {
|
|
const manager = new BuildInputLoader({ repositoryRoot: root, ref: "HEAD", siteDefinitionPath: "packages/site-definition", builderVersion: BUILDER_VERSION });
|
|
return { manager, input: manager.load(runId) };
|
|
};
|
|
|
|
const withRepository = (callback: (root: string, output: string) => void): void => {
|
|
const root = createRepository();
|
|
const output = fs.mkdtempSync(path.join(os.tmpdir(), "labyricorn-output-"));
|
|
try { callback(root, output); }
|
|
finally { fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(output, { recursive: true, force: true }); }
|
|
};
|
|
|
|
test("reference repository theme renders declared routes and only declared assets", () => withRepository((root, output) => {
|
|
const { manager, input } = load(root, "build-000001");
|
|
try {
|
|
const result = new BuildEngine(output).build(input);
|
|
assert.equal(result.success, true);
|
|
assert.match(input.siteDefinition.commit, /^[0-9a-f]{40}$/);
|
|
assert.equal(input.contentItems.length, 3);
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "index.html")));
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "articles/index.html")));
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "articles/repository-rendering/index.html")));
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "projects/index.html")));
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "project/website-engine-control-plane/index.html")));
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "project/website-engine-control-plane/demo.html")));
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "404.html")));
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "assets/styles/theme.css")));
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "assets/scripts/theme.js")));
|
|
assert.ok(fs.existsSync(path.join(result.outputDirectory, "assets/images/logo.svg")));
|
|
assert.equal(fs.existsSync(path.join(result.outputDirectory, ".theme/theme.yml")), false);
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(result.outputDirectory, "build-manifest.json"), "utf8"));
|
|
assert.equal(manifest.siteDefinition.commit, input.siteDefinition.commit);
|
|
assert.equal(manifest.theme.commit, input.theme.snapshot.commit);
|
|
assert.equal(manifest.artifactBuildId, input.artifactBuildId);
|
|
assert.match(fs.readFileSync(path.join(result.outputDirectory, "index.html"), "utf8"), /renders this homepage from committed Markdown/);
|
|
assert.match(fs.readFileSync(path.join(result.outputDirectory, "articles/repository-rendering/index.html"), "utf8"), /Every build resolves one exact Git commit/);
|
|
assert.match(fs.readFileSync(path.join(result.outputDirectory, "project/website-engine-control-plane/index.html"), "utf8"), /joins its allowlisted declaration/);
|
|
assert.doesNotMatch(fs.readFileSync(path.join(result.outputDirectory, "index.html"), "utf8"), /fonts\.googleapis|tailwindcss\.com/);
|
|
} finally { manager.dispose(); }
|
|
}));
|
|
|
|
test("equivalent resolved inputs are byte-identical despite different run IDs", () => withRepository((root, output) => {
|
|
const first = load(root, "build-000001");
|
|
const second = load(root, "build-999999");
|
|
try {
|
|
assert.equal(first.input.artifactBuildId, second.input.artifactBuildId);
|
|
const engine = new BuildEngine(output);
|
|
const one = engine.build(first.input);
|
|
if (process.platform !== "win32") {
|
|
assert.equal(fs.statSync(one.outputDirectory).mode & 0o777, 0o750);
|
|
}
|
|
fs.chmodSync(one.outputDirectory, 0o700);
|
|
const two = engine.build(second.input);
|
|
if (process.platform !== "win32") {
|
|
assert.equal(fs.statSync(two.outputDirectory).mode & 0o777, 0o750);
|
|
}
|
|
assert.equal(one.artifactChecksum, two.artifactChecksum);
|
|
assert.equal(one.outputDirectory, two.outputDirectory);
|
|
assert.deepEqual(one.generatedFiles, two.generatedFiles);
|
|
assert.equal(fs.readFileSync(path.join(one.outputDirectory, "checksums.json"), "utf8"), fs.readFileSync(path.join(two.outputDirectory, "checksums.json"), "utf8"));
|
|
} finally { first.manager.dispose(); second.manager.dispose(); }
|
|
}));
|
|
|
|
test("an already resolved snapshot is unchanged when the branch advances", () => withRepository((root, output) => {
|
|
const pinned = load(root, "build-pinned");
|
|
const css = path.join(root, "packages/site-definition/.theme/assets/styles/theme.css");
|
|
fs.appendFileSync(css, "\n.branch-advanced{display:block}\n");
|
|
const advancedCommit = commit(root, "advance theme css");
|
|
const advanced = load(root, "build-advanced");
|
|
try {
|
|
assert.notEqual(pinned.input.siteDefinition.commit, advancedCommit);
|
|
assert.equal(advanced.input.siteDefinition.commit, advancedCommit);
|
|
const engine = new BuildEngine(output);
|
|
const pinnedResult = engine.build(pinned.input);
|
|
const advancedResult = engine.build(advanced.input);
|
|
assert.notEqual(pinned.input.artifactBuildId, advanced.input.artifactBuildId);
|
|
assert.notEqual(pinnedResult.artifactChecksum, advancedResult.artifactChecksum);
|
|
assert.doesNotMatch(fs.readFileSync(path.join(pinnedResult.outputDirectory, "assets/styles/theme.css"), "utf8"), /branch-advanced/);
|
|
} finally { pinned.manager.dispose(); advanced.manager.dispose(); }
|
|
}));
|
|
|
|
test("a missing theme fails before any release directory exists", () => withRepository((root, output) => {
|
|
fs.rmSync(path.join(root, "packages/site-definition/.theme"), { recursive: true, force: true });
|
|
commit(root, "remove theme");
|
|
const manager = new BuildInputLoader({ repositoryRoot: root, builderVersion: BUILDER_VERSION });
|
|
try {
|
|
assert.throws(() => manager.load("missing-theme"), /E_THEME_ROOT_INVALID/);
|
|
assert.equal(fs.existsSync(path.join(output, "releases")), false);
|
|
} finally { manager.dispose(); }
|
|
}));
|
|
|
|
test("unknown manifest keys and escaping paths fail closed", () => withRepository((root) => {
|
|
const manifest = path.join(root, "packages/site-definition/.theme/theme.yml");
|
|
fs.appendFileSync(manifest, "unknownKey: rejected\n");
|
|
commit(root, "invalid manifest key");
|
|
let manager = new BuildInputLoader({ repositoryRoot: root, builderVersion: BUILDER_VERSION });
|
|
assert.throws(() => manager.load("invalid-key"), /E_THEME_MANIFEST_INVALID/);
|
|
manager.dispose();
|
|
|
|
git(root, ["reset", "--hard", "HEAD~1"]);
|
|
fs.appendFileSync(manifest, "\n");
|
|
const source = fs.readFileSync(manifest, "utf8").replace("templates/layout.liquid", "../outside.liquid");
|
|
fs.writeFileSync(manifest, source);
|
|
commit(root, "escaping template path");
|
|
manager = new BuildInputLoader({ repositoryRoot: root, builderVersion: BUILDER_VERSION });
|
|
assert.throws(() => manager.load("escape"), /E_THEME_MANIFEST_INVALID|E_THEME_PATH_ESCAPE/);
|
|
manager.dispose();
|
|
}));
|
|
|
|
test("strict Liquid rejects unknown variables without publishing a release", () => withRepository((root, output) => {
|
|
const template = path.join(root, "packages/site-definition/.theme/templates/home.liquid");
|
|
fs.appendFileSync(template, "\n{{ process.env.SECRET }}\n");
|
|
commit(root, "unsafe variable");
|
|
const { manager, input } = load(root, "strict-variable");
|
|
try {
|
|
const engine = new BuildEngine(output);
|
|
assert.throws(() => engine.build(input), /undefined variable|not defined|process/i);
|
|
assert.equal(fs.existsSync(engine.buildDirectory(input.artifactBuildId)), false);
|
|
} finally { manager.dispose(); }
|
|
}));
|
|
|
|
test("project declaration controls the project route and standalone publication", () => withRepository((root, output) => {
|
|
fs.writeFileSync(path.join(root, "packages/site-definition/projects.yml"), "projects: []\n");
|
|
commit(root, "remove project declaration");
|
|
const { manager, input } = load(root, "no-project");
|
|
try {
|
|
const result = new BuildEngine(output).build(input);
|
|
assert.equal(result.success, true);
|
|
assert.equal(input.projects.length, 0);
|
|
assert.equal(fs.existsSync(path.join(result.outputDirectory, "project/website-engine-control-plane/index.html")), false);
|
|
assert.equal(fs.existsSync(path.join(result.outputDirectory, "project/website-engine-control-plane/demo.html")), false);
|
|
} finally { manager.dispose(); }
|
|
}));
|
|
|
|
|
|
test("project declarations cannot silently shadow non-project content", () => withRepository((root, output) => {
|
|
fs.rmSync(path.join(root, "packages/site-definition/projects/control-plane.md"));
|
|
fs.writeFileSync(path.join(root, "packages/site-definition/projects/.gitkeep"), "");
|
|
const article = path.join(root, "packages/site-definition/articles/welcome.md");
|
|
fs.writeFileSync(article, fs.readFileSync(article, "utf8").replace("route: /articles/repository-rendering/", "route: /project/website-engine-control-plane/"));
|
|
commit(root, "create route collision");
|
|
const { manager, input } = load(root, "route-collision");
|
|
try {
|
|
const result = new BuildEngine(output).build(input);
|
|
assert.equal(result.success, false);
|
|
assert.ok(result.validationReport.errors.some((error) => error.code === "E_ROUTE_COLLISION"));
|
|
} finally { manager.dispose(); }
|
|
}));
|
|
|
|
test("declared standalone files are copied byte-for-byte", () => withRepository((root, output) => {
|
|
const { manager, input } = load(root, "publication");
|
|
try {
|
|
const result = new BuildEngine(output).build(input);
|
|
const source = fs.readFileSync(path.join(root, "packages/site-definition/demo-project/demo.html"));
|
|
const published = fs.readFileSync(path.join(result.outputDirectory, "project/website-engine-control-plane/demo.html"));
|
|
assert.deepEqual(published, source);
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(result.outputDirectory, "build-manifest.json"), "utf8"));
|
|
assert.equal(manifest.publishedFiles[0].sourceChecksum, `sha256-${input.publishedFiles[0].sourceChecksum}`);
|
|
} finally { manager.dispose(); }
|
|
}));
|
|
|
|
test("promotion verifies every artifact byte and rejects tampering", () => withRepository((root, output) => {
|
|
const { manager, input } = load(root, "tamper");
|
|
try {
|
|
const engine = new BuildEngine(output);
|
|
const result = engine.build(input);
|
|
fs.appendFileSync(path.join(result.outputDirectory, "index.html"), "tampered");
|
|
assert.throws(() => engine.activate(input.artifactBuildId), /E_RELEASE_CHECKSUM/);
|
|
assert.equal(fs.existsSync(engine.currentDirectory()), false);
|
|
} finally { manager.dispose(); }
|
|
}));
|
|
|
|
test("markdown output is escaped and heading IDs are deterministic", () => {
|
|
const first = renderMarkdown("# Repeat\n\n## Repeat\n\nText <img src=x> and [safe](/docs/).\n\n```html\n<script>x</script>\n```");
|
|
const second = renderMarkdown("# Repeat\n\n## Repeat\n\nText <img src=x> and [safe](/docs/).\n\n```html\n<script>x</script>\n```");
|
|
assert.equal(first, second);
|
|
assert.match(first, /id="repeat"/);
|
|
assert.match(first, /id="repeat-2"/);
|
|
assert.match(first, /<img src=x>/);
|
|
assert.doesNotMatch(first, /<script>x<\/script>/);
|
|
});
|
|
|
|
test("production sources contain no embedded renderer or synthesized preview fallback", () => {
|
|
const boundary = fs.readFileSync(path.resolve("src/backend/buildEngine.ts"), "utf8");
|
|
assert.match(boundary, /AI or other open-schema output is never a release input/);
|
|
assert.match(boundary, /export \* from ".\/repositoryBuildEngine"/);
|
|
const server = fs.readFileSync(path.resolve("server.ts"), "utf8");
|
|
assert.doesNotMatch(server, /cdn\.tailwindcss\.com|Simple HTML renderer|SITE HEADER/);
|
|
assert.doesNotMatch(server, /child_process|execAsync/);
|
|
const preview = fs.readFileSync(path.resolve("src/backend/http/previewRouter.ts"), "utf8");
|
|
const control = fs.readFileSync(path.resolve("src/backend/http/controlRouter.ts"), "utf8");
|
|
const storeSource = fs.readFileSync(path.resolve("src/backend/store.ts"), "utf8");
|
|
assert.match(preview, /E_RELEASE_UNAVAILABLE/);
|
|
assert.match(control, /E_CONFIG_READ_ONLY/);
|
|
assert.match(storeSource, /REMOTE_PUBLICATION_IMPLEMENTED = false as const/);
|
|
assert.doesNotMatch(storeSource, /REMOTE_PUBLICATION_IMPLEMENTED\s*=\s*process\.env/);
|
|
});
|