Files
xboct-flow/src/frontmatter/validateFrontmatter.test.ts
T

133 lines
4.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { MAX_TASK_TITLE_LENGTH } from "@/model/taskFile";
import { validateFrontmatter } from "./validateFrontmatter";
const valid = `---
id: "550e8400-e29b-41d4-a716-446655440000"
title: Fix login
status: todo
priority: high
tags: [bug]
assignee: ""
created: 2026-07-12T10:00:00.000Z
updated: 2026-07-12T10:00:00.000Z
---
Body
`;
describe("validateFrontmatter", () => {
it("accepts a valid task file", () => {
expect(validateFrontmatter(valid)).toEqual([]);
});
it("errors when frontmatter is missing", () => {
const issues = validateFrontmatter("no frontmatter\n");
expect(issues).toHaveLength(1);
expect(issues[0].severity).toBe("error");
expect(issues[0].message).toMatch(/frontmatter/i);
});
it("errors when frontmatter is unclosed", () => {
const issues = validateFrontmatter("---\ntitle: x\n");
expect(issues.some((i) => /not closed/i.test(i.message))).toBe(true);
});
it("errors on invalid status", () => {
const content = valid.replace("status: todo", "status: nope");
const issues = validateFrontmatter(content);
const statusIssue = issues.find((i) => i.field === "status");
expect(statusIssue).toBeDefined();
expect(statusIssue!.severity).toBe("error");
expect(statusIssue!.range.startLine).toBeGreaterThan(0);
});
it("errors on invalid priority", () => {
const content = valid.replace("priority: high", "priority: urgent");
const issues = validateFrontmatter(content);
expect(issues.some((i) => i.field === "priority")).toBe(true);
});
it("errors when id is missing", () => {
const content = valid.replace(
'id: "550e8400-e29b-41d4-a716-446655440000"\n',
"",
);
const issues = validateFrontmatter(content);
expect(
issues.some((i) => i.field === "id" && /missing/i.test(i.message)),
).toBe(true);
});
it("errors when title exceeds max length", () => {
const long = "x".repeat(MAX_TASK_TITLE_LENGTH + 1);
const content = valid.replace("title: Fix login", `title: ${long}`);
const issues = validateFrontmatter(content);
const titleIssue = issues.find((i) => i.field === "title");
expect(titleIssue).toBeDefined();
expect(titleIssue!.message).toMatch(String(MAX_TASK_TITLE_LENGTH));
});
it("errors when tags is not an array", () => {
const content = valid.replace("tags: [bug]", "tags: bug");
const issues = validateFrontmatter(content);
expect(issues.some((i) => i.field === "tags")).toBe(true);
});
it("warns on unknown fields", () => {
const content = valid.replace(
"updated: 2026-07-12T10:00:00.000Z",
"updated: 2026-07-12T10:00:00.000Z\nfoo: bar",
);
const issues = validateFrontmatter(content);
const unknown = issues.find((i) => i.field === "foo");
expect(unknown).toBeDefined();
expect(unknown!.severity).toBe("warning");
});
it("errors on broken YAML", () => {
const content = `---
title: [unterminated
---
`;
const issues = validateFrontmatter(content);
expect(issues.some((i) => /yaml/i.test(i.message))).toBe(true);
});
it("does not warn about unknown tags when knownTags is omitted", () => {
const issues = validateFrontmatter(valid);
expect(issues.filter((i) => i.field === "tags")).toEqual([]);
});
it("warns when a tag is not in knownTags", () => {
const content = valid.replace("tags: [bug]", "tags: [bug, nope]");
const issues = validateFrontmatter(content, { knownTags: ["bug"] });
const unknown = issues.filter(
(i) => i.field === "tags" && i.severity === "warning",
);
expect(unknown).toHaveLength(1);
expect(unknown[0].message).toMatch(/nope/i);
expect(unknown[0].code).toBe("unknown-tag");
expect(unknown[0].tag).toBe("nope");
});
it("does not warn when all tags are known", () => {
const issues = validateFrontmatter(valid, {
knownTags: ["bug", "frontend"],
});
expect(issues.filter((i) => i.severity === "warning")).toEqual([]);
});
it("warns for every tag when knownTags is empty array (config exists)", () => {
const issues = validateFrontmatter(valid, { knownTags: [] });
expect(
issues.some(
(i) =>
i.field === "tags" &&
i.severity === "warning" &&
/bug/i.test(i.message),
),
).toBe(true);
});
});