feat: added a hidden toggle availible in frontmatter which renders the note unlisted, also updated agent behavior with a compliance skill
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import http from "node:http";
|
||||
import { isPostHidden, isPostPublic, postComparator } from "../src/_utils/sort-posts.js";
|
||||
import { createServer, normalizeObsidianMarkdown } from "../server.js";
|
||||
|
||||
test("Hidden Posts: isPostHidden accurately identifies hidden flags across data shapes", () => {
|
||||
// 1. Explicit boolean true
|
||||
assert.equal(isPostHidden({ data: { hidden: true } }), true, "Explicit boolean true in data");
|
||||
assert.equal(isPostHidden({ hidden: true }), true, "Root level hidden true");
|
||||
|
||||
// 2. Truthy string representations (case-insensitive & trimmed)
|
||||
assert.equal(isPostHidden({ data: { hidden: "true" } }), true, "String 'true'");
|
||||
assert.equal(isPostHidden({ data: { hidden: "TRUE" } }), true, "Uppercase string 'TRUE'");
|
||||
assert.equal(isPostHidden({ data: { hidden: " true " } }), true, "Whitespace-padded 'true'");
|
||||
assert.equal(isPostHidden({ data: { hidden: "yes" } }), true, "String 'yes'");
|
||||
assert.equal(isPostHidden({ data: { hidden: "YES" } }), true, "String 'YES'");
|
||||
assert.equal(isPostHidden({ data: { hidden: "1" } }), true, "String '1'");
|
||||
assert.equal(isPostHidden({ data: { hidden: 1 } }), true, "Numeric 1");
|
||||
|
||||
// 3. Falsy representations
|
||||
assert.equal(isPostHidden({ data: { hidden: false } }), false, "Explicit boolean false");
|
||||
assert.equal(isPostHidden({ data: { hidden: "false" } }), false, "String 'false'");
|
||||
assert.equal(isPostHidden({ data: { hidden: "no" } }), false, "String 'no'");
|
||||
assert.equal(isPostHidden({ data: { hidden: "0" } }), false, "String '0'");
|
||||
assert.equal(isPostHidden({ data: { hidden: 0 } }), false, "Numeric 0");
|
||||
assert.equal(isPostHidden({ data: { hidden: "" } }), false, "Empty string");
|
||||
assert.equal(isPostHidden({ data: { hidden: null } }), false, "Null property");
|
||||
assert.equal(isPostHidden({ data: {} }), false, "Missing hidden property");
|
||||
assert.equal(isPostHidden({}), false, "Empty post object");
|
||||
|
||||
// 4. Adversarial edge cases
|
||||
assert.equal(isPostHidden(null), false, "Null input");
|
||||
assert.equal(isPostHidden(undefined), false, "Undefined input");
|
||||
assert.equal(isPostHidden("unexpected-string"), false, "Primitive string input");
|
||||
assert.equal(isPostHidden(12345), false, "Primitive number input");
|
||||
assert.equal(isPostHidden([]), false, "Array input");
|
||||
});
|
||||
|
||||
test("Hidden Posts: isPostPublic requires post to be neither draft nor hidden", () => {
|
||||
// Public post: not draft, not hidden
|
||||
assert.equal(
|
||||
isPostPublic({ data: { title: "Public Post", draft: false, hidden: false } }),
|
||||
true,
|
||||
"Standard post is public"
|
||||
);
|
||||
assert.equal(
|
||||
isPostPublic({ data: { title: "Public Post" } }),
|
||||
true,
|
||||
"Post without draft or hidden is public"
|
||||
);
|
||||
|
||||
// Hidden post is NOT public
|
||||
assert.equal(
|
||||
isPostPublic({ data: { title: "Hidden Post", hidden: true } }),
|
||||
false,
|
||||
"Hidden post is not public"
|
||||
);
|
||||
assert.equal(
|
||||
isPostPublic({ data: { title: "Hidden Post", hidden: "true" } }),
|
||||
false,
|
||||
"Hidden string post is not public"
|
||||
);
|
||||
|
||||
// Draft post is NOT public
|
||||
assert.equal(
|
||||
isPostPublic({ data: { title: "Draft Post", draft: true } }),
|
||||
false,
|
||||
"Draft post is not public"
|
||||
);
|
||||
|
||||
// Both draft and hidden is NOT public
|
||||
assert.equal(
|
||||
isPostPublic({ data: { title: "Draft and Hidden", draft: true, hidden: true } }),
|
||||
false,
|
||||
"Draft and hidden post is not public"
|
||||
);
|
||||
|
||||
// Adversarial checks
|
||||
assert.equal(isPostPublic(null), false, "Null is not public");
|
||||
assert.equal(isPostPublic(undefined), false, "Undefined is not public");
|
||||
assert.equal(isPostPublic("random"), false, "Non-object is not public");
|
||||
});
|
||||
|
||||
test("Hidden Posts: Collection filtering excludes hidden posts from public posts list and tagList", () => {
|
||||
const posts = [
|
||||
{
|
||||
data: { title: "Normal Post", tags: ["tech"] },
|
||||
date: new Date("2026-09-10")
|
||||
},
|
||||
{
|
||||
data: { title: "Pinned Post", pinned: true, tags: ["tech", "featured"] },
|
||||
date: new Date("2026-09-01")
|
||||
},
|
||||
{
|
||||
data: { title: "Draft Post", draft: true, tags: ["wip"] },
|
||||
date: new Date("2026-09-15")
|
||||
},
|
||||
{
|
||||
data: { title: "Secret Hidden Post", hidden: true, tags: ["secret-only"] },
|
||||
date: new Date("2026-09-20")
|
||||
},
|
||||
{
|
||||
data: { title: "Another Hidden Post", hidden: "yes", tags: ["internal"] },
|
||||
date: new Date("2026-09-21")
|
||||
},
|
||||
{
|
||||
data: { title: "Explicit Unhidden Post", hidden: false, tags: ["news"] },
|
||||
date: new Date("2026-09-12")
|
||||
}
|
||||
];
|
||||
|
||||
// Filter public posts using isPostPublic (or !draft && !isPostHidden)
|
||||
const publicPosts = posts.filter(isPostPublic).sort(postComparator);
|
||||
|
||||
const titles = publicPosts.map((p) => p.data.title);
|
||||
assert.deepEqual(
|
||||
titles,
|
||||
["Pinned Post", "Explicit Unhidden Post", "Normal Post"],
|
||||
"Public posts must include only non-draft, non-hidden posts in proper sorted order"
|
||||
);
|
||||
|
||||
// Verify secret-only tags from hidden posts do not leak into public tag list
|
||||
const publicTags = new Set();
|
||||
posts
|
||||
.filter(isPostPublic)
|
||||
.forEach((item) => {
|
||||
const tags = item.data.tags || [];
|
||||
for (const t of tags) publicTags.add(t);
|
||||
});
|
||||
|
||||
assert.ok(!publicTags.has("secret-only"), "Hidden post tag 'secret-only' must not appear in public tag list");
|
||||
assert.ok(!publicTags.has("internal"), "Hidden post tag 'internal' must not appear in public tag list");
|
||||
assert.ok(!publicTags.has("wip"), "Draft post tag 'wip' must not appear in public tag list");
|
||||
assert.ok(publicTags.has("tech"), "Public post tag 'tech' must appear in public tag list");
|
||||
assert.ok(publicTags.has("featured"), "Public post tag 'featured' must appear in public tag list");
|
||||
});
|
||||
|
||||
test("Hidden Posts: Post layout injects noindex meta tag when hidden is true", () => {
|
||||
const postLayoutPath = path.resolve("src/_includes/layouts/post.njk");
|
||||
assert.ok(fs.existsSync(postLayoutPath), "post.njk must exist");
|
||||
const postLayout = fs.readFileSync(postLayoutPath, "utf-8");
|
||||
|
||||
// Adversarial verification: hidden posts must instruct search crawlers not to index or follow
|
||||
assert.match(
|
||||
postLayout,
|
||||
/noindex,\s*nofollow/i,
|
||||
"post.njk must define 'noindex, nofollow' robots meta tag when hidden"
|
||||
);
|
||||
assert.match(
|
||||
postLayout,
|
||||
/{%\s*if\s+hidden\b/i,
|
||||
"post.njk must conditionally check if post is hidden"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test("Hidden Posts: Obsidian normalization preserves hidden frontmatter property", () => {
|
||||
const obsidianNote = `---
|
||||
title: "Confidential System Blueprint"
|
||||
created: 2026-09-23
|
||||
tags: ["#architecture", "#secret"]
|
||||
hidden: true
|
||||
---
|
||||
|
||||
# Blueprint
|
||||
Details for authorized eyes only.
|
||||
`;
|
||||
|
||||
const normalized = normalizeObsidianMarkdown(obsidianNote);
|
||||
assert.match(normalized, /hidden:\s*true/, "Normalized markdown must preserve 'hidden: true'");
|
||||
assert.match(normalized, /title:\s*"Confidential System Blueprint"/, "Preserves title");
|
||||
assert.match(normalized, /date:\s*2026-09-23/, "Bridges created to date");
|
||||
assert.match(normalized, /layout:\s*layouts\/post\.njk/, "Adds post layout");
|
||||
});
|
||||
|
||||
test("Hidden Posts: End-to-end static compilation and accessibility via direct link", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
|
||||
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
|
||||
|
||||
try {
|
||||
// 1. Create a public post and a hidden post
|
||||
const publicPostContent = `---
|
||||
title: "Public Announcement"
|
||||
date: 2026-09-20
|
||||
layout: layouts/post.njk
|
||||
tags: ["news"]
|
||||
excerpt: "This is a public announcement for everyone to read."
|
||||
---
|
||||
Public content visible to everyone.`;
|
||||
|
||||
const hiddenPostContent = `---
|
||||
title: "Private Unlisted Memo"
|
||||
date: 2026-09-22
|
||||
layout: layouts/post.njk
|
||||
tags: ["confidential"]
|
||||
excerpt: "This is an unlisted memo meant for direct link access only."
|
||||
hidden: true
|
||||
---
|
||||
Secret memo content available by link only.`;
|
||||
|
||||
fs.writeFileSync(path.join(tmpPosts, "2026-09-20-public-post.md"), publicPostContent, "utf-8");
|
||||
fs.writeFileSync(path.join(tmpPosts, "2026-09-22-secret-memo.md"), hiddenPostContent, "utf-8");
|
||||
|
||||
// 2. Set up server with custom directories
|
||||
process.env.API_SECRET = "test-secret";
|
||||
const server = createServer(tmpDir, tmpPosts);
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const port = server.address().port;
|
||||
|
||||
try {
|
||||
// Simulate static files created by build
|
||||
fs.mkdirSync(path.join(tmpDir, "posts/2026-09-22-secret-memo"), { recursive: true });
|
||||
fs.mkdirSync(path.join(tmpDir, "posts/2026-09-20-public-post"), { recursive: true });
|
||||
|
||||
const secretHtml = `<!DOCTYPE html><html><head><title>Private Unlisted Memo</title><meta name="robots" content="noindex, nofollow"></head><body>Secret memo content available by link only.</body></html>`;
|
||||
const publicHtml = `<!DOCTYPE html><html><head><title>Public Announcement</title></head><body>Public content visible to everyone.</body></html>`;
|
||||
const indexHtml = `<!DOCTYPE html><html><body><a href="/posts/2026-09-20-public-post/">Public Announcement</a></body></html>`;
|
||||
|
||||
fs.writeFileSync(path.join(tmpDir, "posts/2026-09-22-secret-memo/index.html"), secretHtml);
|
||||
fs.writeFileSync(path.join(tmpDir, "posts/2026-09-20-public-post/index.html"), publicHtml);
|
||||
fs.writeFileSync(path.join(tmpDir, "index.html"), indexHtml);
|
||||
|
||||
// Verify direct access to hidden post succeeds (HTTP 200 OK)
|
||||
const resSecret = await fetch(`http://127.0.0.1:${port}/posts/2026-09-22-secret-memo/`);
|
||||
assert.strictEqual(resSecret.status, 200, "Direct link to hidden post must return 200 OK");
|
||||
const secretBody = await resSecret.text();
|
||||
assert.match(secretBody, /Secret memo content available by link only/, "Hidden post body is accessible");
|
||||
assert.match(secretBody, /noindex,\s*nofollow/, "Hidden post includes noindex meta tag");
|
||||
|
||||
// Verify public index does NOT contain link or title to hidden post
|
||||
const resIndex = await fetch(`http://127.0.0.1:${port}/`);
|
||||
assert.strictEqual(resIndex.status, 200);
|
||||
const indexBody = await resIndex.text();
|
||||
assert.match(indexBody, /Public Announcement/, "Public post is listed on index");
|
||||
assert.doesNotMatch(indexBody, /Private Unlisted Memo/, "Hidden post MUST NOT appear on index");
|
||||
assert.doesNotMatch(indexBody, /secret-memo/, "Hidden post link MUST NOT appear on index");
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
fs.rmSync(tmpPosts, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("Hidden Posts: Obsidian API ingestion correctly writes hidden frontmatter", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
|
||||
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
|
||||
process.env.API_SECRET = "test-secret";
|
||||
|
||||
const server = createServer(tmpDir, tmpPosts);
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const port = server.address().port;
|
||||
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/posts`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer test-secret"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
slug: "unlisted-post",
|
||||
content: `---
|
||||
title: "Direct Share Only"
|
||||
created: 2026-09-23
|
||||
hidden: true
|
||||
tags: ["#private"]
|
||||
---
|
||||
Secret note content.`
|
||||
})
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
const json = await res.json();
|
||||
assert.strictEqual(json.status, "published");
|
||||
assert.strictEqual(json.path, "/posts/unlisted-post/");
|
||||
|
||||
const writtenFile = path.join(tmpPosts, "unlisted-post.md");
|
||||
assert.ok(fs.existsSync(writtenFile), "Post file must exist");
|
||||
const writtenContent = fs.readFileSync(writtenFile, "utf-8");
|
||||
assert.match(writtenContent, /hidden:\s*true/);
|
||||
assert.match(writtenContent, /layout:\s*layouts\/post\.njk/);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
fs.rmSync(tmpPosts, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const skillDir = path.resolve(".agents/skills/standards-verification");
|
||||
const skillPath = path.join(skillDir, "SKILL.md");
|
||||
const standardsDocPath = path.resolve(".agents/standards.md");
|
||||
const skillStandardsRefPath = path.join(skillDir, "references/standards.md");
|
||||
|
||||
test("Standards Verification Skill: Directory, SKILL.md, and standards document exist", () => {
|
||||
assert.ok(fs.existsSync(skillDir), ".agents/skills/standards-verification directory must exist");
|
||||
assert.ok(fs.existsSync(skillPath), "SKILL.md must exist in standards-verification skill");
|
||||
assert.ok(fs.existsSync(standardsDocPath), "Dedicated standards document .agents/standards.md must exist");
|
||||
assert.ok(fs.existsSync(skillStandardsRefPath), "Skill references/standards.md must exist");
|
||||
});
|
||||
|
||||
test("Standards Verification Skill: SKILL.md contains correct metadata and execution directives", () => {
|
||||
const content = fs.readFileSync(skillPath, "utf-8");
|
||||
|
||||
// 1. YAML frontmatter name and description
|
||||
assert.match(content, /^---\s*\nname:\s*standards-verification/m, "Must declare name: standards-verification");
|
||||
assert.match(content, /description:\s*>-?[\s\S]*?---/, "Must define description in frontmatter");
|
||||
|
||||
// 2. Invocation conditions: called sparingly, major feature, user request, or debugging
|
||||
assert.match(content, /sparingly/i, "Must specify calling sparingly and only when needed");
|
||||
assert.match(content, /major\s+function|major\s+feature/i, "Must mention use case: major function/feature implemented");
|
||||
assert.match(content, /user(?:\s+\w+)?\s+asks/i, "Must mention use case: user explicitly requests verification");
|
||||
assert.match(content, /debug(?:ging)?/i, "Must mention use case: debugging code issues");
|
||||
|
||||
// 3. Targeted inspection via git status / git diff (not whole codebase unless necessary)
|
||||
assert.match(content, /git\s+status/i, "Must specify using git status to identify changes");
|
||||
assert.match(content, /git\s+diff/i, "Must specify using git diff to inspect targeted changes");
|
||||
assert.match(content, /not\s+(?:the\s+)?whole\s+codebase/i, "Must specify NOT inspecting whole codebase unless necessary");
|
||||
|
||||
// 4. Debugging guidance: explain why standards violations cause errors
|
||||
assert.match(content, /(?:suggest|explain)[*\s`_]+why/i, "Must instruct suggesting why standards issues cause errors");
|
||||
|
||||
|
||||
// 5. External standards document reference
|
||||
assert.match(content, /standards\.md/i, "Must refer to standards.md rather than embedding standards directly");
|
||||
});
|
||||
|
||||
test("Standards Document: Comprehensive specifications across all architectural layers", () => {
|
||||
const content = fs.readFileSync(standardsDocPath, "utf-8");
|
||||
|
||||
// 1. Design & CSS Anti-Patterns
|
||||
assert.match(content, /backdrop-filter/i, "Must document prohibition on backdrop-filter");
|
||||
assert.match(content, /box-shadow/i, "Must document prohibition on box-shadow");
|
||||
assert.match(content, /gradient/i, "Must document prohibition on gradients");
|
||||
assert.match(content, /border-radius/i, "Must document border-radius constraints (0px to 4px)");
|
||||
assert.match(content, /instant/i, "Must document instant binary transitions");
|
||||
|
||||
// 2. Templating & Eleventy
|
||||
assert.match(content, /isPostPublic/i, "Must document isPostPublic collection filtering");
|
||||
assert.match(content, /hidden/i, "Must document hidden post behavior and noindex meta tag");
|
||||
|
||||
// 3. Frontmatter & Content Contract
|
||||
assert.match(content, /title/i, "Must document title requirement");
|
||||
assert.match(content, /date|created/i, "Must document date requirement");
|
||||
assert.match(content, /tags/i, "Must document tags requirement");
|
||||
assert.match(content, /excerpt/i, "Must document excerpt requirement");
|
||||
|
||||
// 4. Server & Security Invariants
|
||||
assert.match(content, /path\s+traversal/i, "Must document path traversal defense");
|
||||
assert.match(content, /healthz/i, "Must document healthcheck endpoint");
|
||||
assert.match(content, /X-Content-Type-Options/i, "Must document security headers");
|
||||
|
||||
// 5. Strict TDD & Verification Invariants
|
||||
assert.match(content, /before\s+(?:writing\s+or\s+)?modifying/i, "Must mandate tests before modifying functional code");
|
||||
assert.match(content, /adversarial/i, "Must mandate adversarial testing");
|
||||
assert.match(content, /pass\s+before\s+presenting/i, "Must mandate tests pass before presenting final output");
|
||||
});
|
||||
|
||||
test("Standards Verification Skill: Adversarial checks against full-codebase scanning defaults", () => {
|
||||
const content = fs.readFileSync(skillPath, "utf-8");
|
||||
|
||||
// Adversarial: ensure no instruction tells the agent to indiscriminately scan everything by default
|
||||
assert.doesNotMatch(
|
||||
content,
|
||||
/always\s+scan\s+(?:all|the\s+entire|the\s+whole)\s+codebase/i,
|
||||
"Must not default to scanning the whole codebase"
|
||||
);
|
||||
});
|
||||
|
||||
test("Standards Verification Skill: Helper script verify.js exists and executes safely", () => {
|
||||
const scriptPath = path.join(skillDir, "scripts/verify.js");
|
||||
assert.ok(fs.existsSync(scriptPath), "scripts/verify.js must exist");
|
||||
const content = fs.readFileSync(scriptPath, "utf-8");
|
||||
assert.match(content, /git\s+status/i, "Script must inspect git status");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user