
-

+
One lane, five steps: command, route, gate, ship to a PR.
diff --git a/site/test/astro-config.test.ts b/site/test/astro-config.test.ts
index ca023022..4777c238 100644
--- a/site/test/astro-config.test.ts
+++ b/site/test/astro-config.test.ts
@@ -20,7 +20,8 @@ import { isUnifiedProcessor } from "@astrojs/markdown-remark";
// @ts-expect-error -- untyped .mjs config module
import config from "../astro.config.mjs";
-const BASE = "/codeArbiter";
+// @ts-expect-error -- untyped .mjs config module
+import { BASE } from "../astro.config.mjs";
describe("astro.config markdown wiring", () => {
it("configures no markdown plugins through the deprecated top-level keys", () => {
@@ -41,7 +42,14 @@ describe("astro.config markdown wiring", () => {
const { code } = await renderer.render("[Overview](/overview)");
expect(code).toContain(`href="${BASE}/overview"`);
- expect(code).not.toContain(`href="/overview"`);
+
+ // Only meaningful when the site is served from a subpath. On the apex
+
+ // domain BASE is "" and the prefixing is a correct no-op, so asserting the
+
+ // bare form is absent would assert the opposite of the intended behaviour.
+
+ if (BASE !== "") expect(code).not.toContain(`href="/overview"`);
});
it("leaves an external link untouched through the configured processor", async () => {
diff --git a/site/test/content/documentation-presentation.test.ts b/site/test/content/documentation-presentation.test.ts
index 1d871c79..ae3d33b1 100644
--- a/site/test/content/documentation-presentation.test.ts
+++ b/site/test/content/documentation-presentation.test.ts
@@ -1,6 +1,8 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
+// @ts-expect-error -- untyped .mjs config module
+import { BASE } from "../../astro.config.mjs";
const siteRoot = process.cwd();
const docsRoot = join(siteRoot, "src", "content", "docs");
@@ -96,8 +98,8 @@ describe("documentation presentation regressions", () => {
const artwork = join(siteRoot, "public", "art", "feature-forge.webp");
expect(forge).toContain('class="ca-art-banner');
- expect(forge).toContain('/codeArbiter/art/feature-forge.webp');
- expect(forge).toContain('/codeArbiter/diagrams/two-axis-model.svg');
+ expect(forge).toContain(`${BASE}/art/feature-forge.webp`);
+ expect(forge).toContain(`${BASE}/diagrams/two-axis-model.svg`);
expect(existsSync(artwork)).toBe(true);
expect(themeStyles).toContain(".ca-art-banner");
});
diff --git a/site/test/generator/diagram-href-convention.test.ts b/site/test/generator/diagram-href-convention.test.ts
index 8c653f41..3460a99f 100644
--- a/site/test/generator/diagram-href-convention.test.ts
+++ b/site/test/generator/diagram-href-convention.test.ts
@@ -5,9 +5,10 @@
*
* - In .md / .mdx pages (which cannot import an Astro component or read
* import.meta.env): the root-absolute, base-safe literal
- * src="/codeArbiter/diagrams/
.svg"
- * The base /codeArbiter is already owned by astro.config.mjs, so this adds
- * no coupling the config doesn't already carry.
+ * src="/diagrams/.svg"
+ * The base is owned by astro.config.mjs and imported here, so this adds no
+ * coupling the config doesn't already carry. On the apex domain BASE is ""
+ * and the sanctioned form is simply "/diagrams/.svg".
*
* - In .astro components: import.meta.env.BASE_URL, the base-safe form for
* that context, e.g. src={`${baseUrl}/diagrams/.svg`} — the config's
@@ -17,6 +18,8 @@
* "../diagrams/x.svg", a different hardcoded base) fails the guard.
*/
import { describe, it, expect } from "vitest";
+// @ts-expect-error -- untyped .mjs config module
+import { BASE } from "../../astro.config.mjs";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, dirname, extname } from "node:path";
import { fileURLToPath } from "node:url";
@@ -45,8 +48,18 @@ function isSanctioned(value: string, ext: string): boolean {
// import.meta.env.BASE_URL form: `${baseUrl}/diagrams/.svg`
return /^\$\{baseUrl\}\/diagrams\/[\w.-]+\.svg$/.test(value);
}
- // .md / .mdx: the root-absolute base-safe literal.
- return /^\/codeArbiter\/diagrams\/[\w.-]+\.svg$/.test(value);
+ // .md / .mdx: the root-absolute base-safe literal, with the base derived from
+ // base.mjs rather than repeated here. Repeating it is what let this guard and
+ // the config disagree when the base moved to the apex domain.
+ //
+ // The base is matched as a plain string prefix, never interpolated into a
+ // RegExp. A base such as "/docs.v2" or "/v1.0" contains regex metacharacters,
+ // and an unescaped `.` matches any character — the guard would then accept
+ // "/docsXv2/diagrams/a.svg" and quietly stop guarding. Since this file exists
+ // to survive a base change, it must not break on one.
+ const prefix = `${BASE}/diagrams/`;
+ if (!value.startsWith(prefix)) return false;
+ return /^[\w.-]+\.svg$/.test(value.slice(prefix.length));
}
describe("diagram
convention (Task 21)", () => {
diff --git a/site/test/generator/diagrams.test.ts b/site/test/generator/diagrams.test.ts
index 743adc74..c4268156 100644
--- a/site/test/generator/diagrams.test.ts
+++ b/site/test/generator/diagrams.test.ts
@@ -11,6 +11,8 @@
* is no src/assets copy to keep in sync.
*/
import { describe, it, expect } from "vitest";
+// @ts-expect-error -- untyped .mjs config module
+import { BASE } from "../../astro.config.mjs";
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
@@ -104,7 +106,7 @@ describe("concept diagrams (AC-9)", () => {
});
it("annotates the unchanged statusline capture with nine table keys", () => {
- expect(statuslineGuide).toContain("/codeArbiter/diagrams/statusline.png");
+ expect(statuslineGuide).toContain(`${BASE}/diagrams/statusline.png`);
expect(statuslineGuide.match(/ca-statusline-map__marker--\d/g)).toHaveLength(9);
for (let key = 1; key <= 9; key += 1) {
expect(statuslineGuide).toContain(`| ${key} |`);
diff --git a/site/test/link-audit/cli.test.ts b/site/test/link-audit/cli.test.ts
index 93ecf1b4..727d7cbd 100644
--- a/site/test/link-audit/cli.test.ts
+++ b/site/test/link-audit/cli.test.ts
@@ -82,14 +82,14 @@ describe("link-audit CLI", () => {
}, 60_000);
it("exits zero on a minimal complete dist", () => {
- const dist = makeDist({ "index.html": `icon` });
+ const dist = makeDist({ "index.html": `icon` });
const { status, stdout } = runCli(dist);
expect(status).toBe(0);
expect(stdout).toContain("link-audit: OK");
}, 60_000);
it("exits non-zero on a dangling internal link", () => {
- const dist = makeDist({ "index.html": `dangling` });
+ const dist = makeDist({ "index.html": `dangling` });
const { status, stderr } = runCli(dist);
expect(status).toBe(1);
expect(stderr).toContain("link failure");
diff --git a/site/test/link-audit/lib.test.ts b/site/test/link-audit/lib.test.ts
index 82c38e98..f72abc43 100644
--- a/site/test/link-audit/lib.test.ts
+++ b/site/test/link-audit/lib.test.ts
@@ -8,9 +8,17 @@ import {
resolveToDistFile,
auditDist,
missingRequiredAssets,
- BASE,
} from "../../scripts/link-audit/lib";
+/** A fixed subpath base for the algorithm cases below.
+ *
+ * Deliberately NOT the site's live BASE. These cases exercise prefix stripping
+ * and the outside-base classification, which only exist when the base is
+ * non-empty; binding them to the live value made all nine disappear the moment
+ * the site moved to an apex domain. The apex case (base "") is covered by its
+ * own describe block at the end of this file. */
+const TEST_BASE = "/codeArbiter";
+
/** Build a throwaway dist/ tree. `assets` controls the chrome the audit pins;
* `pages` is a map of dist-relative path -> file contents. */
function makeDist(options: {
@@ -69,7 +77,7 @@ describe("resolveToDistFile", () => {
const distRoot = "/fake/dist";
it("resolves a base-prefixed root-absolute target to a dist file", () => {
- const result = resolveToDistFile("/codeArbiter/overview/", "/codeArbiter/x", distRoot, BASE);
+ const result = resolveToDistFile("/codeArbiter/overview/", "/codeArbiter/x", distRoot, TEST_BASE);
expect(result).toEqual({
kind: "resolved",
distFile: join(distRoot, "overview", "index.html"),
@@ -77,7 +85,7 @@ describe("resolveToDistFile", () => {
});
it("classifies a base-less root-absolute target as outside-base (regression: previously silently skipped)", () => {
- const result = resolveToDistFile("/overview/", "/codeArbiter/x", distRoot, BASE);
+ const result = resolveToDistFile("/overview/", "/codeArbiter/x", distRoot, TEST_BASE);
expect(result).toEqual({ kind: "outside-base", normalizedPath: "/overview/" });
});
@@ -86,7 +94,7 @@ describe("resolveToDistFile", () => {
"../concepts/",
"/codeArbiter/guides/troubleshooting",
distRoot,
- BASE,
+ TEST_BASE,
);
expect(result).toEqual({
kind: "resolved",
@@ -95,12 +103,12 @@ describe("resolveToDistFile", () => {
});
it("classifies a page-relative target that normalizes outside the base as outside-base", () => {
- const result = resolveToDistFile("../../overview/", "/codeArbiter/x", distRoot, BASE);
+ const result = resolveToDistFile("../../overview/", "/codeArbiter/x", distRoot, TEST_BASE);
expect(result?.kind).toBe("outside-base");
});
it("maps an extensionless route to its directory index", () => {
- const result = resolveToDistFile("/codeArbiter/overview", "/codeArbiter/x", distRoot, BASE);
+ const result = resolveToDistFile("/codeArbiter/overview", "/codeArbiter/x", distRoot, TEST_BASE);
expect(result).toEqual({
kind: "resolved",
distFile: join(distRoot, "overview", "index.html"),
@@ -108,7 +116,7 @@ describe("resolveToDistFile", () => {
});
it("maps a file-like target (has an extension) verbatim", () => {
- const result = resolveToDistFile("/codeArbiter/favicon.svg", "/codeArbiter/x", distRoot, BASE);
+ const result = resolveToDistFile("/codeArbiter/favicon.svg", "/codeArbiter/x", distRoot, TEST_BASE);
expect(result).toEqual({
kind: "resolved",
distFile: join(distRoot, "favicon.svg"),
@@ -116,7 +124,7 @@ describe("resolveToDistFile", () => {
});
it("returns null for an empty target", () => {
- expect(resolveToDistFile("", "/codeArbiter/x", distRoot, BASE)).toBeNull();
+ expect(resolveToDistFile("", "/codeArbiter/x", distRoot, TEST_BASE)).toBeNull();
});
});
@@ -145,7 +153,7 @@ describe("auditDist", () => {
});
it("resolves base-prefixed internal links and reports base-less ones and dangling ones as failures", () => {
- const result = auditDist(dist, BASE);
+ const result = auditDist(dist, TEST_BASE);
const messages = result.failures.map((f) => f.message);
expect(messages.some((m) => m.includes("outside base path"))).toBe(true);
@@ -173,7 +181,7 @@ describe("auditDist page-inventory invariant", () => {
// "found nothing wrong" and "looked at nothing" are different outcomes.
const dist = track(makeDist({ pages: {} }));
- const result = auditDist(dist, BASE);
+ const result = auditDist(dist, TEST_BASE);
expect(result.pageCount).toBe(0);
expect(missingRequiredAssets(dist)).toEqual([]);
@@ -184,7 +192,7 @@ describe("auditDist page-inventory invariant", () => {
it("fails a dist that contains only non-HTML files", () => {
const dist = track(makeDist({ pages: { "robots.txt": "User-agent: *" } }));
- const result = auditDist(dist, BASE);
+ const result = auditDist(dist, TEST_BASE);
expect(result.pageCount).toBe(0);
expect(result.failures.some((f) => /zero HTML pages/i.test(f.message))).toBe(true);
@@ -197,7 +205,7 @@ describe("auditDist page-inventory invariant", () => {
}),
);
- const result = auditDist(dist, BASE);
+ const result = auditDist(dist, TEST_BASE);
expect(result.pageCount).toBe(1);
expect(result.checked).toBe(1);
@@ -263,3 +271,44 @@ describe("missingRequiredAssets", () => {
});
}
});
+
+describe("resolveToDistFile on an apex domain (base '')", () => {
+ const distRoot = "/fake/dist";
+ const APEX_BASE = "";
+
+ // The site's live configuration. With an empty base every root-absolute
+ // target is inside the base by definition, so the outside-base classification
+ // that the subpath cases above exercise cannot fire here — that is the
+ // behaviour change the apex move introduced, pinned rather than assumed.
+ it("resolves a root-absolute target with no prefix to strip", () => {
+ const result = resolveToDistFile("/overview/", "/x", distRoot, APEX_BASE);
+ expect(result).toEqual({
+ kind: "resolved",
+ distFile: join(distRoot, "overview", "index.html"),
+ });
+ });
+
+ it("treats every root-absolute target as inside the base", () => {
+ const result = resolveToDistFile("/anything/", "/x", distRoot, APEX_BASE);
+ expect(result?.kind).toBe("resolved");
+ });
+
+ it("maps a file-like target verbatim", () => {
+ const result = resolveToDistFile("/diagrams/x.svg", "/x", distRoot, APEX_BASE);
+ expect(result).toEqual({ kind: "resolved", distFile: join(distRoot, "diagrams", "x.svg") });
+ });
+
+ it("cannot classify anything as outside-base, so escapes fall to the missing-file check", () => {
+ // posix.normalize clamps at the root, so "../../../etc/passwd" from "/x"
+ // becomes "/etc/passwd" — which is inside a base of "". The outside-base
+ // classification is therefore INERT on an apex domain; what still catches a
+ // bad target is auditDist's dangling-file check, not this guard. Pinned so
+ // the next reader does not assume a protection that is no longer load-bearing.
+ // "passwd" is extensionless, so it maps to a directory index like any route.
+ const result = resolveToDistFile("../../../etc/passwd", "/x", distRoot, APEX_BASE);
+ expect(result).toEqual({
+ kind: "resolved",
+ distFile: join(distRoot, "etc", "passwd", "index.html"),
+ });
+ });
+});