feat: adopt strict TDD framework and add adversarial test suite
- Update AGENTS.md and .agents/rules/blog-system.md with strict TDD mandate - Extract Eleventy custom filters into modular src/_filters/index.js for unit testing - Add npm test script using Node.js native test runner (node:test) - Create test/site-config.test.js for environment validation and fatal error assertions - Create test/filters.test.js for adversarial filter edge cases (dates, limits, reading time, headings) - Create test/frontmatter-schema.test.js enforcing post metadata contracts - Create test/design-rules.test.js enforcing strict anti-patterns (no glassmorphism, no shadows, max 4px radius) - Create test/build-integrity.test.js checking static output, zero 404 links, and TOC anchor targets
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
const siteDir = path.resolve("_site");
|
||||
|
||||
function getAllHtmlFiles(dir) {
|
||||
let results = [];
|
||||
const list = fs.readdirSync(dir);
|
||||
for (const file of list) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat && stat.isDirectory()) {
|
||||
results = results.concat(getAllHtmlFiles(filePath));
|
||||
} else if (file.endsWith(".html")) {
|
||||
results.push(filePath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
test("Build & Link Integrity: Output inspection and link verification", () => {
|
||||
// Ensure fresh build exists
|
||||
if (!fs.existsSync(siteDir)) {
|
||||
execSync("npm run build", { stdio: "pipe" });
|
||||
}
|
||||
|
||||
// 1. Core Pages Exist
|
||||
assert.ok(fs.existsSync(path.join(siteDir, "index.html")), "Root _site/index.html must exist");
|
||||
assert.ok(fs.existsSync(path.join(siteDir, "dispatches/index.html")), "_site/dispatches/index.html must exist");
|
||||
assert.ok(fs.existsSync(path.join(siteDir, "assets/css/main.css")), "_site/assets/css/main.css must exist");
|
||||
|
||||
const htmlFiles = getAllHtmlFiles(siteDir);
|
||||
assert.ok(htmlFiles.length >= 4, "Must compile all core pages and articles");
|
||||
|
||||
for (const htmlFile of htmlFiles) {
|
||||
const relHtml = path.relative(siteDir, htmlFile);
|
||||
const content = fs.readFileSync(htmlFile, "utf-8");
|
||||
|
||||
// 2. HTML Boilerplate Invariants
|
||||
assert.match(content, /<!DOCTYPE html>/i, `${relHtml} must contain <!DOCTYPE html>`);
|
||||
assert.match(content, /<meta\s+name="viewport"/i, `${relHtml} must have viewport meta tag`);
|
||||
assert.match(content, /<link\s+rel="stylesheet"\s+href="\/assets\/css\/main\.css">/i, `${relHtml} must link to main.css`);
|
||||
|
||||
// 3. Internal Link Integrity (No 404s!)
|
||||
const internalLinks = [...content.matchAll(/href="(\/[^"#?]*)"/g)].map((m) => m[1]);
|
||||
for (const link of internalLinks) {
|
||||
if (link.startsWith("/feed.xml")) continue; // dynamic RSS route
|
||||
// Normalize link to file on disk: /dispatches/ -> _site/dispatches/index.html or _site/dispatches.html
|
||||
let targetPath;
|
||||
if (link.endsWith("/")) {
|
||||
targetPath = path.join(siteDir, link, "index.html");
|
||||
} else if (path.extname(link)) {
|
||||
targetPath = path.join(siteDir, link);
|
||||
} else {
|
||||
targetPath = path.join(siteDir, `${link}.html`);
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
targetPath = path.join(siteDir, link, "index.html");
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
fs.existsSync(targetPath),
|
||||
`Broken internal link in ${relHtml}: href="${link}" resolves to missing file: ${targetPath}`
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Anchor Link Integrity (TOC anchor links match actual heading IDs)
|
||||
const anchorLinks = [...content.matchAll(/href="#([^"]+)"/g)].map((m) => m[1]);
|
||||
for (const anchor of anchorLinks) {
|
||||
// Ignore skip-to-content or standard body anchors
|
||||
if (anchor === "main-content") continue;
|
||||
const expectedIdRegex = new RegExp(`id="${anchor}"`, "i");
|
||||
assert.match(
|
||||
content,
|
||||
expectedIdRegex,
|
||||
`Broken TOC anchor link in ${relHtml}: href="#${anchor}" has no corresponding id="${anchor}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user