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:
venus
2026-09-13 01:16:20 -05:00
parent fc6c17b541
commit e4601dbbcb
11 changed files with 532 additions and 66 deletions
+83
View File
@@ -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}"`
);
}
}
});
+86
View File
@@ -0,0 +1,86 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const cssDir = path.resolve("src/assets/css");
function getAllCssFiles(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(getAllCssFiles(filePath));
} else if (file.endsWith(".css")) {
results.push(filePath);
}
}
return results;
}
test("Design Specification: Strict Anti-Pattern Regression Testing", () => {
const cssFiles = getAllCssFiles(cssDir);
assert.ok(cssFiles.length >= 5, "Must inspect all modular CSS files");
for (const file of cssFiles) {
const relPath = path.relative(process.cwd(), file);
const content = fs.readFileSync(file, "utf-8");
// 1. Anti-Pattern: NO Glassmorphism (backdrop-filter)
assert.doesNotMatch(
content,
/backdrop-filter\s*:/i,
`Violation in ${relPath}: Glassmorphism (backdrop-filter) is strictly forbidden`
);
// 2. Anti-Pattern: NO Drop Shadows or Glows (box-shadow, text-shadow)
// Allowed: box-shadow: none; or text-shadow: none;
const shadowMatches = content.match(/(box-shadow|text-shadow)\s*:\s*([^;]+);/gi) || [];
for (const match of shadowMatches) {
assert.match(
match,
/(box-shadow|text-shadow)\s*:\s*none\s*;/i,
`Violation in ${relPath}: Drop shadows and glows are forbidden (found: '${match}')`
);
}
// 3. Anti-Pattern: NO Gradients (linear, radial, conic)
assert.doesNotMatch(
content,
/(linear-gradient|radial-gradient|conic-gradient)/i,
`Violation in ${relPath}: Gradients are strictly forbidden`
);
// 4. Anti-Pattern: NO Rounded Bubbles (border-radius capped at 4px max)
const radiusMatches = content.match(/border-radius\s*:\s*([^;]+);/gi) || [];
for (const match of radiusMatches) {
// Check for pixel values > 4px (e.g. 5px, 6px, 8px, 12px, 20px)
const pxMatch = match.match(/(\d+)\s*px/i);
if (pxMatch) {
const px = parseInt(pxMatch[1], 10);
assert.ok(
px <= 4,
`Violation in ${relPath}: Border radius must be 0px to 4px max, found: ${px}px in '${match}'`
);
}
// Check for percentages (e.g. 50%)
assert.doesNotMatch(
match,
/\d+%/,
`Violation in ${relPath}: Rounded pill/circular percentages forbidden in '${match}'`
);
}
// 5. Anti-Pattern: NO Float/Spring Transitions (Must be instant snaps)
const transitionMatches = content.match(/transition\s*:\s*([^;]+);/gi) || [];
for (const match of transitionMatches) {
assert.match(
match,
/transition\s*:\s*(none(\s*!important)?|0s\s*);/i,
`Violation in ${relPath}: Transitions must be instant (none or 0s), found: '${match}'`
);
}
}
});
+107
View File
@@ -0,0 +1,107 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
postDate,
isoDate,
limit,
filterTagList,
readingTime,
extractHeadings
} from "../src/_filters/index.js";
test("Filters - postDate: formats valid dates", () => {
assert.equal(postDate("2026-07-20"), "JUL 20, 2026");
assert.equal(postDate(new Date("2026-12-31T00:00:00Z")), "DEC 31, 2026");
});
test("Filters - postDate: handles adversarial / invalid inputs gracefully", () => {
assert.equal(postDate(null), "");
assert.equal(postDate(undefined), "");
assert.equal(postDate("invalid-date-string"), "");
assert.equal(postDate(""), "");
});
test("Filters - isoDate: formats ISO strings", () => {
assert.equal(isoDate("2026-07-20"), "2026-07-20");
assert.equal(isoDate(new Date("2026-05-15T00:00:00Z")), "2026-05-15");
});
test("Filters - isoDate: handles adversarial / invalid inputs gracefully", () => {
assert.equal(isoDate(null), "");
assert.equal(isoDate(undefined), "");
assert.equal(isoDate("not-a-date"), "");
});
test("Filters - limit: slices arrays correctly", () => {
assert.deepEqual(limit([1, 2, 3, 4, 5], 3), [1, 2, 3]);
assert.deepEqual(limit([1, 2], 5), [1, 2]);
});
test("Filters - limit: handles adversarial inputs (negative, 0, non-array)", () => {
assert.deepEqual(limit([1, 2, 3], 0), []);
assert.deepEqual(limit([1, 2, 3], -5), []);
assert.deepEqual(limit(null, 3), []);
assert.deepEqual(limit("not-an-array", 3), []);
});
test("Filters - filterTagList: removes internal system tags", () => {
const input = ["firmware", "posts", "security", "all", "nav", "post"];
assert.deepEqual(filterTagList(input), ["firmware", "security"]);
});
test("Filters - filterTagList: handles adversarial inputs (# prefix, casing, non-string)", () => {
const input = ["#firmware", "#POSTS", "ALL", "#homelab", 42, null, undefined];
assert.deepEqual(filterTagList(input), ["#firmware", "#homelab"]);
assert.deepEqual(filterTagList(null), []);
assert.deepEqual(filterTagList(undefined), []);
});
test("Filters - readingTime: calculates reading time from word count", () => {
// 199 words -> 1 min
const shortText = "word ".repeat(199);
assert.equal(readingTime(shortText), "1 MIN READ");
// 201 words -> 2 min
const mediumText = "word ".repeat(201);
assert.equal(readingTime(mediumText), "2 MIN READ");
// 1000 words -> 5 min
const longText = "word ".repeat(1000);
assert.equal(readingTime(longText), "5 MIN READ");
});
test("Filters - readingTime: respects explicit frontmatter override", () => {
assert.equal(readingTime("any text", "15 MIN READ"), "15 MIN READ");
});
test("Filters - readingTime: handles adversarial HTML tags without inflating word count", () => {
// HTML tags should be stripped before counting words
const htmlText = "<div><span>word</span> <p>another</p></div>";
assert.equal(readingTime(htmlText), "1 MIN READ");
assert.equal(readingTime(""), "1 MIN READ");
assert.equal(readingTime(null), "1 MIN READ");
});
test("Filters - extractHeadings: extracts H2 and H3 with IDs and titles", () => {
const html = `
<h1>Title To Ignore</h1>
<h2 id="section-1">1.0 First Section</h2>
<p>Some text</p>
<h3 id="sub-1">1.1 Sub Section</h3>
<h4 id="h4-ignore">Ignore H4</h4>
<h2 id="section-2">2.0 Second Section with <code>code</code> and <span class="tag">span</span></h2>
`;
const headings = extractHeadings(html);
assert.equal(headings.length, 3);
assert.deepEqual(headings[0], { level: 2, id: "section-1", title: "1.0 First Section" });
assert.deepEqual(headings[1], { level: 3, id: "sub-1", title: "1.1 Sub Section" });
assert.deepEqual(headings[2], { level: 2, id: "section-2", title: "2.0 Second Section with code and span" });
});
test("Filters - extractHeadings: handles adversarial HTML inputs gracefully", () => {
assert.deepEqual(extractHeadings(""), []);
assert.deepEqual(extractHeadings(null), []);
assert.deepEqual(extractHeadings(undefined), []);
assert.deepEqual(extractHeadings("<p>No headings here</p>"), []);
});
+71
View File
@@ -0,0 +1,71 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const postsDir = path.resolve("src/posts");
test("Content Contract: All posts in src/posts/ satisfy frontmatter schema", () => {
assert.ok(fs.existsSync(postsDir), "src/posts directory must exist");
const postFiles = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
assert.ok(postFiles.length > 0, "There must be at least one published post");
for (const file of postFiles) {
const filePath = path.join(postsDir, file);
const content = fs.readFileSync(filePath, "utf-8");
// 1. Filename Convention: YYYY-MM-DD-slug.md
assert.match(
file,
/^\d{4}-\d{2}-\d{2}-[a-z0-9-]+\.md$/,
`Post filename '${file}' must follow 'YYYY-MM-DD-slug.md' naming standard`
);
// 2. YAML Frontmatter Boundary check
assert.match(
content,
/^---\n([\s\S]*?)\n---/,
`Post '${file}' must start with valid YAML frontmatter delimiters`
);
// Extract frontmatter block
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
const fmText = fmMatch[1];
// 3. Title Invariant
assert.match(
fmText,
/title:\s*"[^"]{5,}"/,
`Post '${file}' must contain a valid title of at least 5 characters`
);
// 4. Date Invariant (ISO Format)
assert.match(
fmText,
/date:\s*\d{4}-\d{2}-\d{2}/,
`Post '${file}' must define an ISO date (YYYY-MM-DD)`
);
// 5. Layout Invariant
assert.match(
fmText,
/layout:\s*layouts\/post\.njk/,
`Post '${file}' must specify 'layout: layouts/post.njk'`
);
// 6. Tags Invariant
assert.match(
fmText,
/tags:\s*\n(\s+-\s+[a-z0-9-]+\n)+/,
`Post '${file}' must specify a non-empty YAML tags list`
);
// 7. Excerpt Invariant
assert.match(
fmText,
/excerpt:\s*"[^"]{20,}"/,
`Post '${file}' must provide a substantive excerpt of at least 20 characters`
);
}
});
+64
View File
@@ -0,0 +1,64 @@
import test from "node:test";
import assert from "node:assert/strict";
import { execSync } from "node:child_process";
test("Site Config: throws explicit error when SITE_DOMAIN is unset", () => {
// Run isolated Node process with empty environment to verify error throw
const cmd = `node -e '
process.env.SITE_DOMAIN = "";
delete process.env.SITE_DOMAIN;
// mock empty cwd without .env
process.chdir("/tmp");
import("${process.cwd()}/src/_data/site.js")
.then(() => process.exit(0))
.catch(err => {
if (err.message.includes("FATAL CONFIGURATION ERROR: SITE_DOMAIN")) {
process.exit(42);
}
process.exit(1);
});
'`;
try {
execSync(cmd, { stdio: "pipe" });
assert.fail("Should have thrown error when SITE_DOMAIN is missing");
} catch (err) {
assert.equal(err.status, 42, "Process exited with status 42 (expected error caught)");
}
});
test("Site Config: correctly parses SITE_DOMAIN and derives service subdomains", async () => {
process.env.SITE_DOMAIN = "testcluster.internal";
delete process.env.SITE_URL;
// Import site configuration dynamically
const { default: site } = await import(`../src/_data/site.js?t=${Date.now()}`);
assert.equal(site.domain, "testcluster.internal");
assert.equal(site.url, "https://testcluster.internal");
// Verify dynamic subdomains
const gitService = site.services.find((s) => s.name === "Forgejo Git");
assert.ok(gitService, "Forgejo Git service exists");
assert.equal(gitService.url, "https://git.testcluster.internal");
assert.equal(gitService.tag, "git.testcluster.internal:2222");
const chefService = site.services.find((s) => s.name === "CyberChef");
assert.ok(chefService, "CyberChef service exists");
assert.equal(chefService.url, "https://chef.testcluster.internal");
// Verify navigation Git Forge URL
const navGit = site.nav.find((n) => n.label === "Git Forge");
assert.ok(navGit, "Nav Git Forge link exists");
assert.equal(navGit.url, "https://git.testcluster.internal");
});
test("Site Config: respects explicit SITE_URL override", async () => {
process.env.SITE_DOMAIN = "riverrooks.dev";
process.env.SITE_URL = "http://localhost:8080";
const { default: site } = await import(`../src/_data/site.js?t=${Date.now()}`);
assert.equal(site.domain, "riverrooks.dev");
assert.equal(site.url, "http://localhost:8080");
});