/* Unit tests for behavior snapshot covering its documented public contract and regression-sensitive edge cases. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-0588: initial behavior snapshot redirect exclusion tests. RFC-0592: update wildcard matching test for /de directory root (now matches /de/*). RFC-0595: add redirect page detection tests (contentHash: null + redirectTarget). */ import { test, expect, beforeEach, afterEach } from "vitest"; import fs from "node:fs/promises"; import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; import path from "node:path"; import os from "node:os"; import { isRouteRedirected, collectRoutes } from "./behavior-snapshot-commands.ts"; import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; import { parseRedirectRules, extractRedirectTarget, type RedirectRule, } from "@warpgogol/werkstatt-shared/redirects"; let tmpDir: string; beforeEach(() => { tmpDir = mkdtempSync(path.join(os.tmpdir(), "bsnap-test-")); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); test("isRouteRedirected: /de/* wildcard matches /de, /de/agb, /de/agb/terms, /de/", () => { const rules = parseRedirectRules("/de/* / 308"); expect(isRouteRedirected("/de", rules)).toBe(true); expect(isRouteRedirected("/de/agb", rules)).toBe(true); expect(isRouteRedirected("/de/agb/terms", rules)).toBe(true); expect(isRouteRedirected("/agb", rules)).toBe(false); }); test("isRouteRedirected: literal path /old-page matches exactly, not sub-paths", () => { const rules = parseRedirectRules("/old-page /new-page 301"); expect(isRouteRedirected("/old-page", rules)).toBe(true); expect(isRouteRedirected("/old-page/sub", rules)).toBe(false); }); test("isRouteRedirected: 410 status rules are NOT excluded", () => { const rules = parseRedirectRules("/gone / 410"); expect(isRouteRedirected("/gone", rules)).toBe(false); }); test("isRouteRedirected: empty rules array returns false for any route", () => { expect(isRouteRedirected("/anything", [])).toBe(false); }); test("isRouteRedirected: 301 status rules are excluded", () => { const rules = parseRedirectRules("/old /new 301"); expect(isRouteRedirected("/old", rules)).toBe(true); }); test("isRouteRedirected: 308 status rules are excluded", () => { const rules = parseRedirectRules("/old /new 308"); expect(isRouteRedirected("/old", rules)).toBe(true); }); test("collectRoutes: excludes redirected routes from snapshot", async () => { mkdirSync(path.join(tmpDir, "agb"), { recursive: true }); mkdirSync(path.join(tmpDir, "de", "agb"), { recursive: true }); await fs.writeFile(path.join(tmpDir, "index.html"), "home"); await fs.writeFile(path.join(tmpDir, "agb", "index.html"), "agb"); await fs.writeFile(path.join(tmpDir, "de", "agb", "index.html"), "de-agb"); const rules: RedirectRule[] = parseRedirectRules("/de/* / 308"); const routes = await collectRoutes(tmpDir, rules, createDefaultIO().io); const paths = routes.map((r) => r.path); expect(paths).toContain("/"); expect(paths).toContain("/agb"); expect(paths).not.toContain("/de/agb"); }); test("collectRoutes: without redirect rules, all routes included", async () => { mkdirSync(path.join(tmpDir, "agb"), { recursive: true }); mkdirSync(path.join(tmpDir, "de", "agb"), { recursive: true }); await fs.writeFile(path.join(tmpDir, "index.html"), "home"); await fs.writeFile(path.join(tmpDir, "agb", "index.html"), "agb"); await fs.writeFile(path.join(tmpDir, "de", "agb", "index.html"), "de-agb"); const routes = await collectRoutes(tmpDir, [], createDefaultIO().io); const paths = routes.map((r) => r.path); expect(paths).toContain("/"); expect(paths).toContain("/agb"); expect(paths).toContain("/de/agb"); }); test("parseRedirectRules: import from @warpgogol/werkstatt-shared/redirects works correctly", () => { const rules = parseRedirectRules("/old /new 301\n# comment\n/de/* / 308\n"); expect(rules).toHaveLength(2); expect(rules[0].from).toBe("/old"); expect(rules[0].to).toBe("/new"); expect(rules[0].status).toBe(301); expect(rules[1].from).toBe("/de/*"); expect(rules[1].status).toBe(308); }); test("collectRoutes: redirect pages get contentHash: null + redirectTarget", async () => { mkdirSync(path.join(tmpDir, "de"), { recursive: true }); const normalHtml = "Homecontent"; const redirectHtml = ''; await fs.writeFile(path.join(tmpDir, "index.html"), normalHtml); await fs.writeFile(path.join(tmpDir, "de", "index.html"), redirectHtml); const routes = await collectRoutes(tmpDir, [], createDefaultIO().io); const homeRoute = routes.find((r) => r.path === "/"); expect(homeRoute).toBeDefined(); expect(homeRoute!.contentHash).not.toBeNull(); expect(homeRoute!.redirectTarget).toBeUndefined(); const deRoute = routes.find((r) => r.path === "/de"); expect(deRoute).toBeDefined(); expect(deRoute!.contentHash).toBeNull(); expect(deRoute!.redirectTarget).toBe("/"); }); test("collectRoutes: redirect page with unparseable target gets 'unknown'", async () => { mkdirSync(path.join(tmpDir, "weird"), { recursive: true }); const redirectHtml = ''; await fs.writeFile(path.join(tmpDir, "weird", "index.html"), redirectHtml); const routes = await collectRoutes(tmpDir, [], createDefaultIO().io); const weirdRoute = routes.find((r) => r.path === "/weird"); expect(weirdRoute).toBeDefined(); expect(weirdRoute!.contentHash).toBeNull(); expect(weirdRoute!.redirectTarget).toBe("unknown"); }); test("collectRoutes: non-redirect routes retain contentHash as string", async () => { await fs.writeFile(path.join(tmpDir, "index.html"), "home"); const routes = await collectRoutes(tmpDir, [], createDefaultIO().io); expect(routes).toHaveLength(1); expect(routes[0].contentHash).not.toBeNull(); expect(typeof routes[0].contentHash).toBe("string"); }); test("extractRedirectTarget: import from @warpgogol/werkstatt-shared/redirects works in handoff tests", () => { const html = ''; expect(extractRedirectTarget(html)).toBe("/target"); });