mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
522 lines
17 KiB
TypeScript
522 lines
17 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
|
import {
|
|
extractFrontmatter,
|
|
parseSkillFrontmatter,
|
|
} from "../hooks/skill-map-frontmatter.mjs";
|
|
|
|
/**
|
|
* Property / fuzz tests for the inline YAML parser (parseSimpleYaml via
|
|
* parseSkillFrontmatter) and frontmatter extraction.
|
|
* Goal: no crashes or unhandled exceptions from any input.
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Parse YAML frontmatter string; returns result or Error. */
|
|
function safeParse(yaml: string): ReturnType<typeof parseSkillFrontmatter> | Error {
|
|
try {
|
|
return parseSkillFrontmatter(yaml);
|
|
} catch (err) {
|
|
return err as Error;
|
|
}
|
|
}
|
|
|
|
/** Must either return a valid result or throw with a message. */
|
|
function expectNoUnhandled(yaml: string) {
|
|
const result = safeParse(yaml);
|
|
if (result instanceof Error) {
|
|
expect(result.message).toBeTruthy();
|
|
return;
|
|
}
|
|
expect(result).toHaveProperty("name");
|
|
expect(result).toHaveProperty("description");
|
|
expect(result).toHaveProperty("metadata");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// extractFrontmatter edge cases
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: extractFrontmatter", () => {
|
|
test("empty string returns empty yaml and body", () => {
|
|
const { yaml, body } = extractFrontmatter("");
|
|
expect(yaml).toBe("");
|
|
expect(body).toBe("");
|
|
});
|
|
|
|
test("no frontmatter delimiters returns everything as body", () => {
|
|
const input = "Just some markdown\nwith no frontmatter";
|
|
const { yaml, body } = extractFrontmatter(input);
|
|
expect(yaml).toBe("");
|
|
expect(body).toBe(input);
|
|
});
|
|
|
|
test("BOM is stripped", () => {
|
|
const input = "\uFEFF---\nname: test\n---\nbody";
|
|
const { yaml, body } = extractFrontmatter(input);
|
|
expect(yaml).toBe("name: test");
|
|
expect(body).toBe("body");
|
|
});
|
|
|
|
test("Windows line endings (CRLF)", () => {
|
|
const input = "---\r\nname: test\r\n---\r\nbody";
|
|
const { yaml, body } = extractFrontmatter(input);
|
|
expect(yaml).toBe("name: test");
|
|
expect(body).toBe("body");
|
|
});
|
|
|
|
test("frontmatter with only dashes (empty YAML)", () => {
|
|
const input = "---\n\n---\nbody";
|
|
const { yaml, body } = extractFrontmatter(input);
|
|
expect(yaml).toBe("");
|
|
expect(body).toBe("body");
|
|
});
|
|
|
|
test("multiple --- blocks returns first match", () => {
|
|
const input = "---\nfirst: 1\n---\nmiddle\n---\nsecond: 2\n---\nend";
|
|
const { yaml } = extractFrontmatter(input);
|
|
expect(yaml).toBe("first: 1");
|
|
});
|
|
|
|
test("--- not at start is not frontmatter", () => {
|
|
const input = "some text\n---\nname: test\n---\nbody";
|
|
const { yaml, body } = extractFrontmatter(input);
|
|
expect(yaml).toBe("");
|
|
expect(body).toBe(input);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// parseSkillFrontmatter: empty / missing input
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: parseSkillFrontmatter empty inputs", () => {
|
|
test("empty string", () => {
|
|
const result = parseSkillFrontmatter("");
|
|
expect(result).toEqual({ name: "", description: "", summary: "", metadata: {}, validate: [], chainTo: [] });
|
|
});
|
|
|
|
test("whitespace only", () => {
|
|
const result = parseSkillFrontmatter(" \n \n ");
|
|
expect(result).toEqual({ name: "", description: "", summary: "", metadata: {}, validate: [], chainTo: [] });
|
|
});
|
|
|
|
test("null/undefined don't crash", () => {
|
|
expect(parseSkillFrontmatter(null as any)).toEqual({ name: "", description: "", summary: "", metadata: {}, validate: [], chainTo: [] });
|
|
expect(parseSkillFrontmatter(undefined as any)).toEqual({ name: "", description: "", summary: "", metadata: {}, validate: [], chainTo: [] });
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Bare values (per CLAUDE.md: bare null/true/false are strings, not JS types)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: bare value handling", () => {
|
|
test("bare null is string 'null'", () => {
|
|
const result = safeParse("name: null");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("null");
|
|
}
|
|
});
|
|
|
|
test("bare true is string 'true'", () => {
|
|
const result = safeParse("name: true");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("true");
|
|
}
|
|
});
|
|
|
|
test("bare false is string 'false'", () => {
|
|
const result = safeParse("name: false");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("false");
|
|
}
|
|
});
|
|
|
|
test("numeric values coerced to empty string for name field", () => {
|
|
// parseSkillFrontmatter coerces non-string values to "" for name
|
|
const result = safeParse("name: 42");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tab indentation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: tab indentation", () => {
|
|
test("tab at start of line throws", () => {
|
|
const yaml = "name: test\n\tdescription: bad";
|
|
const result = safeParse(yaml);
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toContain("tab");
|
|
});
|
|
|
|
test("tab in value is fine (not indentation)", () => {
|
|
const result = safeParse("name: hello\tworld");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("hello\tworld");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Unclosed brackets (per CLAUDE.md: treated as scalar strings)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: unclosed brackets", () => {
|
|
test("unclosed [ in value is scalar string", () => {
|
|
const result = safeParse("name: [unclosed");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("[unclosed");
|
|
}
|
|
});
|
|
|
|
test("unclosed [ with content", () => {
|
|
const result = safeParse("name: [a, b, c");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("[a, b, c");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Quoted strings
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: quoted strings", () => {
|
|
test("single-quoted string", () => {
|
|
const result = safeParse("name: 'hello world'");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("hello world");
|
|
}
|
|
});
|
|
|
|
test("double-quoted string", () => {
|
|
const result = safeParse('name: "hello world"');
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("hello world");
|
|
}
|
|
});
|
|
|
|
test("unterminated single quote throws", () => {
|
|
const result = safeParse("name: 'hello");
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toContain("unterminated");
|
|
});
|
|
|
|
test("unterminated double quote throws", () => {
|
|
const result = safeParse('name: "hello');
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toContain("unterminated");
|
|
});
|
|
|
|
test("empty quoted strings", () => {
|
|
const r1 = safeParse("name: ''");
|
|
expect(r1).not.toBeInstanceOf(Error);
|
|
if (!(r1 instanceof Error)) expect(r1.name).toBe("");
|
|
|
|
const r2 = safeParse('name: ""');
|
|
expect(r2).not.toBeInstanceOf(Error);
|
|
if (!(r2 instanceof Error)) expect(r2.name).toBe("");
|
|
});
|
|
|
|
test("quotes inside different quote type", () => {
|
|
const result = safeParse(`name: "it's a test"`);
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("it's a test");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Inline arrays
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: inline arrays", () => {
|
|
test("empty array", () => {
|
|
const result = safeParse("metadata:\n pathPatterns: []");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.metadata).toEqual({ pathPatterns: [] });
|
|
}
|
|
});
|
|
|
|
test("array with trailing comma throws (empty entry)", () => {
|
|
const result = safeParse("metadata:\n pathPatterns: [a, b, ]");
|
|
expect(result).toBeInstanceOf(Error);
|
|
});
|
|
|
|
test("array with leading comma throws (empty entry)", () => {
|
|
const result = safeParse("metadata:\n pathPatterns: [, a, b]");
|
|
expect(result).toBeInstanceOf(Error);
|
|
});
|
|
|
|
test("nested inline arrays are split by top-level commas", () => {
|
|
// The inline parser doesn't recursively parse nested brackets within
|
|
// the top-level array — brackets inside items are treated as scalars
|
|
const result = safeParse("metadata:\n data: [[1, 2], [3, 4]]");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.metadata).toEqual({ data: ["[1", "2]", "[3", "4]"] });
|
|
}
|
|
});
|
|
|
|
test("array with quoted commas", () => {
|
|
const result = safeParse(`metadata:\n pathPatterns: ["a,b", "c,d"]`);
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.metadata).toEqual({ pathPatterns: ["a,b", "c,d"] });
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Block arrays (YAML list syntax)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: block arrays", () => {
|
|
test("standard block array", () => {
|
|
const yaml = "metadata:\n pathPatterns:\n - '*.ts'\n - '*.tsx'";
|
|
const result = safeParse(yaml);
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.metadata).toEqual({ pathPatterns: ["*.ts", "*.tsx"] });
|
|
}
|
|
});
|
|
|
|
test("block array with empty dash items", () => {
|
|
const yaml = "metadata:\n pathPatterns:\n -\n -";
|
|
const result = safeParse(yaml);
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.metadata).toEqual({ pathPatterns: ["", ""] });
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Comments
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: comments", () => {
|
|
test("comment-only document is empty", () => {
|
|
const result = safeParse("# just a comment\n# another one");
|
|
expect(result).toEqual({ name: "", description: "", summary: "", metadata: {}, validate: [], chainTo: [] });
|
|
});
|
|
|
|
test("inline comments are NOT stripped (becomes part of value)", () => {
|
|
// The inline YAML parser doesn't strip inline comments
|
|
const result = safeParse("name: hello # world");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("hello # world");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Indentation edge cases
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: indentation", () => {
|
|
test("inconsistent indentation throws", () => {
|
|
const yaml = "metadata:\n priority: 5\n pathPatterns: []";
|
|
const result = safeParse(yaml);
|
|
expect(result).toBeInstanceOf(Error);
|
|
});
|
|
|
|
test("deeply nested structure", () => {
|
|
const yaml = [
|
|
"metadata:",
|
|
" pathPatterns:",
|
|
" - '*.ts'",
|
|
" priority: 10",
|
|
].join("\n");
|
|
const result = safeParse(yaml);
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.metadata).toEqual({ pathPatterns: ["*.ts"], priority: 10 });
|
|
}
|
|
});
|
|
|
|
test("leading spaces on top-level key throws", () => {
|
|
const result = safeParse(" name: test");
|
|
expect(result).toBeInstanceOf(Error);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Empty document / whitespace variations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: empty and whitespace documents", () => {
|
|
test("newline only", () => {
|
|
expectNoUnhandled("\n");
|
|
});
|
|
|
|
test("multiple newlines", () => {
|
|
expectNoUnhandled("\n\n\n\n");
|
|
});
|
|
|
|
test("spaces only", () => {
|
|
expectNoUnhandled(" ");
|
|
});
|
|
|
|
test("mixed whitespace no content", () => {
|
|
expectNoUnhandled(" \n \n ");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Colons in values
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: colons in values", () => {
|
|
test("colon in value (URL)", () => {
|
|
const result = safeParse("name: https://example.com");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("https://example.com");
|
|
}
|
|
});
|
|
|
|
test("colon at end of key with empty value", () => {
|
|
const result = safeParse("name:");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Unicode in YAML
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: unicode in YAML", () => {
|
|
test("unicode key name", () => {
|
|
expectNoUnhandled("名前: テスト");
|
|
});
|
|
|
|
test("emoji in values", () => {
|
|
expectNoUnhandled("name: 🚀 rocket");
|
|
});
|
|
|
|
test("RTL characters", () => {
|
|
expectNoUnhandled("name: مرحبا");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stress: random YAML-like strings
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: random YAML-like strings", () => {
|
|
const chars = "abcABC012: \n-[]'\"#,\t{}|>!&*?@`%";
|
|
|
|
function randomYaml(len: number, seed: number): string {
|
|
let result = "";
|
|
let s = seed;
|
|
for (let i = 0; i < len; i++) {
|
|
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
|
result += chars[s % chars.length];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
test("200 random YAML strings don't cause unhandled exceptions", () => {
|
|
for (let i = 0; i < 200; i++) {
|
|
const len = (i % 80) + 1;
|
|
const yaml = randomYaml(len, i * 31337);
|
|
try {
|
|
parseSkillFrontmatter(yaml);
|
|
} catch (err) {
|
|
// Must be a descriptive error, not a raw crash
|
|
expect(err).toBeInstanceOf(Error);
|
|
expect((err as Error).message).toBeTruthy();
|
|
}
|
|
}
|
|
});
|
|
|
|
test("random strings through extractFrontmatter + parse pipeline", () => {
|
|
for (let i = 0; i < 100; i++) {
|
|
const len = (i % 120) + 10;
|
|
const raw = `---\n${randomYaml(len, i * 7919)}\n---\nbody`;
|
|
try {
|
|
const { yaml } = extractFrontmatter(raw);
|
|
if (yaml) parseSkillFrontmatter(yaml);
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(Error);
|
|
expect((err as Error).message).toBeTruthy();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Realistic full frontmatter round-trips
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("fuzz: realistic frontmatter round-trips", () => {
|
|
test("full skill frontmatter parses correctly", () => {
|
|
const yaml = [
|
|
"name: Next.js",
|
|
"description: Framework for React",
|
|
"metadata:",
|
|
" priority: 10",
|
|
" pathPatterns:",
|
|
" - 'next.config.*'",
|
|
" - 'app/**/*.tsx'",
|
|
" - 'pages/**/*.tsx'",
|
|
" bashPatterns:",
|
|
" - 'npx next'",
|
|
" - 'next dev'",
|
|
].join("\n");
|
|
|
|
const result = safeParse(yaml);
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("Next.js");
|
|
expect(result.description).toBe("Framework for React");
|
|
expect(result.metadata).toEqual({
|
|
priority: 10,
|
|
pathPatterns: ["next.config.*", "app/**/*.tsx", "pages/**/*.tsx"],
|
|
bashPatterns: ["npx next", "next dev"],
|
|
});
|
|
}
|
|
});
|
|
|
|
test("frontmatter with only name", () => {
|
|
const result = safeParse("name: minimal");
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.name).toBe("minimal");
|
|
expect(result.metadata).toEqual({});
|
|
}
|
|
});
|
|
|
|
test("metadata is not object (array) falls back to empty", () => {
|
|
const yaml = "name: test\nmetadata:\n - item1\n - item2";
|
|
const result = safeParse(yaml);
|
|
expect(result).not.toBeInstanceOf(Error);
|
|
if (!(result instanceof Error)) {
|
|
expect(result.metadata).toEqual({});
|
|
}
|
|
});
|
|
});
|