import { describe, expect, it } from "@effect/vitest"; import appsClientPlugin, { CUSTOM_TOOLS_LABEL, CUSTOM_TOOLS_PLUGIN_KEY, type CustomToolsFetch, appsIntegrationPlugin, formatSyncErrors, getCustomToolSource, listCustomToolSources, removeCustomToolSource, parseGitHubSourceUrl, slugifyCustomToolsAppName, suggestCustomToolsAppName, syncCustomToolSource, syncStatusLabel, validateGitHubSourceUrl, } from "./plugin-client"; import { asSnapshotId } from "../seams/artifact-store"; import { sourcePanelModel, syncNoticeFromResult } from "./source-panel-model"; const jsonResponse = (body: unknown, init?: ResponseInit): Response => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, ...init, }); describe("custom tools console client", () => { const demoSource = { slug: "demo-tools", name: "Demo tools", scope: "demo-tools", url: "https://github.com/RhysSullivan/executor-custom-tools-demo", repo: "RhysSullivan/executor-custom-tools-demo", ref: "main", hasToken: true, upstreamSha: "abc123", snapshotId: "snap1", publishedAt: "2026-07-06T12:00:00.000Z", tools: ["repo-summary", "stale-issues"], skipped: [{ path: "README.md", reason: "ignored" as const }], }; it("registers the manual Custom tools tile", () => { const manualTiles = [appsIntegrationPlugin].map((plugin) => ({ href: `/integrations/add/${plugin.key}`, label: plugin.label, })); expect(appsClientPlugin.id).toBe(CUSTOM_TOOLS_PLUGIN_KEY); expect(appsClientPlugin.integrationPlugin?.label).toBe(CUSTOM_TOOLS_LABEL); expect(manualTiles).toContainEqual({ href: "/integrations/add/apps", label: "Custom tools", }); }); it("validates the GitHub URL shape", () => { expect(validateGitHubSourceUrl("https://github.com/UsefulSoftwareCo/executor")).toBeNull(); expect(validateGitHubSourceUrl("UsefulSoftwareCo/executor")).toBeNull(); expect(validateGitHubSourceUrl("https://gitlab.com/UsefulSoftwareCo/executor")).toBe( "GitHub source URLs must use github.com.", ); expect(validateGitHubSourceUrl("UsefulSoftwareCo")).toBe( "Use a GitHub repo URL like https://github.com/owner/repo, optionally with /tree/ or /commit/.", ); expect(validateGitHubSourceUrl("")).toBe("Enter a GitHub URL."); }); it("carries detected repo URL variants into the add form defaults", () => { const treeUrl = "https://github.com/RhysSullivan/executor-custom-tools-demo/tree/feature/custom-tools"; const commitUrl = "https://github.com/RhysSullivan/executor-custom-tools-demo/commit/abc1234"; const tree = parseGitHubSourceUrl(treeUrl); const commit = parseGitHubSourceUrl(commitUrl); expect(tree).toMatchObject({ ok: true, value: { repo: "RhysSullivan/executor-custom-tools-demo", ref: "feature/custom-tools", url: treeUrl, }, }); expect(commit).toMatchObject({ ok: true, value: { repo: "RhysSullivan/executor-custom-tools-demo", ref: "abc1234", url: commitUrl, }, }); expect(suggestCustomToolsAppName(treeUrl)).toBe("executor-custom-tools-demo"); expect(slugifyCustomToolsAppName(suggestCustomToolsAppName(treeUrl))).toBe( "executor-custom-tools-demo", ); }); it("does not classify non-repo GitHub URLs as custom-tools add input", () => { expect(parseGitHubSourceUrl("https://gist.github.com/RhysSullivan/abc1234").ok).toBe(false); expect( parseGitHubSourceUrl( "https://github.com/RhysSullivan/executor-custom-tools-demo/blob/main/openapi.json", ).ok, ).toBe(false); }); it("models the custom-tools detail panel without default debug fields", () => { const panel = sourcePanelModel(demoSource, { now: Date.parse("2026-07-06T12:05:00.000Z"), }); expect(panel.title).toBe("Demo tools"); expect(panel.repository).toEqual({ href: "https://github.com/RhysSullivan/executor-custom-tools-demo", label: "github.com/RhysSullivan/executor-custom-tools-demo", }); expect(panel.lastSynced).toBe("Last synced 5m ago"); expect(panel.publishedTools).toEqual({ href: "/integrations/demo-tools?tab=tools", label: "2 tools", }); expect(panel).not.toHaveProperty("toolNames"); expect(panel).not.toHaveProperty("skipped"); expect(panel).not.toHaveProperty("upstreamSha"); expect(panel).not.toHaveProperty("hasToken"); }); it("keeps skipped files and upstream SHA in the post-sync result details", () => { const notice = syncNoticeFromResult( { status: "published", snapshotId: asSnapshotId("snap2"), upstreamSha: "def456", tools: ["repo-summary", "stale-issues", "third-tool"], skipped: [{ path: "workflows/x.ts", reason: "not supported yet" }], }, demoSource.tools, ); expect(notice.message).toBe("Published 3 tools."); expect(notice.added).toEqual(["third-tool"]); expect(notice.skipped).toEqual([{ path: "workflows/x.ts", reason: "not supported yet" }]); expect(notice.upstreamSha).toBe("def456"); }); it("surfaces successful sync and source detail data", async () => { let syncBody = ""; let removed = false; const fetchImpl: CustomToolsFetch = async (input, init) => { const url = String(input); if (url.endsWith("/sync")) { syncBody = String(init?.body ?? ""); return jsonResponse({ status: "published", snapshotId: "snap1", upstreamSha: "abc123", tools: ["repo-summary", "stale-issues"], skipped: [], }); } if (url.endsWith("/demo-tools")) { if (init?.method === "DELETE") { removed = true; return jsonResponse({ removed: true }); } return jsonResponse({ source: { slug: "demo-tools", name: "demo-tools", scope: "demo-tools", url: "https://github.com/RhysSullivan/executor-custom-tools-demo", repo: "RhysSullivan/executor-custom-tools-demo", ref: "main", hasToken: true, upstreamSha: "abc123", snapshotId: "snap1", publishedAt: "2026-07-06T12:00:00.000Z", tools: ["repo-summary", "stale-issues"], skipped: [], }, }); } return jsonResponse({ sources: [ { slug: "demo-tools", name: "demo-tools", scope: "demo-tools", url: "https://github.com/RhysSullivan/executor-custom-tools-demo", repo: "RhysSullivan/executor-custom-tools-demo", ref: "main", hasToken: true, upstreamSha: "abc123", snapshotId: "snap1", publishedAt: "2026-07-06T12:00:00.000Z", tools: ["repo-summary", "stale-issues"], skipped: [], }, ], }); }; const syncResult = await syncCustomToolSource( { name: "demo-tools", url: "https://github.com/RhysSullivan/executor-custom-tools-demo", token: "ghp_demo", }, fetchImpl, ); const listed = await listCustomToolSources(fetchImpl); const detail = await getCustomToolSource("demo-tools", fetchImpl); const remove = await removeCustomToolSource("demo-tools", fetchImpl); expect(syncStatusLabel(syncResult)).toBe("Published 2 tools."); expect(syncBody).toContain("ghp_demo"); expect(syncBody).toContain("demo-tools"); expect(syncResult.tools).toEqual(["repo-summary", "stale-issues"]); expect(detail.source?.slug).toBe("demo-tools"); expect(listed.sources[0]?.hasToken).toBe(true); expect(listed.sources[0]?.tools).toEqual(["repo-summary", "stale-issues"]); expect(remove).toEqual({ removed: true }); expect(removed).toBe(true); }); it("renders failed sync errors readably", async () => { const fetchImpl: CustomToolsFetch = async () => jsonResponse({ status: "failed", tools: [], skipped: [], errors: [ { stage: "collect", message: "schema library not supported for schema export", diagnostics: [{ path: "tools/x.ts", message: "vendor: nope" }], }, ], }); const result = await syncCustomToolSource( { url: "https://github.com/RhysSullivan/executor-custom-tools-demo", }, fetchImpl, ); expect(syncStatusLabel(result)).toBe("Sync failed."); expect(formatSyncErrors(result)).toEqual([ "collect: schema library not supported for schema export (tools/x.ts: vendor: nope)", ]); }); it("shows the up-to-date sync state", async () => { const fetchImpl: CustomToolsFetch = async () => jsonResponse({ status: "up-to-date", upstreamSha: "abc123", tools: ["repo-summary"], skipped: [], }); const result = await syncCustomToolSource( { url: "https://github.com/RhysSullivan/executor-custom-tools-demo", }, fetchImpl, ); expect(syncStatusLabel(result)).toBe("Already up to date."); }); });