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/<ref> or /commit/<sha>.", ); 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."); });});
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/<ref> or /commit/<sha>.",
);
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.");
});
});