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
+5 -52
View File
@@ -1,7 +1,7 @@
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
import markdownIt from "markdown-it";
import markdownItAnchor from "markdown-it-anchor";
import { DateTime } from "luxon";
import { filters } from "./src/_filters/index.js";
export default function (eleventyConfig) {
// 1. Static Asset Passthrough Copy
@@ -32,57 +32,10 @@ export default function (eleventyConfig) {
eleventyConfig.setLibrary("md", markdownLib);
// 4. Custom Filters
// Format readable dates: SEP 12, 2026
eleventyConfig.addFilter("postDate", (dateObj) => {
if (!dateObj) return "";
return DateTime.fromJSDate(new Date(dateObj), { zone: "utc" })
.toFormat("LLL dd, yyyy")
.toUpperCase();
});
// ISO date format for <time datetime="...">
eleventyConfig.addFilter("isoDate", (dateObj) => {
if (!dateObj) return "";
return DateTime.fromJSDate(new Date(dateObj), { zone: "utc" }).toISODate();
});
// Limit array items
eleventyConfig.addFilter("limit", (arr, limit) => {
if (!Array.isArray(arr)) return [];
return arr.slice(0, limit);
});
// Filter out internal system tags
eleventyConfig.addFilter("filterTagList", (tags) => {
const internalTags = ["all", "nav", "post", "posts"];
return (tags || []).filter((tag) => !internalTags.includes(tag.toLowerCase()));
});
// Reading time estimator (defaults to 200 words/min if frontmatter lacks readingTime)
eleventyConfig.addFilter("readingTime", (text, frontmatterTime) => {
if (frontmatterTime) return frontmatterTime;
if (!text) return "1 MIN READ";
const wordCount = text.trim().split(/\s+/).length;
const minutes = Math.ceil(wordCount / 200);
return `${minutes} MIN READ`;
});
// Extract H2 and H3 headings with IDs for dynamic Table of Contents
eleventyConfig.addFilter("extractHeadings", (htmlContent) => {
if (!htmlContent) return [];
const headingRegex = /<h([23])\s+id="([^"]+)"[^>]*>(.*?)<\/h\1>/gis;
const headings = [];
let match;
while ((match = headingRegex.exec(htmlContent)) !== null) {
headings.push({
level: parseInt(match[1], 10),
id: match[2],
title: match[3].replace(/<[^>]+>/g, "").trim()
});
}
return headings;
});
// 4. Custom Filters (imported from modular src/_filters/index.js)
for (const [name, fn] of Object.entries(filters)) {
eleventyConfig.addFilter(name, fn);
}
// 5. Custom Collections
// Chronological blog posts collection