Files
prompts.chat/src/__tests__/lib/skill-files.test.ts
T
2026-02-03 12:46:35 +03:00

529 lines
17 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
parseSkillFiles,
serializeSkillFiles,
getLanguageFromFilename,
validateFilename,
isValidKebabCase,
parseSkillFrontmatter,
validateSkillFrontmatter,
generateSkillContentWithFrontmatter,
suggestFilename,
DEFAULT_SKILL_FILE,
DEFAULT_SKILL_CONTENT,
type SkillFile,
} from "@/lib/skill-files";
describe("parseSkillFiles", () => {
it("returns default content for empty string", () => {
const result = parseSkillFiles("");
expect(result).toHaveLength(1);
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
expect(result[0].content).toBe(DEFAULT_SKILL_CONTENT);
});
it("returns default content for whitespace-only string", () => {
const result = parseSkillFiles(" \n\t ");
expect(result).toHaveLength(1);
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
});
it("parses single file content without separators", () => {
const content = "# My Skill\n\nSome content here";
const result = parseSkillFiles(content);
expect(result).toHaveLength(1);
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
expect(result[0].content).toBe(content);
});
it("parses multiple files with separators", () => {
const content = "# Main Skill\n\x1FFILE:helper.ts\x1E\nconst x = 1;\n\x1FFILE:config.json\x1E\n{}";
const result = parseSkillFiles(content);
expect(result).toHaveLength(3);
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
expect(result[0].content).toBe("# Main Skill");
expect(result[1].filename).toBe("helper.ts");
expect(result[1].content).toBe("const x = 1;");
expect(result[2].filename).toBe("config.json");
expect(result[2].content).toBe("{}");
});
it("ignores SKILL.md in file separators (already handled as first file)", () => {
const content = "# Main\n\x1FFILE:SKILL.md\x1E\nshould be ignored";
const result = parseSkillFiles(content);
expect(result).toHaveLength(1);
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
});
it("handles empty file content after separator", () => {
const content = "# Main\n\x1FFILE:empty.ts\x1E\n";
const result = parseSkillFiles(content);
expect(result).toHaveLength(2);
expect(result[1].filename).toBe("empty.ts");
expect(result[1].content).toBe("");
});
});
describe("serializeSkillFiles", () => {
it("returns default content for empty array", () => {
expect(serializeSkillFiles([])).toBe(DEFAULT_SKILL_CONTENT);
});
it("serializes single SKILL.md file", () => {
const files: SkillFile[] = [
{ filename: DEFAULT_SKILL_FILE, content: "# My Skill" }
];
expect(serializeSkillFiles(files)).toBe("# My Skill");
});
it("serializes multiple files with separators", () => {
const files: SkillFile[] = [
{ filename: DEFAULT_SKILL_FILE, content: "# Main" },
{ filename: "helper.ts", content: "const x = 1;" },
];
const result = serializeSkillFiles(files);
expect(result).toContain("# Main");
expect(result).toContain("\x1FFILE:helper.ts\x1E");
expect(result).toContain("const x = 1;");
});
it("puts SKILL.md content first regardless of array order", () => {
const files: SkillFile[] = [
{ filename: "other.ts", content: "other" },
{ filename: DEFAULT_SKILL_FILE, content: "# Main" },
];
const result = serializeSkillFiles(files);
expect(result.startsWith("# Main")).toBe(true);
});
it("uses default content if SKILL.md is missing", () => {
const files: SkillFile[] = [
{ filename: "other.ts", content: "other" }
];
const result = serializeSkillFiles(files);
expect(result.startsWith(DEFAULT_SKILL_CONTENT)).toBe(true);
});
it("is reversible with parseSkillFiles", () => {
const files: SkillFile[] = [
{ filename: DEFAULT_SKILL_FILE, content: "# Main Skill" },
{ filename: "helper.ts", content: "export const x = 1;" },
{ filename: "config.json", content: '{ "key": "value" }' },
];
const serialized = serializeSkillFiles(files);
const parsed = parseSkillFiles(serialized);
expect(parsed).toHaveLength(3);
expect(parsed[0]).toEqual(files[0]);
expect(parsed[1]).toEqual(files[1]);
expect(parsed[2]).toEqual(files[2]);
});
});
describe("getLanguageFromFilename", () => {
describe("markdown files", () => {
it("detects .md files", () => {
expect(getLanguageFromFilename("README.md")).toBe("markdown");
expect(getLanguageFromFilename("SKILL.md")).toBe("markdown");
});
it("detects .mdx files", () => {
expect(getLanguageFromFilename("page.mdx")).toBe("markdown");
});
});
describe("JavaScript/TypeScript", () => {
it("detects JavaScript files", () => {
expect(getLanguageFromFilename("index.js")).toBe("javascript");
expect(getLanguageFromFilename("component.jsx")).toBe("javascript");
expect(getLanguageFromFilename("module.mjs")).toBe("javascript");
expect(getLanguageFromFilename("common.cjs")).toBe("javascript");
});
it("detects TypeScript files", () => {
expect(getLanguageFromFilename("index.ts")).toBe("typescript");
expect(getLanguageFromFilename("component.tsx")).toBe("typescript");
});
});
describe("web languages", () => {
it("detects HTML files", () => {
expect(getLanguageFromFilename("index.html")).toBe("html");
expect(getLanguageFromFilename("page.htm")).toBe("html");
});
it("detects CSS files", () => {
expect(getLanguageFromFilename("styles.css")).toBe("css");
expect(getLanguageFromFilename("styles.scss")).toBe("scss");
expect(getLanguageFromFilename("styles.less")).toBe("less");
});
});
describe("data formats", () => {
it("detects JSON files", () => {
expect(getLanguageFromFilename("config.json")).toBe("json");
});
it("detects YAML files", () => {
expect(getLanguageFromFilename("config.yaml")).toBe("yaml");
expect(getLanguageFromFilename("config.yml")).toBe("yaml");
});
it("detects XML files", () => {
expect(getLanguageFromFilename("data.xml")).toBe("xml");
});
it("detects TOML files", () => {
expect(getLanguageFromFilename("config.toml")).toBe("toml");
});
});
describe("programming languages", () => {
it("detects Python files", () => {
expect(getLanguageFromFilename("script.py")).toBe("python");
});
it("detects Go files", () => {
expect(getLanguageFromFilename("main.go")).toBe("go");
});
it("detects Rust files", () => {
expect(getLanguageFromFilename("lib.rs")).toBe("rust");
});
it("detects Java files", () => {
expect(getLanguageFromFilename("Main.java")).toBe("java");
});
it("detects C/C++ files", () => {
expect(getLanguageFromFilename("main.c")).toBe("c");
expect(getLanguageFromFilename("main.cpp")).toBe("cpp");
expect(getLanguageFromFilename("header.h")).toBe("c");
});
});
describe("shell and config files", () => {
it("detects shell files", () => {
expect(getLanguageFromFilename("script.sh")).toBe("shell");
expect(getLanguageFromFilename("script.bash")).toBe("shell");
expect(getLanguageFromFilename(".env")).toBe("shell");
});
it("detects config files", () => {
expect(getLanguageFromFilename("settings.ini")).toBe("ini");
expect(getLanguageFromFilename(".editorconfig")).toBe("ini");
});
});
describe("special filenames", () => {
it("detects Dockerfile", () => {
expect(getLanguageFromFilename("Dockerfile")).toBe("dockerfile");
expect(getLanguageFromFilename("dockerfile")).toBe("dockerfile");
expect(getLanguageFromFilename("Dockerfile.dev")).toBe("dockerfile");
});
it("detects Makefile", () => {
expect(getLanguageFromFilename("Makefile")).toBe("makefile");
expect(getLanguageFromFilename("makefile")).toBe("makefile");
expect(getLanguageFromFilename("GNUmakefile")).toBe("makefile");
});
});
describe("unknown extensions", () => {
it("returns plaintext for unknown extensions", () => {
expect(getLanguageFromFilename("file.unknown")).toBe("plaintext");
expect(getLanguageFromFilename("noextension")).toBe("plaintext");
});
it("returns plaintext for .txt files", () => {
expect(getLanguageFromFilename("notes.txt")).toBe("plaintext");
});
});
});
describe("validateFilename", () => {
const existingFiles = ["existing.ts", "config.json"];
describe("empty and invalid input", () => {
it("rejects empty filename", () => {
expect(validateFilename("", existingFiles)).toBe("filenameEmpty");
expect(validateFilename(" ", existingFiles)).toBe("filenameEmpty");
});
it("rejects invalid characters", () => {
expect(validateFilename("file<name>.ts", existingFiles)).toBe("filenameInvalidChars");
expect(validateFilename("file:name.ts", existingFiles)).toBe("filenameInvalidChars");
expect(validateFilename('file"name.ts', existingFiles)).toBe("filenameInvalidChars");
expect(validateFilename("file|name.ts", existingFiles)).toBe("filenameInvalidChars");
expect(validateFilename("file?name.ts", existingFiles)).toBe("filenameInvalidChars");
expect(validateFilename("file*name.ts", existingFiles)).toBe("filenameInvalidChars");
expect(validateFilename("file\\name.ts", existingFiles)).toBe("filenameInvalidChars");
});
});
describe("path validation", () => {
it("rejects paths starting with slash", () => {
expect(validateFilename("/src/file.ts", existingFiles)).toBe("pathStartEndSlash");
});
it("rejects paths ending with slash", () => {
expect(validateFilename("src/file/", existingFiles)).toBe("pathStartEndSlash");
});
it("rejects consecutive slashes", () => {
expect(validateFilename("src//file.ts", existingFiles)).toBe("pathConsecutiveSlashes");
});
it("rejects parent directory references", () => {
expect(validateFilename("../file.ts", existingFiles)).toBe("pathContainsDotDot");
expect(validateFilename("src/../file.ts", existingFiles)).toBe("pathContainsDotDot");
});
it("allows valid directory paths", () => {
expect(validateFilename("src/utils/helper.ts", existingFiles)).toBeNull();
});
});
describe("reserved names", () => {
it("rejects SKILL.md as filename", () => {
expect(validateFilename(DEFAULT_SKILL_FILE, existingFiles)).toBe("filenameReserved");
});
});
describe("duplicates", () => {
it("rejects duplicate filenames (case-insensitive)", () => {
expect(validateFilename("existing.ts", existingFiles)).toBe("filenameDuplicate");
expect(validateFilename("EXISTING.TS", existingFiles)).toBe("filenameDuplicate");
expect(validateFilename("Existing.Ts", existingFiles)).toBe("filenameDuplicate");
});
});
describe("length limits", () => {
it("rejects paths over 200 characters", () => {
const longPath = "a".repeat(201);
expect(validateFilename(longPath, existingFiles)).toBe("pathTooLong");
});
it("allows paths up to 200 characters", () => {
const maxPath = "a".repeat(200);
expect(validateFilename(maxPath, existingFiles)).toBeNull();
});
});
describe("valid filenames", () => {
it("accepts valid simple filenames", () => {
expect(validateFilename("newfile.ts", existingFiles)).toBeNull();
expect(validateFilename("readme.md", existingFiles)).toBeNull();
});
it("accepts valid paths with directories", () => {
expect(validateFilename("src/components/button.tsx", existingFiles)).toBeNull();
});
});
});
describe("isValidKebabCase", () => {
it("accepts valid kebab-case names", () => {
expect(isValidKebabCase("my-skill")).toBe(true);
expect(isValidKebabCase("another-great-skill")).toBe(true);
expect(isValidKebabCase("skill123")).toBe(true);
expect(isValidKebabCase("my-skill-v2")).toBe(true);
});
it("requires starting with a letter", () => {
expect(isValidKebabCase("123-skill")).toBe(false);
expect(isValidKebabCase("-my-skill")).toBe(false);
});
it("rejects uppercase letters", () => {
expect(isValidKebabCase("My-Skill")).toBe(false);
expect(isValidKebabCase("mySkill")).toBe(false);
});
it("rejects underscores and spaces", () => {
expect(isValidKebabCase("my_skill")).toBe(false);
expect(isValidKebabCase("my skill")).toBe(false);
});
it("rejects empty string", () => {
expect(isValidKebabCase("")).toBe(false);
});
});
describe("parseSkillFrontmatter", () => {
it("parses valid frontmatter", () => {
const content = `---
name: my-skill
description: A test skill
---
# My Skill`;
const result = parseSkillFrontmatter(content);
expect(result).toEqual({
name: "my-skill",
description: "A test skill",
});
});
it("returns null for missing frontmatter", () => {
const content = "# My Skill\n\nNo frontmatter here";
expect(parseSkillFrontmatter(content)).toBeNull();
});
it("handles partial frontmatter", () => {
const content = `---
name: my-skill
---
# My Skill`;
const result = parseSkillFrontmatter(content);
expect(result).toEqual({ name: "my-skill" });
});
it("handles multi-file content", () => {
const content = `---
name: multi-file-skill
description: Has multiple files
---
# Main\n\x1FFILE:helper.ts\x1E\nconst x = 1;`;
const result = parseSkillFrontmatter(content);
expect(result?.name).toBe("multi-file-skill");
expect(result?.description).toBe("Has multiple files");
});
});
describe("validateSkillFrontmatter", () => {
it("returns null for valid frontmatter", () => {
const content = `---
name: valid-skill
description: A valid description
---
# Content`;
expect(validateSkillFrontmatter(content)).toBeNull();
});
it("returns error for missing frontmatter", () => {
const content = "# No frontmatter";
expect(validateSkillFrontmatter(content)).toBe("frontmatterMissing");
});
it("returns error for missing name", () => {
const content = `---
description: Has description but no name
---`;
expect(validateSkillFrontmatter(content)).toBe("frontmatterNameRequired");
});
it("returns error for default placeholder name", () => {
const content = `---
name: my-skill-name
description: Real description
---`;
expect(validateSkillFrontmatter(content)).toBe("frontmatterNameRequired");
});
it("returns error for invalid name format", () => {
const content = `---
name: Invalid Name
description: Real description
---`;
expect(validateSkillFrontmatter(content)).toBe("frontmatterNameInvalidFormat");
});
it("returns error for missing description", () => {
const content = `---
name: valid-name
---`;
expect(validateSkillFrontmatter(content)).toBe("frontmatterDescriptionRequired");
});
it("returns error for default placeholder description", () => {
const content = `---
name: valid-name
description: A clear description of what this skill does and when to use it
---`;
expect(validateSkillFrontmatter(content)).toBe("frontmatterDescriptionRequired");
});
});
describe("generateSkillContentWithFrontmatter", () => {
it("generates content with frontmatter from title and description", () => {
const result = generateSkillContentWithFrontmatter("My Test Skill", "A description");
expect(result).toContain("---");
expect(result).toContain("name: my-test-skill");
expect(result).toContain("description: A description");
expect(result).toContain("# My Test Skill");
});
it("converts title to kebab-case", () => {
const result = generateSkillContentWithFrontmatter("Complex Title With Numbers 123", "desc");
expect(result).toContain("name: complex-title-with-numbers-123");
});
it("handles special characters in title", () => {
const result = generateSkillContentWithFrontmatter("Café Helper", "desc");
expect(result).toContain("name: cafe-helper");
});
it("uses default description when empty", () => {
const result = generateSkillContentWithFrontmatter("Test", "");
expect(result).toContain("description: A clear description of what this skill does");
});
it("uses default title when empty", () => {
const result = generateSkillContentWithFrontmatter("", "desc");
expect(result).toContain("# My Skill");
});
});
describe("suggestFilename", () => {
it("suggests README.md first for empty list", () => {
expect(suggestFilename([])).toBe("README.md");
});
it("skips already existing files", () => {
expect(suggestFilename(["README.md"])).toBe("config.json");
expect(suggestFilename(["README.md", "config.json"])).toBe("schema.json");
});
it("is case-insensitive when checking existing files", () => {
expect(suggestFilename(["readme.md"])).toBe("config.json");
expect(suggestFilename(["README.MD"])).toBe("config.json");
});
it("falls back to numbered files when all suggestions taken", () => {
const allTaken = [
"README.md", "config.json", "schema.json", "template.md",
"example.ts", "utils.ts", "types.ts", "constants.ts"
];
expect(suggestFilename(allTaken)).toBe("file1.md");
});
it("increments number for fallback files", () => {
const withFile1 = [
"README.md", "config.json", "schema.json", "template.md",
"example.ts", "utils.ts", "types.ts", "constants.ts", "file1.md"
];
expect(suggestFilename(withFile1)).toBe("file2.md");
});
});