import { randomUUID } from "node:crypto"; import os from "node:os"; import path from "node:path"; import { promises as fs } from "node:fs"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { agents, companies, companySkills, createDb } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; import { companySkillService } from "../services/company-skills.ts"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( `Skipping embedded Postgres company skill service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, ); } describeEmbeddedPostgres("companySkillService.list", () => { let db!: ReturnType; let svc!: ReturnType; let tempDb: Awaited> | null = null; let oldPaperclipHome: string | undefined; let paperclipHome: string | null = null; const cleanupDirs = new Set(); beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("paperclip-company-skills-service-"); oldPaperclipHome = process.env.PAPERCLIP_HOME; paperclipHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-company-skills-home-")); process.env.PAPERCLIP_HOME = paperclipHome; db = createDb(tempDb.connectionString); svc = companySkillService(db); }, 20_000); afterEach(async () => { await db.delete(agents); await db.delete(companySkills); await db.delete(companies); await Promise.all(Array.from(cleanupDirs, (dir) => fs.rm(dir, { recursive: true, force: true }))); cleanupDirs.clear(); }); afterAll(async () => { if (oldPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = oldPaperclipHome; if (paperclipHome) { await fs.rm(paperclipHome, { recursive: true, force: true }); } await tempDb?.cleanup(); }); it("lists skills without exposing markdown content", async () => { const companyId = randomUUID(); const skillId = randomUUID(); const skillDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-heavy-skill-")); cleanupDirs.add(skillDir); await fs.writeFile(path.join(skillDir, "SKILL.md"), "# Heavy Skill\n", "utf8"); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(companySkills).values({ id: skillId, companyId, key: `company/${companyId}/heavy-skill`, slug: "heavy-skill", name: "Heavy Skill", description: "Large skill used for list projection regression coverage.", markdown: `# Heavy Skill\n\n${"x".repeat(250_000)}`, sourceType: "local_path", sourceLocator: skillDir, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "local_path" }, }); const listed = await svc.list(companyId); const skill = listed.find((entry) => entry.id === skillId); expect(skill).toBeDefined(); expect(skill).not.toHaveProperty("markdown"); expect(skill).toMatchObject({ id: skillId, key: `company/${companyId}/heavy-skill`, slug: "heavy-skill", name: "Heavy Skill", sourceType: "local_path", sourceLocator: skillDir, attachedAgentCount: 0, sourceBadge: "local", editable: true, }); }); it("rejects skill inventory refresh for a missing company", async () => { await expect(svc.list(randomUUID())).rejects.toMatchObject({ status: 404, message: "Company not found", }); }); it("does not persist audit failures for remote-source skills", async () => { const companyId = randomUUID(); const skillId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(companySkills).values({ id: skillId, companyId, key: "github.com/acme/remote-skill", slug: "remote-skill", name: "Remote Skill", description: null, markdown: "# Remote Skill\n", sourceType: "github", sourceLocator: "https://github.com/acme/remote-skill", sourceRef: "main", trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "github", owner: "acme", repo: "remote-skill" }, }); await expect(svc.auditSkill(companyId, skillId)).rejects.toMatchObject({ status: 422, message: "Only local-path and catalog-managed company skills support audit.", }); await expect(svc.getById(companyId, skillId)).resolves.toMatchObject({ metadata: { sourceKind: "github", owner: "acme", repo: "remote-skill" }, }); }); it("does not persist audit failures for gitea-source skills", async () => { const companyId = randomUUID(); const skillId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(companySkills).values({ id: skillId, companyId, key: "git.example.com/acme/remote-skill", slug: "remote-skill", name: "Remote Skill", description: null, markdown: "# Remote Skill\n", sourceType: "gitea", sourceLocator: "https://git.example.com/acme/remote-skill", sourceRef: "main", trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "gitea", hostname: "git.example.com", owner: "acme", repo: "remote-skill" }, }); await expect(svc.auditSkill(companyId, skillId)).rejects.toMatchObject({ status: 422, message: "Only local-path and catalog-managed company skills support audit.", }); await expect(svc.getById(companyId, skillId)).resolves.toMatchObject({ metadata: { sourceKind: "gitea", owner: "acme", repo: "remote-skill" }, }); }); it("preserves missing local-path skills that active agents still desire", async () => { const companyId = randomUUID(); const skillId = randomUUID(); const skillKey = `company/${companyId}/reflection-coach`; const missingSkillDir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-missing-used-skill-")), "gone"); cleanupDirs.add(path.dirname(missingSkillDir)); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(companySkills).values({ id: skillId, companyId, key: skillKey, slug: "reflection-coach", name: "Reflection Coach", description: null, markdown: "# Reflection Coach\n", sourceType: "local_path", sourceLocator: missingSkillDir, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "local_path" }, }); await db.insert(agents).values({ id: randomUUID(), companyId, name: "Reviewer", role: "engineer", status: "active", adapterType: "codex_local", adapterConfig: { paperclipSkillSync: { desiredSkills: [skillKey], }, }, }); const listed = await svc.list(companyId); const listedSkill = listed.find((skill) => skill.id === skillId); const detail = await svc.detail(companyId, skillId); const stored = await svc.getById(companyId, skillId); const marker = stored?.metadata?.missingSource; expect(listedSkill).toMatchObject({ id: skillId, attachedAgentCount: 1, }); expect(detail?.usedByAgents).toEqual([ expect.objectContaining({ name: "Reviewer", desired: true, }), ]); expect(marker).toMatchObject({ reason: "local_source_missing", sourceType: "local_path", sourceLocator: missingSkillDir, sourcePath: missingSkillDir, }); expect(Number.isNaN(Date.parse(String((marker as Record).detectedAt)))).toBe(false); }); it("continues pruning missing local-path skills that no active agent desires", async () => { const companyId = randomUUID(); const skillId = randomUUID(); const missingSkillDir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-missing-unused-skill-")), "gone"); cleanupDirs.add(path.dirname(missingSkillDir)); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(companySkills).values({ id: skillId, companyId, key: `company/${companyId}/unused-skill`, slug: "unused-skill", name: "Unused Skill", description: null, markdown: "# Unused Skill\n", sourceType: "local_path", sourceLocator: missingSkillDir, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "local_path" }, }); const listed = await svc.list(companyId); expect(listed.find((skill) => skill.id === skillId)).toBeUndefined(); await expect(svc.getById(companyId, skillId)).resolves.toBeNull(); }); it("clears the missing-source marker when a local-path skill source returns", async () => { const companyId = randomUUID(); const skillId = randomUUID(); const skillDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-restored-skill-")); cleanupDirs.add(skillDir); await fs.writeFile(path.join(skillDir, "SKILL.md"), "# Restored Skill\n", "utf8"); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(companySkills).values({ id: skillId, companyId, key: `company/${companyId}/restored-skill`, slug: "restored-skill", name: "Restored Skill", description: null, markdown: "# Restored Skill\n", sourceType: "local_path", sourceLocator: skillDir, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "local_path", missingSource: { reason: "local_source_missing", sourceType: "local_path", sourceLocator: skillDir, sourcePath: skillDir, detectedAt: "2026-05-28T00:00:00.000Z", }, }, }); await svc.list(companyId); const stored = await svc.getById(companyId, skillId); expect(stored?.metadata).toEqual({ sourceKind: "local_path" }); }); it("marks source-missing company skills as unavailable during read-only runtime listing", async () => { const companyId = randomUUID(); const skillId = randomUUID(); const skillKey = `company/${companyId}/reflection-coach`; const missingSkillDir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-readonly-missing-skill-")), "gone"); cleanupDirs.add(path.dirname(missingSkillDir)); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(companySkills).values({ id: skillId, companyId, key: skillKey, slug: "reflection-coach", name: "Reflection Coach", description: null, markdown: "# Reflection Coach\n", sourceType: "local_path", sourceLocator: missingSkillDir, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "local_path" }, }); await db.insert(agents).values({ id: randomUUID(), companyId, name: "Reviewer", role: "engineer", status: "active", adapterType: "codex_local", adapterConfig: { paperclipSkillSync: { desiredSkills: [skillKey], }, }, }); const entries = await svc.listRuntimeSkillEntries(companyId, { materializeMissing: false }); const entry = entries.find((candidate) => candidate.key === skillKey); expect(entry).toMatchObject({ key: skillKey, sourceStatus: "missing", missingDetail: expect.stringContaining(missingSkillDir), }); await expect(fs.stat(entry!.source)).rejects.toMatchObject({ code: "ENOENT" }); }); it("materializes source-missing company skills from the stored markdown during runtime listing", async () => { const companyId = randomUUID(); const skillId = randomUUID(); const skillKey = `company/${companyId}/runtime-coach`; const missingSkillDir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-missing-skill-")), "gone"); cleanupDirs.add(path.dirname(missingSkillDir)); await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(companySkills).values({ id: skillId, companyId, key: skillKey, slug: "runtime-coach", name: "Runtime Coach", description: null, markdown: "# Runtime Coach\n\nRecovered from DB.\n", sourceType: "local_path", sourceLocator: missingSkillDir, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "local_path" }, }); await db.insert(agents).values({ id: randomUUID(), companyId, name: "Runner", role: "engineer", status: "active", adapterType: "codex_local", adapterConfig: { paperclipSkillSync: { desiredSkills: [skillKey], }, }, }); const entries = await svc.listRuntimeSkillEntries(companyId); const entry = entries.find((candidate) => candidate.key === skillKey); expect(entry).toMatchObject({ key: skillKey, sourceStatus: "available", }); await expect(fs.readFile(path.join(entry!.source, "SKILL.md"), "utf8")).resolves.toBe( "# Runtime Coach\n\nRecovered from DB.\n", ); }); });