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
+16 -14
View File
@@ -1,24 +1,26 @@
import fs from "node:fs";
import path from "node:path";
// Load .env variables into process.env if present
try {
const envPath = path.resolve(process.cwd(), ".env");
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, "utf-8");
for (const line of envContent.split("\n")) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
const [key, ...vals] = trimmed.split("=");
const value = vals.join("=").trim().replace(/^["']|["']$/g, "");
if (!process.env[key.trim()]) {
process.env[key.trim()] = value;
// Load .env variables into process.env if SITE_DOMAIN is not already defined
if (!process.env.SITE_DOMAIN) {
try {
const envPath = path.resolve(process.cwd(), ".env");
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, "utf-8");
for (const line of envContent.split("\n")) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
const [key, ...vals] = trimmed.split("=");
const value = vals.join("=").trim().replace(/^["']|["']$/g, "");
if (!process.env[key.trim()]) {
process.env[key.trim()] = value;
}
}
}
}
} catch (e) {
// Silent fallback
}
} catch (e) {
// Silent fallback
}
const siteDomain = process.env.SITE_DOMAIN;
+74
View File
@@ -0,0 +1,74 @@
import { DateTime } from "luxon";
/**
* Custom Eleventy Filters for River Rooks Blog
*/
export const postDate = (dateObj) => {
if (!dateObj) return "";
const date = typeof dateObj === "string" ? new Date(dateObj) : dateObj;
const dt = DateTime.fromJSDate(date, { zone: "utc" });
if (!dt.isValid) return "";
return dt.toFormat("LLL dd, yyyy").toUpperCase();
};
export const isoDate = (dateObj) => {
if (!dateObj) return "";
const date = typeof dateObj === "string" ? new Date(dateObj) : dateObj;
const dt = DateTime.fromJSDate(date, { zone: "utc" });
if (!dt.isValid) return "";
return dt.toISODate() || "";
};
export const limit = (arr, count) => {
if (!Array.isArray(arr)) return [];
if (count <= 0) return [];
return arr.slice(0, count);
};
export const filterTagList = (tags) => {
const internalTags = ["all", "nav", "post", "posts"];
if (!tags) return [];
const tagArray = Array.isArray(tags) ? tags : [tags];
return tagArray.filter((tag) => {
if (typeof tag !== "string") return false;
const clean = tag.replace(/^#/, "").toLowerCase();
return !internalTags.includes(clean);
});
};
export const readingTime = (text, frontmatterTime) => {
if (frontmatterTime) return frontmatterTime;
if (!text || typeof text !== "string") return "1 MIN READ";
const clean = text.replace(/<[^>]+>/g, "").trim();
if (!clean) return "1 MIN READ";
const words = clean.split(/\s+/).filter(Boolean).length;
const minutes = Math.max(1, Math.ceil(words / 200));
return `${minutes} MIN READ`;
};
export const extractHeadings = (htmlContent) => {
if (!htmlContent || typeof htmlContent !== "string") 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;
};
export const filters = {
postDate,
isoDate,
limit,
filterTagList,
readingTime,
extractHeadings
};
export default filters;