merge: wave 1 executor (02-01) into main

This commit is contained in:
ZacharyZhang-NY committed 2026-03-26 00:31:03 -04:00
commit e3a5e20d90
7 files changed
+1078

No files matched your search

Submodule .claude/worktrees/agent-ac899a98 added at fe0d3698dc.
+243
View File
@@ -0,0 +1,243 @@
// ---------------------------------------------------------------------------
// SKILL.md format parser, inventory classifier, and trust level derivation
// ---------------------------------------------------------------------------
import { extname } from "node:path";
import type { FileInventoryEntry, TrustLevel } from "./skill-types.js";
// ---------------------------------------------------------------------------
// Minimal YAML frontmatter parsing (no external YAML library)
// Follows the Paperclip reference pattern for SKILL.md files.
// ---------------------------------------------------------------------------
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function parseYamlScalar(rawValue: string): unknown {
const trimmed = rawValue.trim();
if (trimmed === "") return "";
if (trimmed === "null" || trimmed === "~") return null;
if (trimmed === "true") return true;
if (trimmed === "false") return false;
if (trimmed === "[]") return [];
if (trimmed === "{}") return {};
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
if (
trimmed.startsWith('"') ||
trimmed.startsWith("[") ||
trimmed.startsWith("{")
) {
try {
return JSON.parse(trimmed);
} catch {
return trimmed;
}
}
return trimmed;
}
function prepareYamlLines(
raw: string,
): Array<{ indent: number; content: string }> {
return raw
.split("\n")
.map((line) => ({
indent: line.match(/^ */)?.[0].length ?? 0,
content: line.trim(),
}))
.filter((line) => line.content.length > 0 && !line.content.startsWith("#"));
}
function parseYamlBlock(
lines: Array<{ indent: number; content: string }>,
startIndex: number,
indentLevel: number,
): { value: unknown; nextIndex: number } {
let index = startIndex;
while (index < lines.length && lines[index]!.content.length === 0)
index += 1;
if (index >= lines.length || lines[index]!.indent < indentLevel) {
return { value: {}, nextIndex: index };
}
const isArray =
lines[index]!.indent === indentLevel &&
lines[index]!.content.startsWith("-");
if (isArray) {
const values: unknown[] = [];
while (index < lines.length) {
const line = lines[index]!;
if (line.indent < indentLevel) break;
if (line.indent !== indentLevel || !line.content.startsWith("-")) break;
const remainder = line.content.slice(1).trim();
index += 1;
if (!remainder) {
const nested = parseYamlBlock(lines, index, indentLevel + 2);
values.push(nested.value);
index = nested.nextIndex;
continue;
}
const inlineObjectSeparator = remainder.indexOf(":");
if (
inlineObjectSeparator > 0 &&
!remainder.startsWith('"') &&
!remainder.startsWith("{") &&
!remainder.startsWith("[")
) {
const key = remainder.slice(0, inlineObjectSeparator).trim();
const rawValue = remainder.slice(inlineObjectSeparator + 1).trim();
const nextObject: Record<string, unknown> = {
[key]: parseYamlScalar(rawValue),
};
if (index < lines.length && lines[index]!.indent > indentLevel) {
const nested = parseYamlBlock(lines, index, indentLevel + 2);
if (isPlainRecord(nested.value)) {
Object.assign(nextObject, nested.value);
}
index = nested.nextIndex;
}
values.push(nextObject);
continue;
}
values.push(parseYamlScalar(remainder));
}
return { value: values, nextIndex: index };
}
// Object parsing
const record: Record<string, unknown> = {};
while (index < lines.length) {
const line = lines[index]!;
if (line.indent < indentLevel) break;
const colonIndex = line.content.indexOf(":");
if (colonIndex < 0) {
index += 1;
continue;
}
const key = line.content.slice(0, colonIndex).trim();
const remainder = line.content.slice(colonIndex + 1).trim();
index += 1;
if (!remainder && index < lines.length && lines[index]!.indent > indentLevel) {
const nested = parseYamlBlock(lines, index, lines[index]!.indent);
record[key] = nested.value;
index = nested.nextIndex;
continue;
}
record[key] = parseYamlScalar(remainder);
}
return { value: record, nextIndex: index };
}
function parseYamlFrontmatter(raw: string): Record<string, unknown> {
const prepared = prepareYamlLines(raw);
if (prepared.length === 0) return {};
const parsed = parseYamlBlock(prepared, 0, prepared[0]!.indent);
return isPlainRecord(parsed.value) ? parsed.value : {};
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Parse a SKILL.md file with optional YAML frontmatter.
* Normalizes CRLF to LF before parsing.
*/
export function parseFrontmatterMarkdown(
raw: string,
): { frontmatter: Record<string, unknown>; body: string } {
const normalized = raw.replace(/\r\n/g, "\n");
if (!normalized.startsWith("---\n")) {
return { frontmatter: {}, body: normalized.trim() };
}
const closing = normalized.indexOf("\n---\n", 4);
if (closing < 0) {
// Check for closing delimiter at EOF (no trailing newline)
const eofClosing = normalized.indexOf("\n---", 4);
if (eofClosing >= 0 && eofClosing + 4 >= normalized.length) {
const frontmatterRaw = normalized.slice(4, eofClosing).trim();
return {
frontmatter: parseYamlFrontmatter(frontmatterRaw),
body: "",
};
}
return { frontmatter: {}, body: normalized.trim() };
}
const frontmatterRaw = normalized.slice(4, closing).trim();
const body = normalized.slice(closing + 5).trim();
return {
frontmatter: parseYamlFrontmatter(frontmatterRaw),
body,
};
}
/**
* Classify a file path within a skill package into its inventory kind.
* Uses directory prefixes and file extensions for classification.
*/
export function classifyInventoryKind(
relativePath: string,
): FileInventoryEntry["kind"] {
// Normalize path separators and lowercase for matching
const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
if (normalized.endsWith("/skill.md") || normalized === "skill.md")
return "skill";
if (normalized.startsWith("references/")) return "reference";
if (normalized.startsWith("scripts/")) return "script";
if (normalized.startsWith("assets/")) return "asset";
if (normalized.endsWith(".md")) return "markdown";
const ext = extname(normalized).toLowerCase();
const scriptExtensions = new Set([
".sh",
".js",
".mjs",
".cjs",
".ts",
".py",
".rb",
".bash",
]);
if (scriptExtensions.has(ext)) return "script";
const assetExtensions = new Set([
".png",
".jpg",
".jpeg",
".gif",
".svg",
".webp",
".pdf",
]);
if (assetExtensions.has(ext)) return "asset";
return "other";
}
/**
* Derive the trust level for a skill based on its file inventory.
* Scripts/executables -> highest risk; assets -> medium; markdown only -> lowest.
*/
export function deriveTrustLevel(inventory: FileInventoryEntry[]): TrustLevel {
if (inventory.some((entry) => entry.kind === "script"))
return "scripts_executables";
if (
inventory.some(
(entry) => entry.kind === "asset" || entry.kind === "other",
)
)
return "assets";
return "markdown_only";
}
/**
* Estimate the token count for a text string.
* Uses a rough 1:4 character-to-token ratio.
*/
export function estimateTokens(text: string): number {
if (text.length === 0) return 0;
return Math.ceil(text.length / 4);
}
+194
View File
@@ -0,0 +1,194 @@
// ---------------------------------------------------------------------------
// Canonical skill key derivation and skill import source resolution
// ---------------------------------------------------------------------------
import { createHash } from "node:crypto";
import { resolve } from "node:path";
import type { SkillSourceType } from "./skill-types.js";
// ---------------------------------------------------------------------------
// Slug normalization helpers
// ---------------------------------------------------------------------------
const SLUG_DELIM_RE = /[^a-z0-9]+/g;
const SLUG_TRIM_RE = /^-+|-+$/g;
/**
* Normalize a value into a URL-safe slug.
* Returns null if the value is falsy or results in an empty string.
*/
export function normalizeSkillSlug(
value: string | undefined | null,
): string | null {
if (!value) return null;
const trimmed = value.trim();
if (!trimmed) return null;
const normalized = trimmed
.toLowerCase()
.replace(SLUG_DELIM_RE, "-")
.replace(SLUG_TRIM_RE, "");
return normalized.length > 0 ? normalized : null;
}
// ---------------------------------------------------------------------------
// Hashing
// ---------------------------------------------------------------------------
/**
* Compute the first 10 characters of a SHA-256 hex digest.
* Used for hash segments in canonical keys.
*/
export function hashSkillValue(value: string): string {
return createHash("sha256").update(value).digest("hex").slice(0, 10);
}
// ---------------------------------------------------------------------------
// Canonical key derivation
// ---------------------------------------------------------------------------
function asString(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Derive a deterministic canonical key for a skill.
*
* Key formats by source type:
* - GitHub/skills_sh with owner+repo: `{owner}/{repo}/{slug}`
* - URL with sourceLocator: `url/{normalizedHost}/{hash10}/{slug}`
* - local_path with sourceLocator: `local/{hash10 of resolved path}/{slug}`
* - Fallback (managed or unknown): `company/{companyId}/{slug}`
*/
export function deriveCanonicalSkillKey(
companyId: string,
input: {
slug: string;
sourceType: SkillSourceType;
sourceLocator: string | null;
metadata: Record<string, unknown> | null;
},
): string {
const slug = normalizeSkillSlug(input.slug) ?? "skill";
const metadata = isPlainRecord(input.metadata) ? input.metadata : null;
const owner = normalizeSkillSlug(asString(metadata?.owner));
const repo = normalizeSkillSlug(asString(metadata?.repo));
// GitHub or skills.sh with owner/repo metadata
if (
(input.sourceType === "github" || input.sourceType === "skills_sh") &&
owner &&
repo
) {
return `${owner}/${repo}/${slug}`;
}
// URL source with locator
if (input.sourceType === "url") {
const locator = asString(input.sourceLocator);
if (locator) {
try {
const url = new URL(locator);
const host = normalizeSkillSlug(url.host) ?? "url";
return `url/${host}/${hashSkillValue(locator)}/${slug}`;
} catch {
return `url/unknown/${hashSkillValue(locator)}/${slug}`;
}
}
}
// Local path source with locator
if (input.sourceType === "local_path") {
const locator = asString(input.sourceLocator);
if (locator) {
return `local/${hashSkillValue(resolve(locator))}/${slug}`;
}
}
// Fallback: company-scoped key
return `company/${companyId}/${slug}`;
}
// ---------------------------------------------------------------------------
// Skill import source resolution
// ---------------------------------------------------------------------------
// Three-part key: org/repo/skill
const THREE_PART_KEY_RE =
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
// Two-part key: org/repo
const TWO_PART_KEY_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
// skills.sh URL pattern
const SKILLS_SH_RE =
/^https?:\/\/(?:www\.)?skills\.sh\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:\/([A-Za-z0-9_.-]+))?(?:[?#].*)?$/i;
export interface ParsedSkillImportSource {
resolvedSource: string;
requestedSkillSlug: string | null;
originalSkillsShUrl: string | null;
}
/**
* Parse a raw skill import source string and resolve it to a normalized form.
*
* Supported input formats:
* - `owner/repo/skill` -> GitHub URL with skills.sh origin tracking
* - `owner/repo` -> GitHub URL
* - `https://skills.sh/owner/repo/skill` -> GitHub URL with skills.sh origin
* - `https://github.com/owner/repo` -> pass through
* - `/local/path` -> pass through
*/
export function parseSkillImportSource(
rawInput: string,
): ParsedSkillImportSource {
const trimmed = rawInput.trim();
// Three-part key (org/repo/skill) -- originates from skills.sh registry
if (
!/^https?:\/\//i.test(trimmed) &&
THREE_PART_KEY_RE.test(trimmed)
) {
const [owner, repo, skillSlugRaw] = trimmed.split("/");
return {
resolvedSource: `https://github.com/${owner}/${repo}`,
requestedSkillSlug: normalizeSkillSlug(skillSlugRaw),
originalSkillsShUrl: `https://skills.sh/${owner}/${repo}/${skillSlugRaw}`,
};
}
// Two-part key (org/repo) -- resolve to GitHub
if (
!/^https?:\/\//i.test(trimmed) &&
TWO_PART_KEY_RE.test(trimmed)
) {
return {
resolvedSource: `https://github.com/${trimmed}`,
requestedSkillSlug: null,
originalSkillsShUrl: null,
};
}
// skills.sh URL -> resolve to GitHub
const skillsShMatch = trimmed.match(SKILLS_SH_RE);
if (skillsShMatch) {
const [, owner, repo, skillSlugRaw] = skillsShMatch;
return {
resolvedSource: `https://github.com/${owner}/${repo}`,
requestedSkillSlug: skillSlugRaw
? normalizeSkillSlug(skillSlugRaw)
: null,
originalSkillsShUrl: trimmed,
};
}
// Everything else -- pass through as resolvedSource
return {
resolvedSource: trimmed,
requestedSkillSlug: null,
originalSkillsShUrl: null,
};
}
+114
View File
@@ -0,0 +1,114 @@
// ---------------------------------------------------------------------------
// Skill system type definitions and constants
// ---------------------------------------------------------------------------
/**
* Source type for a skill -- matches the company_skills.source_type column.
*/
export type SkillSourceType =
| "github"
| "local_path"
| "url"
| "skills_sh"
| "managed";
/**
* Trust level derived from the file inventory of a skill package.
* Determines what content is injected and how cautiously.
*/
export type TrustLevel =
| "markdown_only"
| "assets"
| "scripts_executables";
/**
* A single file entry within a skill package inventory.
*/
export interface FileInventoryEntry {
path: string;
kind: "skill" | "reference" | "script" | "asset" | "markdown" | "other";
}
/**
* Full row interface matching the company_skills table.
* Property names use camelCase (JS convention) even though DB columns are snake_case.
*/
export interface SkillRecord {
id: string;
companyId: string;
key: string;
slug: string;
name: string;
description: string | null;
markdown: string;
sourceType: SkillSourceType;
sourceLocator: string | null;
sourceRef: string | null;
trustLevel: TrustLevel;
compatibility: string;
fileInventory: FileInventoryEntry[];
metadata: Record<string, unknown> | null;
createdAt: string;
updatedAt: string;
}
/**
* Result returned after importing or updating a skill.
*/
export interface SkillImportResult {
skillId: string;
key: string;
name: string;
isUpdate: boolean;
}
/**
* Runtime representation of a skill entry for adapter injection.
*/
export interface RuntimeSkillEntry {
runtimeName: string;
sourcePath: string;
name: string;
tokenEstimate: number;
}
/**
* Minimal skill reference passed to worker threads.
* Subset of RuntimeSkillEntry to minimize serialization overhead.
*/
export interface ManagedSkillRef {
runtimeName: string;
sourcePath: string;
}
/**
* Skill-to-agent assignment record.
*/
export interface SkillAssignment {
agentId: string;
skillId: string;
}
/**
* Update status for a skill -- used to check if a newer version is available.
*/
export interface SkillUpdateStatus {
skillId: string;
key: string;
currentRef: string | null;
latestRef: string | null;
hasUpdate: boolean;
}
/**
* Per-adapter token budgets for skill content injection.
* Values represent the maximum token count of skill content that can be
* included in a single prompt for each adapter type.
*/
export const TOKEN_BUDGETS: Readonly<Record<string, number>> = {
claude_local: 50000,
codex_local: 30000,
gemini_local: 50000,
opencode_local: 30000,
kimi_local: 20000,
};
+200
View File
@@ -0,0 +1,200 @@
import { describe, it, expect } from "vitest";
import {
parseFrontmatterMarkdown,
classifyInventoryKind,
deriveTrustLevel,
estimateTokens,
} from "@main/skill-format";
import type { FileInventoryEntry } from "@main/skill-types";
describe("skill-format", () => {
describe("parseFrontmatterMarkdown", () => {
it("parses standard frontmatter and body", () => {
const result = parseFrontmatterMarkdown(
"---\nname: test\nslug: test\n---\n# Body",
);
expect(result.frontmatter).toEqual({ name: "test", slug: "test" });
expect(result.body).toBe("# Body");
});
it("returns empty frontmatter for text without frontmatter", () => {
const result = parseFrontmatterMarkdown("No frontmatter");
expect(result.frontmatter).toEqual({});
expect(result.body).toBe("No frontmatter");
});
it("handles frontmatter with no body", () => {
const result = parseFrontmatterMarkdown("---\nname: test\n---");
expect(result.frontmatter).toEqual({ name: "test" });
expect(result.body).toBe("");
});
it("normalizes CRLF to LF before parsing", () => {
const result = parseFrontmatterMarkdown(
"---\r\nname: test\r\n---\r\n# Body",
);
expect(result.frontmatter).toEqual({ name: "test" });
expect(result.body).toBe("# Body");
});
it("handles quoted string values", () => {
const result = parseFrontmatterMarkdown(
'---\nname: "My Skill"\n---\n',
);
expect(result.frontmatter.name).toBe("My Skill");
});
it("parses boolean values", () => {
const result = parseFrontmatterMarkdown(
"---\nenabled: true\ndisabled: false\n---\n",
);
expect(result.frontmatter.enabled).toBe(true);
expect(result.frontmatter.disabled).toBe(false);
});
it("parses numeric values", () => {
const result = parseFrontmatterMarkdown(
"---\nversion: 42\nprecision: 3.14\n---\n",
);
expect(result.frontmatter.version).toBe(42);
expect(result.frontmatter.precision).toBe(3.14);
});
it("parses null values", () => {
const result = parseFrontmatterMarkdown("---\nvalue: null\n---\n");
expect(result.frontmatter.value).toBeNull();
});
it("handles empty arrays and objects", () => {
const result = parseFrontmatterMarkdown(
"---\nitems: []\nopts: {}\n---\n",
);
expect(result.frontmatter.items).toEqual([]);
expect(result.frontmatter.opts).toEqual({});
});
it("handles body with multiple lines", () => {
const result = parseFrontmatterMarkdown(
"---\nname: test\n---\nLine 1\nLine 2\nLine 3",
);
expect(result.body).toBe("Line 1\nLine 2\nLine 3");
});
});
describe("classifyInventoryKind", () => {
it("classifies SKILL.md as skill", () => {
expect(classifyInventoryKind("SKILL.md")).toBe("skill");
});
it("classifies nested SKILL.md as skill", () => {
expect(classifyInventoryKind("subdir/SKILL.md")).toBe("skill");
});
it("classifies references/ path as reference", () => {
expect(classifyInventoryKind("references/api.md")).toBe("reference");
});
it("classifies scripts/ path as script", () => {
expect(classifyInventoryKind("scripts/build.sh")).toBe("script");
});
it("classifies assets/ path as asset", () => {
expect(classifyInventoryKind("assets/logo.png")).toBe("asset");
});
it("classifies .md files as markdown", () => {
expect(classifyInventoryKind("docs/guide.md")).toBe("markdown");
});
it("classifies config.json as other", () => {
expect(classifyInventoryKind("config.json")).toBe("other");
});
it("classifies .py files as script", () => {
expect(classifyInventoryKind("helper.py")).toBe("script");
});
it("classifies .sh files as script", () => {
expect(classifyInventoryKind("run.sh")).toBe("script");
});
it("classifies .js files as script", () => {
expect(classifyInventoryKind("index.js")).toBe("script");
});
it("classifies .ts files as script", () => {
expect(classifyInventoryKind("main.ts")).toBe("script");
});
it("classifies image files as asset", () => {
expect(classifyInventoryKind("photo.jpg")).toBe("asset");
expect(classifyInventoryKind("icon.svg")).toBe("asset");
expect(classifyInventoryKind("banner.webp")).toBe("asset");
expect(classifyInventoryKind("doc.pdf")).toBe("asset");
});
it("handles case insensitivity", () => {
expect(classifyInventoryKind("Skill.MD")).toBe("skill");
expect(classifyInventoryKind("References/API.md")).toBe("reference");
});
});
describe("deriveTrustLevel", () => {
it("returns scripts_executables when any script file exists", () => {
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
{ path: "scripts/build.sh", kind: "script" },
];
expect(deriveTrustLevel(inventory)).toBe("scripts_executables");
});
it("returns assets when asset but no script exists", () => {
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
{ path: "assets/logo.png", kind: "asset" },
];
expect(deriveTrustLevel(inventory)).toBe("assets");
});
it("returns assets when other but no script exists", () => {
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
{ path: "config.json", kind: "other" },
];
expect(deriveTrustLevel(inventory)).toBe("assets");
});
it("returns markdown_only when only markdown and skill files exist", () => {
const inventory: FileInventoryEntry[] = [
{ path: "SKILL.md", kind: "skill" },
{ path: "references/api.md", kind: "reference" },
{ path: "docs/guide.md", kind: "markdown" },
];
expect(deriveTrustLevel(inventory)).toBe("markdown_only");
});
it("returns markdown_only for empty inventory", () => {
expect(deriveTrustLevel([])).toBe("markdown_only");
});
});
describe("estimateTokens", () => {
it("returns 1 for a 4-character string", () => {
expect(estimateTokens("abcd")).toBe(1);
});
it("returns 0 for empty string", () => {
expect(estimateTokens("")).toBe(0);
});
it("rounds up with Math.ceil", () => {
expect(estimateTokens("ab")).toBe(1);
expect(estimateTokens("abcde")).toBe(2);
});
it("handles longer strings", () => {
const text = "a".repeat(100);
expect(estimateTokens(text)).toBe(25);
});
});
});
+180
View File
@@ -0,0 +1,180 @@
import { describe, it, expect } from "vitest";
import {
deriveCanonicalSkillKey,
normalizeSkillSlug,
hashSkillValue,
parseSkillImportSource,
} from "@main/skill-keys";
describe("skill-keys", () => {
describe("normalizeSkillSlug", () => {
it("lowercases and replaces non-alphanumeric with hyphens", () => {
expect(normalizeSkillSlug("Hello World!")).toBe("hello-world");
});
it("returns null for undefined", () => {
expect(normalizeSkillSlug(undefined)).toBeNull();
});
it("returns null for empty string", () => {
expect(normalizeSkillSlug("")).toBeNull();
});
it("returns null for null", () => {
expect(normalizeSkillSlug(null)).toBeNull();
});
it("trims leading and trailing hyphens", () => {
expect(normalizeSkillSlug("--hello--")).toBe("hello");
});
it("collapses consecutive non-alphanumeric chars", () => {
expect(normalizeSkillSlug("a b c")).toBe("a-b-c");
});
it("returns null for string that becomes empty", () => {
expect(normalizeSkillSlug("!!!")).toBeNull();
});
it("handles already-normalized slugs", () => {
expect(normalizeSkillSlug("code-review")).toBe("code-review");
});
});
describe("hashSkillValue", () => {
it("returns first 10 chars of SHA-256 hex digest", () => {
const hash = hashSkillValue("test-value");
expect(hash).toHaveLength(10);
expect(hash).toMatch(/^[0-9a-f]{10}$/);
});
it("produces deterministic output", () => {
const hash1 = hashSkillValue("same-input");
const hash2 = hashSkillValue("same-input");
expect(hash1).toBe(hash2);
});
it("produces different output for different inputs", () => {
const hash1 = hashSkillValue("input-a");
const hash2 = hashSkillValue("input-b");
expect(hash1).not.toBe(hash2);
});
});
describe("deriveCanonicalSkillKey", () => {
it("returns owner/repo/slug for GitHub source with metadata", () => {
const result = deriveCanonicalSkillKey("company-1", {
slug: "code-review",
sourceType: "github",
sourceLocator: "https://github.com/anthropics/skills",
metadata: { owner: "anthropics", repo: "skills" },
});
expect(result).toBe("anthropics/skills/code-review");
});
it("returns url/host/hash/slug for URL source", () => {
const result = deriveCanonicalSkillKey("company-1", {
slug: "my-skill",
sourceType: "url",
sourceLocator: "https://example.com/skill.md",
metadata: null,
});
expect(result).toMatch(/^url\/example-com\/[0-9a-f]{10}\/my-skill$/);
});
it("returns local/hash/slug for local_path source", () => {
const result = deriveCanonicalSkillKey("company-1", {
slug: "review",
sourceType: "local_path",
sourceLocator: "/home/user/skills/review",
metadata: null,
});
expect(result).toMatch(/^local\/[0-9a-f]{10}\/review$/);
});
it("returns same format as GitHub for skills_sh source with owner/repo", () => {
const result = deriveCanonicalSkillKey("company-1", {
slug: "lint",
sourceType: "skills_sh",
sourceLocator: null,
metadata: { owner: "anthropics", repo: "skills" },
});
expect(result).toBe("anthropics/skills/lint");
});
it("returns company/companyId/slug as fallback", () => {
const result = deriveCanonicalSkillKey("company-1", {
slug: "custom",
sourceType: "managed",
sourceLocator: null,
metadata: null,
});
expect(result).toBe("company/company-1/custom");
});
it("defaults slug to skill when null-like", () => {
const result = deriveCanonicalSkillKey("company-1", {
slug: "",
sourceType: "managed",
sourceLocator: null,
metadata: null,
});
expect(result).toBe("company/company-1/skill");
});
});
describe("parseSkillImportSource", () => {
it("parses three-part key as GitHub with skills.sh origin", () => {
const result = parseSkillImportSource("owner/repo/skill");
expect(result.resolvedSource).toBe("https://github.com/owner/repo");
expect(result.requestedSkillSlug).toBe("skill");
expect(result.originalSkillsShUrl).toBe(
"https://skills.sh/owner/repo/skill",
);
});
it("parses two-part key as GitHub", () => {
const result = parseSkillImportSource("owner/repo");
expect(result.resolvedSource).toBe("https://github.com/owner/repo");
expect(result.requestedSkillSlug).toBeNull();
expect(result.originalSkillsShUrl).toBeNull();
});
it("parses skills.sh URL", () => {
const result = parseSkillImportSource(
"https://skills.sh/owner/repo/skill",
);
expect(result.resolvedSource).toBe("https://github.com/owner/repo");
expect(result.requestedSkillSlug).toBe("skill");
expect(result.originalSkillsShUrl).toBe(
"https://skills.sh/owner/repo/skill",
);
});
it("passes through GitHub URL", () => {
const result = parseSkillImportSource(
"https://github.com/owner/repo",
);
expect(result.resolvedSource).toBe(
"https://github.com/owner/repo",
);
expect(result.requestedSkillSlug).toBeNull();
expect(result.originalSkillsShUrl).toBeNull();
});
it("passes through local path", () => {
const result = parseSkillImportSource("/local/path");
expect(result.resolvedSource).toBe("/local/path");
expect(result.requestedSkillSlug).toBeNull();
expect(result.originalSkillsShUrl).toBeNull();
});
it("handles skills.sh URL without skill slug", () => {
const result = parseSkillImportSource(
"https://skills.sh/owner/repo",
);
expect(result.resolvedSource).toBe("https://github.com/owner/repo");
expect(result.requestedSkillSlug).toBeNull();
});
});
});
+146
View File
@@ -0,0 +1,146 @@
import { describe, it, expect } from "vitest";
import {
TOKEN_BUDGETS,
} from "@main/skill-types";
import type {
SkillSourceType,
TrustLevel,
FileInventoryEntry,
SkillRecord,
SkillImportResult,
RuntimeSkillEntry,
ManagedSkillRef,
SkillAssignment,
SkillUpdateStatus,
} from "@main/skill-types";
describe("skill-types", () => {
describe("TOKEN_BUDGETS", () => {
it("has entries for all five adapter types", () => {
expect(TOKEN_BUDGETS).toHaveProperty("claude_local");
expect(TOKEN_BUDGETS).toHaveProperty("codex_local");
expect(TOKEN_BUDGETS).toHaveProperty("gemini_local");
expect(TOKEN_BUDGETS).toHaveProperty("opencode_local");
expect(TOKEN_BUDGETS).toHaveProperty("kimi_local");
});
it("has numeric token caps for each adapter", () => {
expect(TOKEN_BUDGETS.claude_local).toBe(50000);
expect(TOKEN_BUDGETS.codex_local).toBe(30000);
expect(TOKEN_BUDGETS.gemini_local).toBe(50000);
expect(TOKEN_BUDGETS.opencode_local).toBe(30000);
expect(TOKEN_BUDGETS.kimi_local).toBe(20000);
});
});
describe("type shape checks", () => {
it("SkillSourceType accepts all five source kinds", () => {
const sources: SkillSourceType[] = [
"github",
"local_path",
"url",
"skills_sh",
"managed",
];
expect(sources).toHaveLength(5);
});
it("TrustLevel accepts all three levels", () => {
const levels: TrustLevel[] = [
"markdown_only",
"assets",
"scripts_executables",
];
expect(levels).toHaveLength(3);
});
it("FileInventoryEntry has path and kind", () => {
const entry: FileInventoryEntry = { path: "SKILL.md", kind: "skill" };
expect(entry.path).toBe("SKILL.md");
expect(entry.kind).toBe("skill");
});
it("FileInventoryEntry kind accepts all six values", () => {
const kinds: FileInventoryEntry["kind"][] = [
"skill",
"reference",
"script",
"asset",
"markdown",
"other",
];
expect(kinds).toHaveLength(6);
});
it("SkillRecord has all expected properties", () => {
const record: SkillRecord = {
id: "test-id",
companyId: "company-1",
key: "owner/repo/slug",
slug: "slug",
name: "Test Skill",
description: null,
markdown: "# Test",
sourceType: "github",
sourceLocator: "https://github.com/owner/repo",
sourceRef: "abc123",
trustLevel: "markdown_only",
compatibility: "compatible",
fileInventory: [],
metadata: null,
createdAt: "2025-01-01T00:00:00Z",
updatedAt: "2025-01-01T00:00:00Z",
};
expect(record.id).toBe("test-id");
expect(record.companyId).toBe("company-1");
});
it("SkillImportResult has skillId, key, name, isUpdate", () => {
const result: SkillImportResult = {
skillId: "id-1",
key: "owner/repo/slug",
name: "Test",
isUpdate: false,
};
expect(result.isUpdate).toBe(false);
});
it("RuntimeSkillEntry has runtimeName, sourcePath, name, tokenEstimate", () => {
const entry: RuntimeSkillEntry = {
runtimeName: "claude",
sourcePath: "/path/to/skill",
name: "test-skill",
tokenEstimate: 1000,
};
expect(entry.tokenEstimate).toBe(1000);
});
it("ManagedSkillRef has runtimeName and sourcePath only", () => {
const ref: ManagedSkillRef = {
runtimeName: "claude",
sourcePath: "/path/to/skill",
};
expect(ref.runtimeName).toBe("claude");
expect(ref.sourcePath).toBe("/path/to/skill");
});
it("SkillAssignment has agentId and skillId", () => {
const assignment: SkillAssignment = {
agentId: "agent-1",
skillId: "skill-1",
};
expect(assignment.agentId).toBe("agent-1");
});
it("SkillUpdateStatus has all expected fields", () => {
const status: SkillUpdateStatus = {
skillId: "skill-1",
key: "owner/repo/slug",
currentRef: "abc",
latestRef: "def",
hasUpdate: true,
};
expect(status.hasUpdate).toBe(true);
});
});
});