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,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>"), []);
|
||||
});
|
||||
Reference in New Issue
Block a user