Files

717 lines
25 KiB
TypeScript

import { describe, test, expect } from "bun:test";
import { readdir, readFile, stat, mkdir, writeFile, rm } from "node:fs/promises";
import { join, resolve } from "node:path";
import { detectReDoS } from "../scripts/validate";
const ROOT = resolve(import.meta.dirname, "..");
const SYNTHETIC_TEST_SKILL_PREFIX = "zzz-test-";
async function exists(path: string): Promise<boolean> {
try {
await stat(path);
return true;
} catch {
return false;
}
}
function isSyntheticTestSkillName(name: string): boolean {
return name.startsWith(SYNTHETIC_TEST_SKILL_PREFIX);
}
function issueMentionsSyntheticTestSkill(issue: {
file?: string;
message?: string;
hint?: string;
}): boolean {
return [issue.file, issue.message, issue.hint].some(
(value) => typeof value === "string" && value.includes(SYNTHETIC_TEST_SKILL_PREFIX),
);
}
describe.serial("validate.ts", () => {
test("repo has no non-synthetic validation errors", async () => {
const { report } = await runValidateJson();
const unexpectedErrors = report.issues.filter(
(issue: any) => issue.severity === "error" && !issueMentionsSyntheticTestSkill(issue),
);
if (unexpectedErrors.length > 0) {
console.error(JSON.stringify(unexpectedErrors, null, 2));
}
expect(unexpectedErrors).toEqual([]);
}, 30_000);
test("every graph skill ref resolves to an existing skill directory", async () => {
const graphPath = join(ROOT, "vercel.md");
const graph = await readFile(graphPath, "utf-8");
const refs = [...new Set(
[...graph.matchAll(/⤳\s*skill:\s*([a-z][a-z0-9-]*)/g)].map((m) => m[1]),
)];
expect(refs.length).toBeGreaterThan(0);
const missing: string[] = [];
for (const name of refs) {
if (!(await exists(join(ROOT, "skills", name, "SKILL.md")))) {
missing.push(name);
}
}
expect(missing).toEqual([]);
});
test("JSON report includes per-check timing metrics", async () => {
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
await proc.exited;
const report = JSON.parse(stdout);
expect(report.metrics).toBeArray();
expect(report.metrics.length).toBeGreaterThan(0);
for (const m of report.metrics) {
expect(typeof m.name).toBe("string");
expect(typeof m.durationMs).toBe("number");
}
}, 30_000);
test("JSON report includes checkResults array with correct shape", async () => {
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
await proc.exited;
const report = JSON.parse(stdout);
expect(report.checkResults).toBeArray();
expect(report.checkResults.length).toBeGreaterThan(0);
for (const cr of report.checkResults) {
expect(typeof cr.name).toBe("string");
expect(typeof cr.label).toBe("string");
expect(["pass", "fail", "warn"]).toContain(cr.status);
expect(typeof cr.durationMs).toBe("number");
expect(typeof cr.errorCount).toBe("number");
expect(typeof cr.warningCount).toBe("number");
}
// checkResults count should match metrics count
expect(report.checkResults.length).toBe(report.metrics.length);
}, 30_000);
test("every issue has a check field matching its source check", async () => {
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
await proc.exited;
const report = JSON.parse(stdout);
const validCheckNames = new Set(report.checkResults.map((cr: any) => cr.name));
for (const issue of report.issues) {
expect(typeof issue.check).toBe("string");
expect(issue.check.length).toBeGreaterThan(0);
expect(validCheckNames.has(issue.check)).toBe(true);
}
}, 30_000);
test("every issue has a non-empty hint field", async () => {
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
await proc.exited;
const report = JSON.parse(stdout);
for (const issue of report.issues) {
expect(issue.hint).toBeDefined();
expect(typeof issue.hint).toBe("string");
expect(issue.hint.length).toBeGreaterThan(0);
}
}, 30_000);
test("current state has 0 orphan skills", async () => {
const graphPath = join(ROOT, "vercel.md");
const graph = await readFile(graphPath, "utf-8");
const referencedSkills = new Set(
[...graph.matchAll(/⤳\s*skill:\s*([a-z][a-z0-9-]*)/g)].map((m) => m[1]),
);
const skillsDir = join(ROOT, "skills");
const dirs = await readdir(skillsDir);
const orphans: string[] = [];
for (const dir of dirs.sort()) {
if (isSyntheticTestSkillName(dir)) continue;
if (!(await exists(join(skillsDir, dir, "SKILL.md")))) continue;
if (!referencedSkills.has(dir)) orphans.push(dir);
}
expect(orphans).toEqual([]);
});
test("introducing an orphan skill causes validation failure", async () => {
const orphanDir = join(ROOT, "skills", "fake-orphan-test-skill");
try {
await mkdir(orphanDir, { recursive: true });
await writeFile(
join(orphanDir, "SKILL.md"),
"---\nname: fake-orphan-test-skill\ndescription: test orphan\n---\nTest skill.\n",
);
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
const code = await proc.exited;
expect(code).not.toBe(0);
const report = JSON.parse(stdout);
expect(report.orphanSkills).toContain("fake-orphan-test-skill");
const orphanIssue = report.issues.find((i: any) => i.code === "ORPHAN_SKILL" && i.message.includes("fake-orphan-test-skill"));
expect(orphanIssue).toBeDefined();
expect(orphanIssue.check).toBe("orphanSkills");
} finally {
await rm(orphanDir, { recursive: true, force: true });
}
}, 30_000);
test("JSON report includes orphanSkills field", async () => {
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
await proc.exited;
const report = JSON.parse(stdout);
expect(report.orphanSkills).toBeArray();
}, 30_000);
test("current commands pass command structure check", async () => {
const commandsDir = join(ROOT, "commands");
const cmdFiles = (await readdir(commandsDir))
.filter((f: string) => f.endsWith(".md") && !f.startsWith("_"));
expect(cmdFiles.length).toBeGreaterThan(0);
for (const file of cmdFiles) {
const content = await readFile(join(commandsDir, file), "utf-8");
// Must have Preflight and Verification sections
expect(content).toMatch(/^#{2,3}\s+.*Preflight/im);
expect(content).toMatch(/^#{2,3}\s+.*Verification/im);
// Must contain at least one backtick-fenced vercel CLI example
const codeBlocks = [...content.matchAll(/```[a-z]*\n([\s\S]*?)```/g)];
const hasVercelCli = codeBlocks.some((m) => /\bvercel\b/.test(m[1]));
expect(hasVercelCli).toBe(true);
}
});
test("command missing Verification section fails validation", async () => {
const tmpFile = join(ROOT, "commands", "test-broken-cmd.md");
try {
await writeFile(
tmpFile,
`---\ndescription: Test command missing Verification\n---\n\n# Test Command\n\n## Preflight\n\nCheck stuff.\n\n## Commands\n\n\`\`\`bash\nvercel ls\n\`\`\`\n\n## Summary\n\nDone.\n`,
);
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
const code = await proc.exited;
expect(code).not.toBe(0);
const report = JSON.parse(stdout);
const issue = report.issues.find(
(i: any) =>
i.code === "CMD_MISSING_CRITICAL_SECTIONS" &&
i.message.includes("test-broken-cmd.md") &&
i.message.includes("Verification"),
);
expect(issue).toBeDefined();
expect(issue.file).toBe("commands/test-broken-cmd.md");
expect(issue.hint).toBeDefined();
expect(issue.hint.length).toBeGreaterThan(0);
expect(issue.check).toBe("commandConventions");
} finally {
await rm(tmpFile, { force: true });
}
}, 30_000);
test("SKILL.md with 'vercel logs drain ls' in code fence fails CLI_BANNED_PATTERN", async () => {
const tmpDir = join(ROOT, "skills", "fake-banned-test-skill");
try {
await mkdir(tmpDir, { recursive: true });
await writeFile(
join(tmpDir, "SKILL.md"),
"---\nname: fake-banned-test-skill\ndescription: test banned\n---\n\n# Test\n\n```bash\nvercel logs drain ls\n```\n",
);
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
const code = await proc.exited;
expect(code).not.toBe(0);
const report = JSON.parse(stdout);
const issue = report.issues.find(
(i: any) => i.code === "CLI_BANNED_PATTERN" && i.file?.includes("fake-banned-test-skill"),
);
expect(issue).toBeDefined();
expect(issue.hint).toBeDefined();
expect(issue.line).toBeGreaterThan(0);
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
}, 30_000);
test("command.md with 'vercel integration deploy' in code fence fails CLI_BANNED_PATTERN", async () => {
const tmpFile = join(ROOT, "commands", "test-banned-cmd.md");
try {
await writeFile(
tmpFile,
"---\ndescription: Test banned command\n---\n\n# Test\n\n## Preflight\n\nCheck.\n\n## Verification\n\nOK.\n\n## Commands\n\n```bash\nvercel integration deploy\n```\n",
);
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
const code = await proc.exited;
expect(code).not.toBe(0);
const report = JSON.parse(stdout);
const issue = report.issues.find(
(i: any) => i.code === "CLI_BANNED_PATTERN" && i.file?.includes("test-banned-cmd.md"),
);
expect(issue).toBeDefined();
expect(issue.hint).toBeDefined();
} finally {
await rm(tmpFile, { force: true });
}
}, 30_000);
test("current repo has 0 CLI_BANNED_PATTERN issues", async () => {
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
await proc.exited;
const report = JSON.parse(stdout);
const bannedIssues = report.issues.filter((i: any) => i.code === "CLI_BANNED_PATTERN");
expect(bannedIssues).toEqual([]);
}, 30_000);
test("graph skill ref errors include line numbers", async () => {
// Parse the graph directly and verify that lineOf produces numbers for skill refs
const graphPath = join(ROOT, "vercel.md");
const graph = await readFile(graphPath, "utf-8");
const refs = [...graph.matchAll(/⤳\s*skill:\s*([a-z][a-z0-9-]*)/g)];
expect(refs.length).toBeGreaterThan(0);
for (const m of refs) {
const idx = graph.indexOf(m[0]);
const line = graph.slice(0, idx).split("\n").length;
expect(line).toBeGreaterThan(0);
expect(typeof line).toBe("number");
}
});
});
// ---------------------------------------------------------------------------
// Focused frontmatter validation tests
// ---------------------------------------------------------------------------
/** Helper: run validate in JSON mode, return parsed report + exit code */
async function runValidateJson(): Promise<{ code: number; report: any }> {
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "validate.ts"), "--format", "json", "--coverage", "skip"],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await new Response(proc.stdout).text();
const code = await proc.exited;
return { code, report: JSON.parse(stdout) };
}
/** Helper: create a temp skill dir with given SKILL.md content, run fn, then clean up */
async function withTempSkill(name: string, skillMd: string, fn: () => Promise<void>): Promise<void> {
const dir = join(ROOT, "skills", name);
const skillPath = join(dir, "SKILL.md");
const hadOriginal = await exists(skillPath);
const originalSkillMd = hadOriginal ? await readFile(skillPath, "utf-8") : null;
try {
await mkdir(dir, { recursive: true });
await writeFile(skillPath, skillMd);
await fn();
} finally {
if (hadOriginal && originalSkillMd !== null) {
await writeFile(skillPath, originalSkillMd);
} else {
await rm(dir, { recursive: true, force: true });
}
}
}
describe.serial("validate.ts — focused frontmatter validation", () => {
test("broken YAML frontmatter produces FM_INVALID_YAML", async () => {
await withTempSkill("zzz-test-broken-yaml", [
"---",
"name: broken",
"description: bad yaml",
"\tmetadata: foo",
"---",
"# Broken",
].join("\n"), async () => {
const { code, report } = await runValidateJson();
expect(code).not.toBe(0);
const issue = report.issues.find(
(i: any) => i.code === "FM_INVALID_YAML" && i.message.includes("zzz-test-broken-yaml"),
);
expect(issue).toBeDefined();
expect(issue.hint).toBeDefined();
});
}, 30_000);
test("string pathPatterns produces FM_PATHPATTERNS_TYPE", async () => {
await withTempSkill("zzz-test-string-filepattern", [
"---",
"name: string-fp",
"description: pathPatterns is a bare string",
"metadata:",
" pathPatterns: 'src/**/*.ts'",
"---",
"# String pathPatterns",
].join("\n"), async () => {
const { code, report } = await runValidateJson();
expect(code).not.toBe(0);
const issue = report.issues.find(
(i: any) => i.code === "FM_PATHPATTERNS_TYPE" && i.message.includes("zzz-test-string-filepattern"),
);
expect(issue).toBeDefined();
expect(issue.hint).toMatch(/list/i);
});
}, 30_000);
test("invalid bashPatterns regex produces PATTERN_BASH_COMPILE", async () => {
await withTempSkill("zzz-test-bad-bash-regex", [
"---",
"name: bad-regex",
"description: bashPatterns with invalid regex",
"metadata:",
" bashPatterns:",
" - '[unclosed'",
" pathPatterns:",
" - '**/*.ts'",
"---",
"# Bad regex",
].join("\n"), async () => {
const { code, report } = await runValidateJson();
expect(code).not.toBe(0);
const issue = report.issues.find(
(i: any) => i.code === "PATTERN_BASH_COMPILE" && i.message.includes("zzz-test-bad-bash-regex"),
);
expect(issue).toBeDefined();
expect(issue.hint).toMatch(/regex/i);
});
}, 30_000);
test("SKILL.md with pathPatterns: [42] produces warning-level filtering (no crash)", async () => {
await withTempSkill("zzz-test-nonstring-fp", [
"---",
"name: nonstring-fp",
"description: pathPatterns has a number",
"metadata:",
" pathPatterns:",
" - 42",
" bashPatterns:",
" - '\\\\bvercel\\\\b'",
"---",
"# Non-string pathPatterns",
].join("\n"), async () => {
const { report } = await runValidateJson();
// The number entry gets filtered out, leaving no triggers → SKILL_NO_TRIGGERS
// but should NOT crash
const crashIssues = report.issues.filter(
(i: any) => i.message?.includes("zzz-test-nonstring-fp") && i.code === "FM_INVALID_YAML",
);
expect(crashIssues).toEqual([]);
});
}, 30_000);
test("SKILL.md with pathPatterns: [''] produces warning-level filtering (no crash)", async () => {
await withTempSkill("zzz-test-empty-fp", [
"---",
"name: empty-fp",
"description: pathPatterns has an empty string",
"metadata:",
" pathPatterns:",
" - ''",
" bashPatterns:",
" - '\\\\bvercel\\\\b'",
"---",
"# Empty pathPatterns",
].join("\n"), async () => {
const { report } = await runValidateJson();
const crashIssues = report.issues.filter(
(i: any) => i.message?.includes("zzz-test-empty-fp") && i.code === "FM_INVALID_YAML",
);
expect(crashIssues).toEqual([]);
});
}, 30_000);
test("metadata.name does not mask missing top-level name (FM_NO_NAME regression)", async () => {
await withTempSkill("zzz-test-meta-name-mask", [
"---",
"description: has metadata.name but no top-level name",
"metadata:",
" name: sneaky-name",
" pathPatterns:",
" - '**/*.ts'",
"---",
"# No top-level name",
].join("\n"), async () => {
const { code, report } = await runValidateJson();
expect(code).not.toBe(0);
const issue = report.issues.find(
(i: any) => i.code === "FM_NO_NAME" && i.message.includes("zzz-test-meta-name-mask"),
);
expect(issue).toBeDefined();
});
}, 30_000);
test("string pathPatterns produces FM_PATHPATTERNS_TYPE via structured warningDetails", async () => {
await withTempSkill("zzz-test-structured-fp", [
"---",
"name: structured-fp",
"description: pathPatterns is a bare string (structured test)",
"metadata:",
" pathPatterns: 'src/**/*.ts'",
"---",
"# Structured pathPatterns test",
].join("\n"), async () => {
const { code, report } = await runValidateJson();
expect(code).not.toBe(0);
const issue = report.issues.find(
(i: any) => i.code === "FM_PATHPATTERNS_TYPE" && i.message.includes("zzz-test-structured-fp"),
);
expect(issue).toBeDefined();
// Verify the message includes ", got string" (from structured detail code path)
expect(issue.message).toMatch(/got string/);
expect(issue.hint).toBeDefined();
});
}, 30_000);
test("clean SKILL.md with valid patterns produces no errors for that skill", async () => {
await withTempSkill("zzz-test-clean-skill", [
"---",
"name: clean-skill",
"description: A perfectly valid test skill",
"metadata:",
" docs:",
" - 'https://example.com/clean-skill'",
" pathPatterns:",
" - '**/*.clean-skill-test'",
" bashPatterns:",
" - '\\bvercel-plugin-clean-skill\\b'",
" priority: 5",
"---",
"# Clean Skill",
"",
"This skill is well-formed.",
].join("\n"), async () => {
const { report } = await runValidateJson();
// The only issue for this skill should be ORPHAN_SKILL (no graph ref) — no frontmatter errors
const fmIssues = report.issues.filter(
(i: any) =>
i.message?.includes("zzz-test-clean-skill") &&
i.code !== "ORPHAN_SKILL" &&
i.code !== "SKILL_NO_TRIGGERS" &&
i.code !== "CATALOG_STALE",
);
expect(fmIssues).toEqual([]);
});
}, 30_000);
test("bashPatterns with nested quantifiers produces PATTERN_BASH_REDOS", async () => {
await withTempSkill("zzz-test-redos-pattern", [
"---",
"name: redos-pattern",
"description: bashPatterns with catastrophic backtracking",
"metadata:",
" pathPatterns:",
" - '**/*.ts'",
" bashPatterns:",
" - '(a+)+'",
"---",
"# ReDoS pattern",
].join("\n"), async () => {
const { code, report } = await runValidateJson();
expect(code).not.toBe(0);
const issue = report.issues.find(
(i: any) => i.code === "PATTERN_BASH_REDOS" && i.message.includes("zzz-test-redos-pattern"),
);
expect(issue).toBeDefined();
expect(issue.message).toMatch(/nested quantifiers/);
expect(issue.hint).toMatch(/rewrite/i);
});
}, 30_000);
});
// ---------------------------------------------------------------------------
// Catalog staleness tests
// ---------------------------------------------------------------------------
describe.serial("validate.ts — catalog staleness", () => {
test("generate-catalog.ts runs successfully", async () => {
const proc = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "generate-catalog.ts")],
{ stdout: "pipe", stderr: "pipe" },
);
const code = await proc.exited;
if (code !== 0) {
const stderr = await new Response(proc.stderr).text();
console.error(stderr);
}
expect(code).toBe(0);
expect(await exists(join(ROOT, "generated", "skill-catalog.md"))).toBe(true);
}, 30_000);
test("generated catalog lists every non-synthetic skill from skills/ directory", async () => {
// First regenerate to ensure freshness
const gen = Bun.spawn(
["bun", "run", join(ROOT, "scripts", "generate-catalog.ts")],
{ stdout: "pipe", stderr: "pipe" },
);
await gen.exited;
const catalog = await readFile(join(ROOT, "generated", "skill-catalog.md"), "utf-8");
const skillsDir = join(ROOT, "skills");
const dirs = await readdir(skillsDir);
const skillDirs: string[] = [];
for (const dir of dirs) {
if (isSyntheticTestSkillName(dir)) continue;
if (await exists(join(skillsDir, dir, "SKILL.md"))) {
skillDirs.push(dir);
}
}
// Every skill directory should appear in the catalog
for (const dir of skillDirs) {
expect(catalog).toContain(`\`${dir}\``);
}
}, 30_000);
test("generated catalog contains overlap matrix sections", async () => {
const catalog = await readFile(join(ROOT, "generated", "skill-catalog.md"), "utf-8");
expect(catalog).toContain("## Path Overlap Matrix");
expect(catalog).toContain("## Bash Overlap Matrix");
expect(catalog).toContain("## Skills by Priority");
});
test("stale catalog (missing skill) triggers CATALOG_STALE in validation", async () => {
// Create a temp skill that won't be in the existing catalog
await withTempSkill("zzz-test-catalog-stale", [
"---",
"name: catalog-stale-test",
"description: test catalog staleness detection",
"metadata:",
" pathPatterns:",
" - '**/*.test-catalog-stale'",
"---",
"# Stale catalog test",
].join("\n"), async () => {
// Do NOT regenerate the catalog — it should be stale now
const { code, report } = await runValidateJson();
expect(code).not.toBe(0);
const issue = report.issues.find(
(i: any) => i.code === "CATALOG_STALE" && i.message.includes("zzz-test-catalog-stale"),
);
expect(issue).toBeDefined();
expect(issue.hint).toMatch(/generate-catalog/);
});
}, 30_000);
test("catalog overlap matrix detects vercel.json contention", async () => {
const catalog = await readFile(join(ROOT, "generated", "skill-catalog.md"), "utf-8");
// vercel.json should have multiple competing skills
const vercelJsonRow = catalog.match(/^\| `vercel\.json` \| (.+) \|$/m);
expect(vercelJsonRow).not.toBeNull();
// Should list at least 3 skills competing on vercel.json
const skills = vercelJsonRow![1].split(",");
expect(skills.length).toBeGreaterThanOrEqual(3);
});
});
// ---------------------------------------------------------------------------
// detectReDoS unit tests
// ---------------------------------------------------------------------------
describe.serial("detectReDoS", () => {
test("flags (a+)+ as nested quantifier", () => {
expect(detectReDoS("(a+)+")).not.toBeNull();
});
test("flags (.*)* as nested quantifier", () => {
expect(detectReDoS("(.*)*")).not.toBeNull();
});
test("flags ([^/]+)+ as nested quantifier", () => {
expect(detectReDoS("([^/]+)+")).not.toBeNull();
});
test("flags (\\w+)* as nested quantifier", () => {
expect(detectReDoS("(\\w+)*")).not.toBeNull();
});
test("flags (a+){2,} as nested quantifier", () => {
expect(detectReDoS("(a+){2,}")).not.toBeNull();
});
test("allows simple \\bvercel\\b", () => {
expect(detectReDoS("\\bvercel\\b")).toBeNull();
});
test("allows non-nested [a-z]+", () => {
expect(detectReDoS("[a-z]+")).toBeNull();
});
test("allows simple alternation without quantifier", () => {
expect(detectReDoS("(foo|bar)")).toBeNull();
});
test("allows quantified group without inner quantifier", () => {
expect(detectReDoS("(abc)+")).toBeNull();
});
test("flags quantified backreference \\1+", () => {
expect(detectReDoS("(a)\\1+")).not.toBeNull();
});
});