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:
+16
-14
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user