267 lines · 8.7 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect } from "effect"; import appsClientPlugin, {  CUSTOM_TOOLS_LABEL,  CUSTOM_TOOLS_PLUGIN_KEY,  createCustomToolSourceEffect,  formatSyncErrors,  listCustomToolDirectoriesEffect,  listCustomToolSources,  parseGitSourceUrl,  removeCustomToolSourceEffect,  slugifyCustomToolsAppName,  suggestCustomToolsAppName,  syncCustomToolSourceEffect,  syncStatusLabel,  type AppSourceRecord,  type CustomToolsFetch,} from "./plugin-client";import {  directorySourceVerdict,  sourcePanelModel,  syncNoticeFromResult,} from "./source-panel-model";import { directoryBrowserRows } from "./source-panel-model"; const jsonResponse = (body: unknown, init?: ResponseInit): Response =>  new Response(JSON.stringify(body), {    status: 200,    headers: { "content-type": "application/json" },    ...init,  }); const demoSource: AppSourceRecord = {  slug: "demo-tools",  app: "demo-tools",  kind: "git",  config: {    kind: "git",    url: "https://gitlab.com/acme/demo-tools.git",    ref: "main",    tokenProvider: "default",    tokenItemId: "apps/source-tokens/demo-tools",  },  sourceRef: "abc123def456",  description: "Demo tools",  status: {    type: "published",    at: Date.parse("2026-07-06T12:00:00.000Z"),    tools: ["repo-summary", "stale-issues"],  },  updatedAt: Date.parse("2026-07-06T12:00:00.000Z"),}; describe("custom tools console client", () => {  it("registers the Apps integration plugin", () => {    const plugin = appsClientPlugin({ sourceKinds: ["git"] });     expect(plugin.id).toBe(CUSTOM_TOOLS_PLUGIN_KEY);    expect(plugin.integrationPlugin?.label).toBe(CUSTOM_TOOLS_LABEL);  });   it("validates and suggests generic https Git repository URLs", () => {    expect(parseGitSourceUrl("https://github.com/UsefulSoftwareCo/executor.git")).toMatchObject({      ok: true,      name: "executor",      url: "https://github.com/UsefulSoftwareCo/executor.git",    });    expect(parseGitSourceUrl("https://gitlab.com/acme/tools")).toMatchObject({      ok: true,      name: "tools",    });    expect(parseGitSourceUrl("ssh://gitlab.com/acme/tools")).toMatchObject({      ok: false,      message: "Git repository URLs must use http or https.",    });    expect(parseGitSourceUrl("https://x:secret@gitlab.com/acme/tools")).toMatchObject({      ok: false,      message: "Do not include credentials in the URL. Use the token field.",    });    expect(suggestCustomToolsAppName("https://gitlab.com/acme/tools.git")).toBe("tools");    expect(slugifyCustomToolsAppName("Acme Tools!")).toBe("acme-tools");  });   it("uses the new source routes and keeps token-bearing input out of read responses", async () => {    const calls: Array<{ readonly url: string; readonly init?: RequestInit }> = [];    let createBody = "";    const fetchImpl: CustomToolsFetch = async (input, init) => {      const url = String(input);      calls.push({ url, init });      if (url === "/api/apps/sources" && init?.method === "POST") {        createBody = String(init.body);        return jsonResponse({ source: demoSource });      }      if (url === "/api/apps/sources") {        return jsonResponse({ sources: [demoSource] });      }      if (url === "/api/apps/sources/demo-tools/sync") {        return jsonResponse({          status: "up-to-date",          sourceRef: "abc123def456",          tools: ["repo-summary", "stale-issues"],        });      }      if (url === "/api/apps/sources/demo-tools") {        return jsonResponse({ removed: true });      }      if (url === "/api/apps/fs/dirs?path=%2FUsers%2Fada%2Ftools&includeHidden=true") {        return jsonResponse({          path: "/Users/ada/tools",          parent: "/Users/ada",          dirs: [            { name: "alpha", path: "/Users/ada/tools/alpha", isSymlink: false, hasTools: true },          ],          source: { toolFiles: ["greet.ts"], skipped: [], hasPackageJson: true },        });      }      return jsonResponse({ error: "not found" }, { status: 404 });    };     const created = await Effect.runPromise(      createCustomToolSourceEffect(        {          kind: "git",          slug: "demo-tools",          app: "demo-tools",          url: "https://gitlab.com/acme/demo-tools.git",          token: "ghp_secret",        },        fetchImpl,      ),    );    const listed = await listCustomToolSources(fetchImpl);    const synced = await Effect.runPromise(syncCustomToolSourceEffect("demo-tools", fetchImpl));    const removed = await Effect.runPromise(removeCustomToolSourceEffect("demo-tools", fetchImpl));    const dirs = await Effect.runPromise(      listCustomToolDirectoriesEffect({ path: "/Users/ada/tools", includeHidden: true }, fetchImpl),    );     expect(created.source.slug).toBe("demo-tools");    expect(createBody).toContain("ghp_secret");    expect(JSON.stringify(listed)).not.toContain("ghp_secret");    expect(syncStatusLabel(synced)).toBe("Already up to date.");    expect(removed).toEqual({ removed: true });    expect(dirs.dirs).toEqual([      { name: "alpha", path: "/Users/ada/tools/alpha", isSymlink: false, hasTools: true },    ]);    expect(calls.map((call) => `${call.init?.method ?? "GET"} ${call.url}`)).toEqual([      "POST /api/apps/sources",      "GET /api/apps/sources",      "POST /api/apps/sources/demo-tools/sync",      "DELETE /api/apps/sources/demo-tools",      "GET /api/apps/fs/dirs?path=%2FUsers%2Fada%2Ftools&includeHidden=true",    ]);  });   it("models the source panel and sync notice", () => {    const panel = sourcePanelModel(demoSource, {      now: Date.parse("2026-07-06T12:05:00.000Z"),    });    const notice = syncNoticeFromResult(      {        status: "published",        sourceRef: "def456abc123",        tools: ["repo-summary", "stale-issues", "extra-tool"],      },      demoSource.status.type === "published" ? demoSource.status.tools : [],    );     expect(panel.title).toBe("demo-tools");    expect(panel.source).toBe("Git repository: https://gitlab.com/acme/demo-tools.git @ main");    expect(panel.status).toBe("Published 5m ago");    expect(panel.sourceRef).toBe("abc123def456");    expect(panel.tools).toEqual(["repo-summary", "stale-issues"]);    expect(notice.message).toBe("Published 3 tools.");    expect(notice.added).toEqual(["extra-tool"]);    expect(notice.sourceRef).toBe("def456abc123");  });   it("models the directory browser rows", () => {    expect(      directoryBrowserRows({        path: "/Users/ada/tools",        parent: "/Users/ada",        dirs: [          { name: "alpha", path: "/Users/ada/tools/alpha", isSymlink: false, hasTools: false },          { name: "linked", path: "/Users/ada/tools/linked", isSymlink: true, hasTools: false },        ],        source: { toolFiles: [], skipped: [], hasPackageJson: false },      }),    ).toEqual([      { kind: "parent", name: "..", path: "/Users/ada" },      {        kind: "dir",        name: "alpha",        path: "/Users/ada/tools/alpha",        isSymlink: false,        hasTools: false,      },      {        kind: "dir",        name: "linked",        path: "/Users/ada/tools/linked",        isSymlink: true,        hasTools: false,      },    ]);  });   it("models directory source verdicts", () => {    expect(      directorySourceVerdict({        path: "/repo",        parent: null,        dirs: [{ name: "tools", path: "/repo/tools", isSymlink: false, hasTools: false }],        source: { toolFiles: [], skipped: [], hasPackageJson: false },      }),    ).toEqual({      type: "empty-tools",      message: "tools/ has no tool files (tools/<name>.ts)",    });    expect(      directorySourceVerdict({        path: "/repo",        parent: null,        dirs: [],        source: { toolFiles: [], skipped: [], hasPackageJson: false },      }),    ).toEqual({      type: "missing-tools",      message: "No tools/ folder. Pick a folder containing tools/<name>.ts files.",    });    expect(      directorySourceVerdict({        path: "/repo",        parent: null,        dirs: [],        source: {          toolFiles: ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts", "f.ts"],          skipped: [],          hasPackageJson: true,        },      }),    ).toEqual({      type: "valid",      message: "6 tools found",      visibleTools: ["a", "b", "c", "d", "e"],      moreCount: 1,    });  });   it("formats failed sync diagnostics by stage", () => {    const errors = formatSyncErrors({      status: "failed",      tools: [],      errors: [        {          stage: "collect",          message: "schema export failed",          diagnostics: [{ path: "tools/x.ts", message: "unsupported schema" }],        },      ],    });     expect(errors).toEqual(["collect: schema export failed", "tools/x.ts: unsupported schema"]);  });});