/**
 * Tests for skill-upload CLI command
 */
import * as fs from "fs/promises";
import { tmpdir } from "os";
import * as path from "path";
import * as tar from "tar";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// Track the mock homedir value - set in beforeEach to the test's tmp dir
let mockHomedir = "";
// Mock os.homedir so getNoriSkillsetsDir() resolves under the test dir
vi.mock("os", async (importOriginal) => {
    const actual = (await importOriginal());
    return {
        ...actual,
        homedir: () => mockHomedir,
    };
});
vi.mock("node:os", async (importOriginal) => {
    const actual = (await importOriginal());
    return {
        ...actual,
        homedir: () => mockHomedir,
    };
});
// Mock the registrar API
vi.mock("@/api/registrar.js", () => ({
    REGISTRAR_URL: "https://noriskillsets.dev",
    registrarApi: {
        uploadSkill: vi.fn(),
        getSkillPackument: vi.fn(),
        downloadSkillTarball: vi.fn(),
    },
    NetworkError: class NetworkError extends Error {
        code;
        isNetworkError = true;
        constructor(message, code) {
            super(message);
            this.code = code;
            this.name = "NetworkError";
        }
    },
    ApiError: class ApiError extends Error {
        statusCode;
        isApiError = true;
        constructor(message, statusCode) {
            super(message);
            this.statusCode = statusCode;
            this.name = "ApiError";
        }
    },
}));
// Mock the config module
vi.mock("@/cli/config.js", async (importOriginal) => {
    const actual = (await importOriginal());
    return {
        ...actual,
        loadConfig: vi.fn(),
        getRegistryAuth: vi.fn(),
        getActiveSkillset: (args) => args.config?.activeSkillset ?? null,
    };
});
// Mock the registry auth module
vi.mock("@/api/registryAuth.js", () => ({
    getRegistryAuthToken: vi.fn(),
}));
// Track select/text return values per call
let selectReturnValues = [];
let textReturnValues = [];
// Mock @clack/prompts
vi.mock("@clack/prompts", () => ({
    intro: vi.fn(),
    outro: vi.fn(),
    note: vi.fn(),
    confirm: vi.fn(() => Promise.resolve(false)),
    spinner: vi.fn(() => ({
        start: vi.fn(),
        stop: vi.fn(),
        message: vi.fn(),
    })),
    log: {
        info: vi.fn(),
        success: vi.fn(),
        warn: vi.fn(),
        error: vi.fn(),
        message: vi.fn(),
        step: vi.fn(),
    },
    select: vi.fn(() => {
        const v = selectReturnValues.shift();
        return Promise.resolve(v);
    }),
    text: vi.fn(() => {
        const v = textReturnValues.shift();
        return Promise.resolve(v);
    }),
    isCancel: vi.fn(() => false),
    cancel: vi.fn(),
}));
// Suppress stray console output
vi.spyOn(console, "log").mockImplementation(() => undefined);
vi.spyOn(console, "error").mockImplementation(() => undefined);
import { registrarApi } from "../../../api/registrar.js";
import { getRegistryAuthToken } from "../../../api/registryAuth.js";
import { loadConfig, getRegistryAuth } from "../../../cli/config.js";
import { skillUploadMain } from "./skillUpload.js";
/**
 * Create a local skill directory inside the simulated ~/.nori/profiles/<skillset>/skills/<skillName>
 *
 * @param args - Arguments
 * @param args.skillsetName - Skillset directory name
 * @param args.skillName - Skill directory name
 * @param args.skillMdContent - Contents for SKILL.md (defaults to a minimal valid skill)
 * @param args.noriJson - Contents for the skill's nori.json; pass null to omit the file
 *
 * @returns The absolute path to the skill directory
 */
