import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import type { Skill } from "@oh-my-pi/pi-coding-agent/sdk"; import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; import { cleanupTempHome } from "./helpers/temp-home-cleanup"; function createIsolatedSkillsSettings(): Settings { return Settings.isolated({ "skills.enabled": true, "skills.enableCodexUser": false, "skills.enableClaudeUser": false, "skills.enableClaudeProject": false, "skills.enablePiUser": false, "skills.enablePiProject": true, }); } describe("createAgentSession skills option", () => { let tempDir: string; let skillsDir: string; let tempHomeDir = ""; let originalHome: string | undefined; beforeEach(() => { tempDir = path.join(os.tmpdir(), `pi-sdk-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); // Create skill in .omp/skills/ for native project-level discovery skillsDir = path.join(tempDir, ".omp", "skills", "test-skill"); fs.mkdirSync(skillsDir, { recursive: true }); originalHome = process.env.HOME; tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-sdk-home-")); process.env.HOME = tempHomeDir; const nativeUserSkillsDir = path.join(tempHomeDir, ".omp", "agent", "skills"); fs.mkdirSync(nativeUserSkillsDir, { recursive: true }); // Create a test skill in the pi skills directory fs.writeFileSync( path.join(skillsDir, "SKILL.md"), `--- name: test-skill description: A test skill for SDK tests. --- # Test Skill This is a test skill. `, ); const externalSkillDir = path.join(tempDir, "external-symlinked-skill"); fs.mkdirSync(externalSkillDir, { recursive: true }); fs.writeFileSync( path.join(externalSkillDir, "SKILL.md"), `--- name: symlinked-skill description: Skill loaded through a symlink. --- # Symlinked Skill Loaded via symbolic link. `, ); fs.symlinkSync(externalSkillDir, path.join(path.dirname(skillsDir), "symlinked-skill-link"), "dir"); }); afterEach(cleanupTempHome(() => ({ tempDir, tempHomeDir, originalHome }))); it("should discover skills by default and expose them on session.skills", async () => { const { session } = await createAgentSession({ cwd: tempDir, agentDir: tempDir, sessionManager: SessionManager.inMemory(), settings: createIsolatedSkillsSettings(), }); // Skills should be discovered and exposed on the session expect(session.skills.length).toBeGreaterThan(0); expect(session.skills.some((s: Skill) => s.name === "test-skill")).toBe(true); }); it("should discover skills when skill directory is a symlink", async () => { const { session } = await createAgentSession({ cwd: tempDir, agentDir: tempDir, sessionManager: SessionManager.inMemory(), settings: createIsolatedSkillsSettings(), }); expect(session.skills.some((s: Skill) => s.name === "symlinked-skill")).toBe(true); }); it("should still discover project skills when user skills directory is missing", async () => { const userAgentDir = path.join(tempHomeDir, ".omp", "agent"); fs.rmSync(path.join(userAgentDir, "skills"), { recursive: true, force: true }); fs.writeFileSync(path.join(userAgentDir, "placeholder.txt"), "placeholder"); const { session } = await createAgentSession({ cwd: tempDir, agentDir: tempDir, sessionManager: SessionManager.inMemory(), settings: createIsolatedSkillsSettings(), }); expect(session.skills.some((s: Skill) => s.name === "test-skill")).toBe(true); }); it("should have empty skills when options.skills is empty array (--no-skills)", async () => { const { session } = await createAgentSession({ cwd: tempDir, agentDir: tempDir, sessionManager: SessionManager.inMemory(), skills: [], // Explicitly empty - like --no-skills settings: createIsolatedSkillsSettings(), }); // session.skills should be empty expect(session.skills).toEqual([]); // No warnings since we didn't discover expect(session.skillWarnings).toEqual([]); }); it("should use provided skills when options.skills is explicitly set", async () => { const customSkill: Skill = { name: "custom-skill", description: "A custom skill", filePath: "/fake/path/SKILL.md", baseDir: "/fake/path", source: "custom" as const, }; const { session } = await createAgentSession({ cwd: tempDir, agentDir: tempDir, sessionManager: SessionManager.inMemory(), skills: [customSkill], settings: createIsolatedSkillsSettings(), }); // session.skills should contain only the provided skill expect(session.skills).toEqual([customSkill]); // No warnings since we didn't discover expect(session.skillWarnings).toEqual([]); }); });