const createLocalSkill = async (args) => {
    const { skillsetName, skillName, skillMdContent, noriJson } = args;
    const skillsetDir = path.join(mockHomedir, ".nori", "profiles", skillsetName);
    const skillDir = path.join(skillsetDir, "skills", skillName);
    await fs.mkdir(skillDir, { recursive: true });
    // Create the skillset nori.json marker so parseSkillset resolves the skillset
    const skillsetNoriJsonPath = path.join(skillsetDir, "nori.json");
    try {
        await fs.access(skillsetNoriJsonPath);
    }
    catch {
        await fs.writeFile(skillsetNoriJsonPath, JSON.stringify({ name: skillsetName, version: "1.0.0", type: "skillset" }, null, 2));
    }
    const md = skillMdContent ??
        `---\nname: ${skillName}\ndescription: Test skill description\n---\n\n# ${skillName}\n\nSome content.\n`;
    await fs.writeFile(path.join(skillDir, "SKILL.md"), md);
    if (noriJson !== null) {
        const defaultNoriJson = {
            name: skillName,
            version: "1.0.0",
            type: "skill",
            description: "Test skill description",
        };
        await fs.writeFile(path.join(skillDir, "nori.json"), JSON.stringify(noriJson ?? defaultNoriJson, null, 2));
    }
    return skillDir;
};
/**
 * Create a gzipped tarball containing a SKILL.md with the given content. Used to
 * simulate an existing remote skill tarball for diffing.
 *
 * @param args - Arguments
 * @param args.skillMdContent - Contents to embed as SKILL.md
 *
 * @returns Tarball as ArrayBuffer
 */
const createMockSkillTarball = async (args) => {
    const { skillMdContent } = args;
    const tempDir = await fs.mkdtemp(path.join(tmpdir(), "skill-upload-remote-tarball-"));
    const tarballPath = path.join(tmpdir(), `remote-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.tgz`);
    try {
        await fs.writeFile(path.join(tempDir, "SKILL.md"), skillMdContent);
        tar.create({ gzip: true, file: tarballPath, cwd: tempDir, sync: true }, [
            "SKILL.md",
        ]);
        const buffer = await fs.readFile(tarballPath);
        const arrayBuffer = new ArrayBuffer(buffer.byteLength);
        new Uint8Array(arrayBuffer).set(buffer);
        return arrayBuffer;
    }
    finally {
        await fs.rm(tempDir, { recursive: true, force: true });
        await fs.unlink(tarballPath).catch(() => undefined);
    }
};
/**
 * Standard logged-in config used by tests that expect upload to succeed.
 *
 * @param activeSkillset - Name of the active skillset, or null for none
 *
 * @returns A Config-shaped object with fake auth credentials
 */
const authenticatedConfig = (activeSkillset) => ({
    activeSkillset,
    auth: {
        username: "tester",
        refreshToken: "rt-test",
        apiToken: null,
        organizations: [],
    },
});
describe("skill-upload", () => {
    let testDir;
    let originalEnvApiToken;
    beforeEach(async () => {
        vi.clearAllMocks();
        selectReturnValues = [];
        textReturnValues = [];
        originalEnvApiToken = process.env.NORI_API_TOKEN;
        delete process.env.NORI_API_TOKEN;
        testDir = await fs.mkdtemp(path.join(tmpdir(), "nori-skill-upload-test-"));
        mockHomedir = testDir;
        // Default auth mocks — individual tests can override
        vi.mocked(getRegistryAuth).mockReturnValue(null);
        vi.mocked(getRegistryAuthToken).mockResolvedValue("token-xyz");
    });
    afterEach(async () => {
        if (originalEnvApiToken == null) {
            delete process.env.NORI_API_TOKEN;
        }
        else {
            process.env.NORI_API_TOKEN = originalEnvApiToken;
        }
        if (testDir) {
            await fs.rm(testDir, { recursive: true, force: true });
        }
    });
    describe("skillUploadMain", () => {
        it("uploads a new skill (no remote collision) with version 1.0.0", async () => {
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "my-skill",
            });
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            // Remote skill does not exist (404)
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("Not found"), { statusCode: 404 }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "my-skill",
                version: "1.0.0",
                tarballSha: "sha512-xyz",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            const result = await skillUploadMain({ skillSpec: "my-skill" });
            expect(result.success).toBe(true);
            expect(registrarApi.uploadSkill).toHaveBeenCalledTimes(1);
            const uploadCall = vi.mocked(registrarApi.uploadSkill).mock.calls[0][0];
            expect(uploadCall.skillName).toBe("my-skill");
            expect(uploadCall.version).toBe("1.0.0");
            expect(result.message).toContain("my-skill");
            expect(result.message).toContain("1.0.0");
        });
        it("resolves skill from --skillset flag instead of active skillset", async () => {
            await createLocalSkill({
                skillsetName: "other-profile",
                skillName: "other-skill",
            });
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("Not found"), { statusCode: 404 }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "other-skill",
                version: "1.0.0",
                tarballSha: "sha",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            const result = await skillUploadMain({
                skillSpec: "other-skill",
                skillset: "other-profile",
            });
            expect(result.success).toBe(true);
            expect(registrarApi.uploadSkill).toHaveBeenCalledTimes(1);
        });
        it("treats identical remote content as already up to date (no upload)", async () => {
            const skillMd = `---\nname: stable-skill\ndescription: d\n---\n\n# stable-skill\nSame content.\n`;
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "stable-skill",
                skillMdContent: skillMd,
            });
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            vi.mocked(registrarApi.getSkillPackument).mockResolvedValue({
                name: "stable-skill",
                "dist-tags": { latest: "1.0.0" },
                versions: { "1.0.0": { name: "stable-skill", version: "1.0.0" } },
            });
            vi.mocked(registrarApi.downloadSkillTarball).mockResolvedValue(await createMockSkillTarball({ skillMdContent: skillMd }));
            const result = await skillUploadMain({ skillSpec: "stable-skill" });
            expect(result.success).toBe(true);
            expect(registrarApi.uploadSkill).not.toHaveBeenCalled();
            expect(result.message.toLowerCase()).toContain("up to date");
        });
        it("on content-differs collision, prompts user and uploads with bumped version when accepted", async () => {
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "conflicted-skill",
                skillMdContent: `---\nname: conflicted-skill\n---\n\nLOCAL content\n`,
            });
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            vi.mocked(registrarApi.getSkillPackument).mockResolvedValue({
                name: "conflicted-skill",
                "dist-tags": { latest: "1.2.3" },
                versions: { "1.2.3": { name: "conflicted-skill", version: "1.2.3" } },
            });
            vi.mocked(registrarApi.downloadSkillTarball).mockResolvedValue(await createMockSkillTarball({
                skillMdContent: `---\nname: conflicted-skill\n---\n\nREMOTE content\n`,
            }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "conflicted-skill",
                version: "1.2.4",
                tarballSha: "sha",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            // User picks "bump" then accepts default next version
            selectReturnValues = ["bump"];
            textReturnValues = ["1.2.4"];
            const result = await skillUploadMain({ skillSpec: "conflicted-skill" });
            expect(result.success).toBe(true);
            expect(registrarApi.uploadSkill).toHaveBeenCalledTimes(1);
            const uploadCall = vi.mocked(registrarApi.uploadSkill).mock.calls[0][0];
            expect(uploadCall.version).toBe("1.2.4");
        });
        it("on content-differs collision, cancelling leaves skill un-uploaded", async () => {
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "conflicted-skill",
                skillMdContent: `---\nname: conflicted-skill\n---\n\nLOCAL content\n`,
            });
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            vi.mocked(registrarApi.getSkillPackument).mockResolvedValue({
                name: "conflicted-skill",
                "dist-tags": { latest: "1.0.0" },
                versions: { "1.0.0": { name: "conflicted-skill", version: "1.0.0" } },
            });
            vi.mocked(registrarApi.downloadSkillTarball).mockResolvedValue(await createMockSkillTarball({
                skillMdContent: `---\nname: conflicted-skill\n---\n\nREMOTE content\n`,
            }));
            selectReturnValues = ["cancel"];
            const result = await skillUploadMain({ skillSpec: "conflicted-skill" });
            expect(result.success).toBe(false);
            expect(result.cancelled).toBe(true);
            expect(registrarApi.uploadSkill).not.toHaveBeenCalled();
        });
        it("on content-differs collision, user can view diff then resolve", async () => {
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "diffable-skill",
                skillMdContent: `---\nname: diffable-skill\n---\n\nLOCAL content\n`,
            });
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            vi.mocked(registrarApi.getSkillPackument).mockResolvedValue({
                name: "diffable-skill",
                "dist-tags": { latest: "1.0.0" },
                versions: { "1.0.0": { name: "diffable-skill", version: "1.0.0" } },
            });
            vi.mocked(registrarApi.downloadSkillTarball).mockResolvedValue(await createMockSkillTarball({
                skillMdContent: `---\nname: diffable-skill\n---\n\nREMOTE content\n`,
            }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "diffable-skill",
                version: "1.0.1",
                tarballSha: "sha",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            // User picks "viewDiff" first, then "bump", then accepts default version
            selectReturnValues = ["viewDiff", "bump"];
            textReturnValues = ["1.0.1"];
            const result = await skillUploadMain({ skillSpec: "diffable-skill" });
            // The user chose viewDiff first, then bump. Successful resolution proves
            // the flow accepted the diff action and looped back to the select prompt.
            expect(result.success).toBe(true);
            const uploadCall = vi.mocked(registrarApi.uploadSkill).mock.calls[0][0];
            expect(uploadCall.version).toBe("1.0.1");
        });
        it("errors when the local skill is not found", async () => {
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            // Create skillset dir but no skill subdirectory
            await fs.mkdir(path.join(mockHomedir, ".nori", "profiles", "my-profile"), { recursive: true });
            await fs.writeFile(path.join(mockHomedir, ".nori", "profiles", "my-profile", "nori.json"), JSON.stringify({
                name: "my-profile",
                version: "1.0.0",
                type: "skillset",
            }));
            const result = await skillUploadMain({ skillSpec: "missing-skill" });
            expect(result.success).toBe(false);
            expect(registrarApi.uploadSkill).not.toHaveBeenCalled();
            expect(result.message.toLowerCase()).toContain("not found");
        });
        it("errors when no active skillset is set and --skillset is omitted", async () => {
            vi.mocked(loadConfig).mockResolvedValue({});
            const result = await skillUploadMain({ skillSpec: "any-skill" });
            expect(result.success).toBe(false);
            expect(registrarApi.uploadSkill).not.toHaveBeenCalled();
            expect(result.message.toLowerCase()).toMatch(/active skillset|--skillset/);
        });
        it("blocks uploading a skill marked as type=inlined-skill", async () => {
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "inline-skill",
                noriJson: {
                    name: "inline-skill",
                    version: "1.0.0",
                    type: "inlined-skill",
                },
            });
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            const result = await skillUploadMain({ skillSpec: "inline-skill" });
            expect(result.success).toBe(false);
            expect(registrarApi.uploadSkill).not.toHaveBeenCalled();
            expect(result.message.toLowerCase()).toContain("inline");
        });
        it("uses default description from skill nori.json when --description is not passed", async () => {
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "described-skill",
                noriJson: {
                    name: "described-skill",
                    version: "1.0.0",
                    type: "skill",
                    description: "A lovely skill from nori.json",
                },
            });
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("not found"), { statusCode: 404 }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "described-skill",
                version: "1.0.0",
                tarballSha: "sha",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            await skillUploadMain({ skillSpec: "described-skill" });
            const uploadCall = vi.mocked(registrarApi.uploadSkill).mock.calls[0][0];
            expect(uploadCall.description).toBe("A lovely skill from nori.json");
        });
        it("uploads a namespaced skill when authenticated via API token (no refreshToken)", async () => {
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "my-skill",
            });
            // API-token login: refreshToken null, apiToken set, organizations populated.
            vi.mocked(loadConfig).mockResolvedValue({
                activeSkillset: "my-profile",
                auth: {
                    username: null,
                    organizationUrl: "https://myorg.noriskillsets.dev",
                    refreshToken: null,
                    apiToken: "nori_myorg_0000000000000000000000000000000000000000000000000000000000000000",
                    organizations: ["myorg"],
                },
            });
            vi.mocked(getRegistryAuthToken).mockResolvedValue("api-token-auth");
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("Not found"), { statusCode: 404 }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "my-skill",
                version: "1.0.0",
                tarballSha: "sha",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            const result = await skillUploadMain({ skillSpec: "myorg/my-skill" });
            expect(result.success).toBe(true);
            // Upload hit the org registry with API-token-derived auth.
            expect(registrarApi.uploadSkill).toHaveBeenCalledTimes(1);
            const uploadCall = vi.mocked(registrarApi.uploadSkill).mock.calls[0][0];
            expect(uploadCall.registryUrl).toBe("https://myorg.noriskillsets.dev");
            expect(uploadCall.authToken).toBe("api-token-auth");
        });
        it("uploads a namespaced skill when authenticated via broker-managed idToken only", async () => {
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "my-skill",
            });
            vi.mocked(loadConfig).mockResolvedValue({
                activeSkillset: "my-profile",
                auth: {
                    username: "sprite-service:dev",
                    organizationUrl: "https://myorg.noriskillsets.dev",
                    refreshToken: null,
                    idToken: "session-id-token",
                    idTokenExpiresAt: Date.now() + 60_000,
                    apiToken: null,
                    organizations: ["myorg"],
                },
            });
            vi.mocked(getRegistryAuthToken).mockResolvedValue("id-token-auth");
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("Not found"), { statusCode: 404 }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "my-skill",
                version: "1.0.0",
                tarballSha: "sha",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            const result = await skillUploadMain({ skillSpec: "myorg/my-skill" });
            expect(result.success).toBe(true);
            expect(registrarApi.uploadSkill).toHaveBeenCalledTimes(1);
            const uploadCall = vi.mocked(registrarApi.uploadSkill).mock.calls[0][0];
            expect(uploadCall.registryUrl).toBe("https://myorg.noriskillsets.dev");
            expect(uploadCall.authToken).toBe("id-token-auth");
        });
        it("uploads a public skill when authenticated via public API token config", async () => {
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "my-skill",
            });
            vi.mocked(loadConfig).mockResolvedValue({
                activeSkillset: "my-profile",
                auth: {
                    username: null,
                    organizationUrl: "https://noriskillsets.dev",
                    refreshToken: null,
                    apiToken: "nori_public_1111111111111111111111111111111111111111111111111111111111111111",
                    organizations: ["public"],
                },
            });
            vi.mocked(getRegistryAuthToken).mockResolvedValue("public-api-auth");
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("Not found"), { statusCode: 404 }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "my-skill",
                version: "1.0.0",
                tarballSha: "sha",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            const result = await skillUploadMain({ skillSpec: "my-skill" });
            expect(result.success).toBe(true);
            expect(getRegistryAuthToken).toHaveBeenCalledWith({
                registryAuth: expect.objectContaining({
                    registryUrl: "https://noriskillsets.dev",
                    apiToken: "nori_public_1111111111111111111111111111111111111111111111111111111111111111",
                }),
            });
            const uploadCall = vi.mocked(registrarApi.uploadSkill).mock.calls[0][0];
            expect(uploadCall.registryUrl).toBe("https://noriskillsets.dev");
            expect(uploadCall.authToken).toBe("public-api-auth");
        });
        it("uploads a public skill with NORI_API_TOKEN and no saved config", async () => {
            process.env.NORI_API_TOKEN =
                "nori_public_2222222222222222222222222222222222222222222222222222222222222222";
            await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "my-skill",
            });
            vi.mocked(loadConfig).mockResolvedValue(null);
            vi.mocked(getRegistryAuthToken).mockResolvedValue("public-env-auth");
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("Not found"), { statusCode: 404 }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "my-skill",
                version: "1.0.0",
                tarballSha: "sha",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            const result = await skillUploadMain({
                skillSpec: "my-skill",
                skillset: "my-profile",
                silent: true,
                nonInteractive: true,
            });
            expect(result.success).toBe(true);
            expect(getRegistryAuthToken).toHaveBeenCalledWith({
                registryAuth: expect.objectContaining({
                    registryUrl: "https://noriskillsets.dev",
                }),
            });
            const uploadCall = vi.mocked(registrarApi.uploadSkill).mock.calls[0][0];
            expect(uploadCall.registryUrl).toBe("https://noriskillsets.dev");
            expect(uploadCall.authToken).toBe("public-env-auth");
        });
        it("syncs the local skill nori.json version after a successful upload", async () => {
            const skillDir = await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "synced-skill",
                noriJson: {
                    name: "synced-skill",
                    version: "1.0.0",
                    type: "skill",
                    description: "desc",
                },
            });
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("not found"), { statusCode: 404 }));
            vi.mocked(registrarApi.uploadSkill).mockResolvedValue({
                name: "synced-skill",
                version: "1.0.0",
                tarballSha: "sha",
                createdAt: "2026-04-16T00:00:00.000Z",
            });
            await skillUploadMain({
                skillSpec: "synced-skill",
                version: "2.0.0",
            });
            const updatedNoriJson = JSON.parse(await fs.readFile(path.join(skillDir, "nori.json"), "utf-8"));
            expect(updatedNoriJson.version).toBe("2.0.0");
        });
        it("excludes editor swap files and OS junk from the upload tarball", async () => {
            const skillDir = await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "my-skill",
            });
            // Files that must NOT end up in the tarball.
            await fs.writeFile(path.join(skillDir, ".SKILL.md.swp"), "vim swap garbage");
            await fs.writeFile(path.join(skillDir, ".DS_Store"), "macOS finder metadata");
            await fs.writeFile(path.join(skillDir, "Thumbs.db"), "windows thumbnail cache");
            await fs.writeFile(path.join(skillDir, "._SKILL.md"), "macOS resource fork");
            await fs.writeFile(path.join(skillDir, "SKILL.md~"), "editor backup");
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("Not found"), { statusCode: 404 }));
            let capturedArchiveData = null;
            vi.mocked(registrarApi.uploadSkill).mockImplementation(async (args) => {
                capturedArchiveData = args.archiveData;
                return {
                    name: "my-skill",
                    version: "1.0.0",
                    tarballSha: "sha",
                    createdAt: "2026-04-16T00:00:00.000Z",
                };
            });
            const result = await skillUploadMain({ skillSpec: "my-skill" });
            expect(result.success).toBe(true);
            expect(capturedArchiveData).not.toBeNull();
            const extractDir = await fs.mkdtemp(path.join(tmpdir(), "skill-tarball-extract-"));
            try {
                const tarballBuffer = Buffer.from(capturedArchiveData);
                const tarballPath = path.join(extractDir, "upload.tgz");
                await fs.writeFile(tarballPath, tarballBuffer);
                await tar.extract({ file: tarballPath, cwd: extractDir });
                const extractedFiles = await fs.readdir(extractDir, {
                    recursive: true,
                });
                expect(extractedFiles).toContain("SKILL.md");
                expect(extractedFiles).toContain("nori.json");
                expect(extractedFiles).not.toContain(".SKILL.md.swp");
                expect(extractedFiles).not.toContain(".DS_Store");
                expect(extractedFiles).not.toContain("Thumbs.db");
                expect(extractedFiles).not.toContain("._SKILL.md");
                expect(extractedFiles).not.toContain("SKILL.md~");
            }
            finally {
                await fs.rm(extractDir, { recursive: true, force: true });
            }
        });
        it("excludes dependency/build bloat directories from the upload tarball", async () => {
            const skillDir = await createLocalSkill({
                skillsetName: "my-profile",
                skillName: "my-skill",
            });
            // node_modules at any depth -> excluded
            await fs.mkdir(path.join(skillDir, "node_modules", "lodash"), {
                recursive: true,
            });
            await fs.writeFile(path.join(skillDir, "node_modules", "lodash", "index.js"), "module.exports = {};");
            // .venv at any depth -> excluded
            await fs.mkdir(path.join(skillDir, ".venv", "bin"), { recursive: true });
            await fs.writeFile(path.join(skillDir, ".venv", "bin", "python"), "#!/usr/bin/env python");
            // Rust crate: target adjacent to Cargo.toml -> excluded; crate sources kept
            await fs.mkdir(path.join(skillDir, "rustcrate", "src"), {
                recursive: true,
            });
            await fs.writeFile(path.join(skillDir, "rustcrate", "Cargo.toml"), '[package]\nname = "x"');
            await fs.writeFile(path.join(skillDir, "rustcrate", "src", "main.rs"), "fn main() {}");
            await fs.mkdir(path.join(skillDir, "rustcrate", "target", "debug"), {
                recursive: true,
            });
            await fs.writeFile(path.join(skillDir, "rustcrate", "target", "debug", "app"), "binary");
            // A "target" dir with no adjacent Cargo.toml -> kept
            await fs.mkdir(path.join(skillDir, "data", "target"), {
                recursive: true,
            });
            await fs.writeFile(path.join(skillDir, "data", "target", "results.json"), "{}");
            // A leaf file literally named "target" -> kept
            await fs.mkdir(path.join(skillDir, "keep"), { recursive: true });
            await fs.writeFile(path.join(skillDir, "keep", "target"), "not a dir");
            vi.mocked(loadConfig).mockResolvedValue(authenticatedConfig("my-profile"));
            vi.mocked(registrarApi.getSkillPackument).mockRejectedValue(Object.assign(new Error("Not found"), { statusCode: 404 }));
            let capturedArchiveData = null;
            vi.mocked(registrarApi.uploadSkill).mockImplementation(async (args) => {
                capturedArchiveData = args.archiveData;
                return {
                    name: "my-skill",
                    version: "1.0.0",
                    tarballSha: "sha",
                    createdAt: "2026-04-16T00:00:00.000Z",
                };
            });
            const result = await skillUploadMain({ skillSpec: "my-skill" });
            expect(result.success).toBe(true);
            expect(capturedArchiveData).not.toBeNull();
            const extractDir = await fs.mkdtemp(path.join(tmpdir(), "skill-bloat-extract-"));
            try {
                const tarballPath = path.join(extractDir, "upload.tgz");
                await fs.writeFile(tarballPath, Buffer.from(capturedArchiveData));
                await tar.extract({ file: tarballPath, cwd: extractDir });
                const extractedFiles = await fs.readdir(extractDir, {
                    recursive: true,
                });
                // Legit content is kept
                expect(extractedFiles).toContain("SKILL.md");
                expect(extractedFiles).toContain(path.join("rustcrate", "Cargo.toml"));
                expect(extractedFiles).toContain(path.join("rustcrate", "src", "main.rs"));
                expect(extractedFiles).toContain(path.join("data", "target", "results.json"));
                expect(extractedFiles).toContain(path.join("keep", "target"));
                // Bloat is excluded
                expect(extractedFiles.some((f) => f.split(path.sep).includes("node_modules"))).toBe(false);
                expect(extractedFiles.some((f) => f.split(path.sep).includes(".venv"))).toBe(false);
                expect(extractedFiles.some((f) => f.startsWith(path.join("rustcrate", "target")))).toBe(false);
            }
            finally {
                await fs.rm(extractDir, { recursive: true, force: true });
            }
        });
    });
});
//# sourceMappingURL=skillUpload.test.js.map