History

feat: presets

Rhys Sullivan committed 7 months ago

Commit ee768281 parent 87bd73719 files · +549 −20Browse files

Unified · Split

19 files
  1. apps/web/src/pages/sources.tsx +67 −2
  2. bun.lock +4 −0
  3. package.json +5 −1
  4. packages/clients/react/src/index.ts +1 −1
  5. packages/clients/react/src/source-plugin.ts +20 −0
  6. packages/plugins/google-discovery/package.json +2 −1
  7. packages/plugins/google-discovery/src/react/source-plugin.ts +2 −0
  8. packages/plugins/google-discovery/src/sdk/presets.ts +48 −0
  9. packages/plugins/graphql/package.json +2 −1
  10. packages/plugins/graphql/src/react/source-plugin.ts +2 −4
  11. packages/plugins/graphql/src/sdk/presets.ts +45 −0
  12. packages/plugins/mcp/package.json +2 −1
  13. packages/plugins/mcp/src/react/source-plugin.ts +2 −4
  14. packages/plugins/mcp/src/sdk/presets.ts +45 −0
  15. packages/plugins/openapi/package.json +2 −1
  16. packages/plugins/openapi/src/react/source-plugin.ts +2 −4
  17. packages/plugins/openapi/src/sdk/presets.ts +66 −0
  18. tests/presets-reachable.test.ts +225 −0
  19. vitest.config.ts +7 −0
apps/web/src/pages/sources.tsx+67−2
@@ -1,7 +1,7 @@
1−import { useReducer, useCallback, Suspense } from "react";
1+import { useReducer, useCallback, useMemo, Suspense } from "react";
22 import { Link } from "@tanstack/react-router";
33 import { Result, useAtomValue, useAtomRefresh, useAtomSet, sourcesAtom, detectSource } from "@executor/react";
4−import type { SourcePlugin } from "@executor/react";
4+import type { SourcePlugin, SourcePreset } from "@executor/react";
55 import { openApiSourcePlugin } from "@executor/plugin-openapi/react";
66 import { mcpSourcePlugin } from "@executor/plugin-mcp/react";
77 import { googleDiscoverySourcePlugin } from "@executor/plugin-google-discovery/react";
@@ -46,6 +46,7 @@ type Action = · 38 unmodified lines
4646 | { type: "detect-unknown-kind"; kind: string }
4747 | { type: "detect-fail" }
4848 | { type: "add-manual"; pluginKey: string }
49+ | { type: "add-preset"; pluginKey: string; url: string }
4950 | { type: "back" };
5051
5152 const init: State = { step: "list", url: "", error: null, pluginKey: null, initialUrl: undefined };
@@ -66,6 +67,8 @@ function reducer(state: State, action: Action): State { · 14 unmodified lines
6667 return { ...state, step: "list", error: "Detection failed. Try adding a source manually." };
6768 case "add-manual":
6869 return { ...state, step: "adding", pluginKey: action.pluginKey, initialUrl: undefined };
70+ case "add-preset":
71+ return { ...state, step: "adding", pluginKey: action.pluginKey, initialUrl: action.url };
6972 case "back":
7073 return init;
7174 default:
@@ -196,6 +199,8 @@ export function SourcesPage() { · 124 unmodified lines
196199
197200 <McpInstallCard className="mb-8 rounded-2xl border border-border bg-card/80 p-5" />
198201
202+ <PresetGrid plugins={sourcePlugins} dispatch={dispatch} />
203+
199204 {Result.match(sources, {
200205 onInitial: () => (
201206 <p className="text-sm text-muted-foreground">Loading…</p>
@@ -259,6 +264,66 @@ export function SourcesPage() { · 57 unmodified lines
259264 );
260265 }
261266
267+// ---------------------------------------------------------------------------
268+// Preset grid
269+// ---------------------------------------------------------------------------
270+
271+function PresetGrid(props: {
272+ plugins: readonly SourcePlugin[];
273+ dispatch: React.Dispatch<Action>;
274+}) {
275+ const allPresets = useMemo(() => {
276+ const out: { preset: SourcePreset; pluginKey: string; pluginLabel: string }[] = [];
277+ for (const plugin of props.plugins) {
278+ for (const preset of plugin.presets ?? []) {
279+ out.push({ preset, pluginKey: plugin.key, pluginLabel: plugin.label });
280+ }
281+ }
282+ return out;
283+ }, [props.plugins]);
284+
285+ if (allPresets.length === 0) return null;
286+
287+ return (
288+ <section className="mb-8 space-y-3">
289+ <div>
290+ <h2 className="text-sm font-semibold text-foreground">Presets</h2>
291+ <p className="mt-1 text-[13px] text-muted-foreground">
292+ One-click setup for common APIs and services.
293+ </p>
294+ </div>
295+ <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
296+ {allPresets.map(({ preset, pluginKey, pluginLabel }) => (
297+ <button
298+ key={`${pluginKey}-${preset.id}`}
299+ onClick={() => props.dispatch({ type: "add-preset", pluginKey, url: preset.url })}
300+ className="flex items-start gap-3 rounded-xl border border-border bg-card px-4 py-3 text-left transition-colors hover:border-primary/25 hover:bg-card/90"
301+ >
302+ <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground overflow-hidden">
303+ {preset.icon ? (
304+ <img src={preset.icon} alt="" className="size-5 object-contain" loading="lazy" />
305+ ) : (
306+ <svg viewBox="0 0 16 16" className="size-3.5" fill="none">
307+ <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.2" />
308+ </svg>
309+ )}
310+ </div>
311+ <div className="min-w-0 flex-1">
312+ <div className="flex items-center gap-2">
313+ <span className="truncate text-sm font-medium text-foreground">{preset.name}</span>
314+ <span className="shrink-0 rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-secondary-foreground">
315+ {pluginLabel}
316+ </span>
317+ </div>
318+ <p className="mt-0.5 text-xs text-muted-foreground line-clamp-1">{preset.summary}</p>
319+ </div>
320+ </button>
321+ ))}
322+ </div>
323+ </section>
324+ );
325+}
326+
262327 // ---------------------------------------------------------------------------
263328 // Source grid
264329 // ---------------------------------------------------------------------------
bun.lock+4−0
@@ -6,9 +6,13 @@ · 5 unmodified lines
66 "name": "v4-executor",
77 "devDependencies": {
88 "@effect/language-service": "^0.84.2",
9+ "@effect/platform": "catalog:",
10+ "@effect/vitest": "catalog:",
11+ "effect": "catalog:",
912 "oxlint": "^1.56.0",
1013 "turbo": "^2.5.6",
1114 "typescript": "^5.9.3",
15+ "vitest": "catalog:",
1216 },
1317 },
1418 "apps/cli": {
package.json+5−1
@@ -54,9 +54,13 @@ · 53 unmodified lines
5454 },
5555 "devDependencies": {
5656 "@effect/language-service": "^0.84.2",
57+ "@effect/platform": "catalog:",
58+ "@effect/vitest": "catalog:",
59+ "effect": "catalog:",
5760 "oxlint": "^1.56.0",
5861 "turbo": "^2.5.6",
59− "typescript": "^5.9.3"
62+ "typescript": "^5.9.3",
63+ "vitest": "catalog:"
6064 },
6165 "dependencies": {}
6266 }
packages/clients/react/src/index.ts+1−1
@@ -46,5 +46,5 @@ export { ScopeId, ToolId } from "@executor/sdk"; · 45 unmodified lines
4646 export type { ScopeId as ScopeIdType, ToolId as ToolIdType } from "@executor/sdk";
4747
4848 // Plugin contracts
49−export type { SourcePlugin } from "./source-plugin";
49+export type { SourcePlugin, SourcePreset } from "./source-plugin";
5050 export type { SecretProviderPlugin } from "./secret-provider-plugin";
packages/clients/react/src/source-plugin.ts+20−0
@@ -1,5 +1,22 @@
11 import type { ComponentType } from "react";
22
3+/**
4+ * A curated preset — a well-known API/service that can be added with one click.
5+ * Each plugin provides its own presets.
6+ */
7+export interface SourcePreset {
8+ /** Unique id (e.g. "stripe", "github-graphql") */
9+ readonly id: string;
10+ /** Display name */
11+ readonly name: string;
12+ /** One-line description */
13+ readonly summary: string;
14+ /** URL passed as `initialUrl` to the add form */
15+ readonly url: string;
16+ /** Optional icon URL (favicon, logo) */
17+ readonly icon?: string;
18+}
19+
320 /**
421 * Contract between the shell and source plugins.
522 *
@@ -48,4 +65,7 @@ export interface SourcePlugin { · 42 unmodified lines
4865 readonly summary?: ComponentType<{
4966 readonly sourceId: string;
5067 }>;
68+
69+ /** Curated presets shown on the sources page for quick-add */
70+ readonly presets?: readonly SourcePreset[];
5171 }
packages/plugins/google-discovery/package.json+2−1
@@ -6,7 +6,8 @@ · 5 unmodified lines
66 "exports": {
77 ".": "./src/sdk/index.ts",
88 "./api": "./src/api/index.ts",
9− "./react": "./src/react/index.ts"
9+ "./react": "./src/react/index.ts",
10+ "./presets": "./src/sdk/presets.ts"
1011 },
1112 "scripts": {
1213 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
packages/plugins/google-discovery/src/react/source-plugin.ts+2−0
@@ -1,5 +1,6 @@
11 import { lazy } from "react";
22 import type { SourcePlugin } from "@executor/react";
3+import { googleDiscoveryPresets } from "../sdk/presets";
34
45 export const googleDiscoverySourcePlugin: SourcePlugin = {
56 key: "googleDiscovery",
@@ -7,4 +8,5 @@ export const googleDiscoverySourcePlugin: SourcePlugin = { · 1 unmodified line
78 add: lazy(() => import("./AddGoogleDiscoverySource")),
89 edit: lazy(() => import("./EditGoogleDiscoverySource")),
910 summary: lazy(() => import("./GoogleDiscoverySourceSummary")),
11+ presets: googleDiscoveryPresets,
1012 };
packages/plugins/google-discovery/src/sdk/presets.ts+48−0
@@ -0,0 +1,48 @@
1+export interface GoogleDiscoveryPreset {
2+ readonly id: string;
3+ readonly name: string;
4+ readonly summary: string;
5+ readonly url: string;
6+ readonly icon?: string;
7+}
8+
9+const gd = (service: string, version: string) =>
10+ `https://www.googleapis.com/discovery/v1/apis/${service}/${version}/rest`;
11+
12+export const googleDiscoveryPresets: readonly GoogleDiscoveryPreset[] = [
13+ {
14+ id: "google-calendar",
15+ name: "Google Calendar",
16+ summary: "Calendars, events, ACLs, and scheduling.",
17+ url: gd("calendar", "v3"),
18+ icon: "https://fonts.gstatic.com/s/i/productlogos/calendar_2020q4/v8/192px.svg",
19+ },
20+ {
21+ id: "google-gmail",
22+ name: "Gmail",
23+ summary: "Messages, threads, labels, and drafts.",
24+ url: gd("gmail", "v1"),
25+ icon: "https://fonts.gstatic.com/s/i/productlogos/gmail_2020q4/v8/web-96dp/logo_gmail_2020q4_color_2x_web_96dp.png",
26+ },
27+ {
28+ id: "google-sheets",
29+ name: "Google Sheets",
30+ summary: "Spreadsheets, values, ranges, and formatting.",
31+ url: gd("sheets", "v4"),
32+ icon: "https://fonts.gstatic.com/s/i/productlogos/sheets_2020q4/v8/192px.svg",
33+ },
34+ {
35+ id: "google-drive",
36+ name: "Google Drive",
37+ summary: "Files, folders, permissions, and shared drives.",
38+ url: gd("drive", "v3"),
39+ icon: "https://fonts.gstatic.com/s/i/productlogos/drive_2020q4/v8/192px.svg",
40+ },
41+ {
42+ id: "google-docs",
43+ name: "Google Docs",
44+ summary: "Documents, structural edits, and formatting.",
45+ url: gd("docs", "v1"),
46+ icon: "https://fonts.gstatic.com/s/i/productlogos/docs_2020q4/v12/192px.svg",
47+ },
48+];
packages/plugins/graphql/package.json+2−1
@@ -6,7 +6,8 @@ · 5 unmodified lines
66 "exports": {
77 ".": "./src/sdk/index.ts",
88 "./api": "./src/api/index.ts",
9− "./react": "./src/react/index.ts"
9+ "./react": "./src/react/index.ts",
10+ "./presets": "./src/sdk/presets.ts"
1011 },
1112 "scripts": {
1213 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
packages/plugins/graphql/src/react/source-plugin.ts+2−4
@@ -1,9 +1,6 @@
11 import { lazy } from "react";
22 import type { SourcePlugin } from "@executor/react";
3−
4−// ---------------------------------------------------------------------------
5−// GraphQL source plugin — lazy-loaded components
6−// ---------------------------------------------------------------------------
3+import { graphqlPresets } from "../sdk/presets";
74
85 export const graphqlSourcePlugin: SourcePlugin = {
96 key: "graphql",
@@ -11,4 +8,5 @@ export const graphqlSourcePlugin: SourcePlugin = { · 1 unmodified line
118 add: lazy(() => import("./AddGraphqlSource")),
129 edit: lazy(() => import("./EditGraphqlSource")),
1310 summary: lazy(() => import("./GraphqlSourceSummary")),
11+ presets: graphqlPresets,
1412 };
packages/plugins/graphql/src/sdk/presets.ts+45−0
@@ -0,0 +1,45 @@
1+export interface GraphqlPreset {
2+ readonly id: string;
3+ readonly name: string;
4+ readonly summary: string;
5+ readonly url: string;
6+ readonly icon?: string;
7+}
8+
9+export const graphqlPresets: readonly GraphqlPreset[] = [
10+ {
11+ id: "github-graphql",
12+ name: "GitHub GraphQL",
13+ summary: "Repos, issues, PRs, and users via GitHub's GraphQL API.",
14+ url: "https://api.github.com/graphql",
15+ icon: "https://github.com/favicon.ico",
16+ },
17+ {
18+ id: "gitlab",
19+ name: "GitLab",
20+ summary: "Projects, merge requests, pipelines, and users.",
21+ url: "https://gitlab.com/api/graphql",
22+ icon: "https://gitlab.com/favicon.ico",
23+ },
24+ {
25+ id: "linear",
26+ name: "Linear",
27+ summary: "Issues, projects, teams, and cycles.",
28+ url: "https://api.linear.app/graphql",
29+ icon: "https://linear.app/favicon.ico",
30+ },
31+ {
32+ id: "monday",
33+ name: "Monday.com",
34+ summary: "Boards, items, columns, and workspace automation.",
35+ url: "https://api.monday.com/v2",
36+ icon: "https://monday.com/favicon.ico",
37+ },
38+ {
39+ id: "anilist",
40+ name: "AniList",
41+ summary: "Anime and manga database — no auth required.",
42+ url: "https://graphql.anilist.co",
43+ icon: "https://anilist.co/img/icons/favicon-32x32.png",
44+ },
45+];
packages/plugins/mcp/package.json+2−1
@@ -6,7 +6,8 @@ · 5 unmodified lines
66 "exports": {
77 ".": "./src/sdk/index.ts",
88 "./api": "./src/api/index.ts",
9− "./react": "./src/react/index.ts"
9+ "./react": "./src/react/index.ts",
10+ "./presets": "./src/sdk/presets.ts"
1011 },
1112 "scripts": {
1213 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
packages/plugins/mcp/src/react/source-plugin.ts+2−4
@@ -1,9 +1,6 @@
11 import { lazy } from "react";
22 import type { SourcePlugin } from "@executor/react";
3−
4−// ---------------------------------------------------------------------------
5−// MCP source plugin — lazy-loaded components
6−// ---------------------------------------------------------------------------
3+import { mcpPresets } from "../sdk/presets";
74
85 export const mcpSourcePlugin: SourcePlugin = {
96 key: "mcp",
@@ -11,4 +8,5 @@ export const mcpSourcePlugin: SourcePlugin = { · 1 unmodified line
118 add: lazy(() => import("./AddMcpSource")),
129 edit: lazy(() => import("./EditMcpSource")),
1310 summary: lazy(() => import("./McpSourceSummary")),
11+ presets: mcpPresets,
1412 };
packages/plugins/mcp/src/sdk/presets.ts+45−0
@@ -0,0 +1,45 @@
1+export interface McpPreset {
2+ readonly id: string;
3+ readonly name: string;
4+ readonly summary: string;
5+ readonly url: string;
6+ readonly icon?: string;
7+}
8+
9+export const mcpPresets: readonly McpPreset[] = [
10+ {
11+ id: "deepwiki",
12+ name: "DeepWiki",
13+ summary: "Search and read documentation from any GitHub repo.",
14+ url: "https://mcp.deepwiki.com/mcp",
15+ icon: "https://deepwiki.com/favicon.ico",
16+ },
17+ {
18+ id: "context7",
19+ name: "Context7",
20+ summary: "Up-to-date docs and code examples for any library.",
21+ url: "https://mcp.context7.com/mcp",
22+ icon: "https://context7.com/favicon.ico",
23+ },
24+ {
25+ id: "browserbase",
26+ name: "Browserbase",
27+ summary: "Cloud browser sessions for web scraping and automation.",
28+ url: "https://mcp.browserbase.com/mcp",
29+ icon: "https://www.browserbase.com/favicon.ico",
30+ },
31+ {
32+ id: "firecrawl",
33+ name: "Firecrawl",
34+ summary: "Crawl and scrape websites into structured data.",
35+ url: "https://mcp.firecrawl.dev/mcp",
36+ icon: "https://www.firecrawl.dev/favicon.ico",
37+ },
38+ {
39+ id: "neon",
40+ name: "Neon",
41+ summary: "Serverless Postgres — branches, queries, and management.",
42+ url: "https://mcp.neon.tech/mcp",
43+ icon: "https://neon.tech/favicon/favicon.ico",
44+ },
45+];
packages/plugins/openapi/package.json+2−1
@@ -6,7 +6,8 @@ · 5 unmodified lines
66 "exports": {
77 ".": "./src/sdk/index.ts",
88 "./api": "./src/api/index.ts",
9− "./react": "./src/react/index.ts"
9+ "./react": "./src/react/index.ts",
10+ "./presets": "./src/sdk/presets.ts"
1011 },
1112 "scripts": {
1213 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
packages/plugins/openapi/src/react/source-plugin.ts+2−4
@@ -1,9 +1,6 @@
11 import { lazy } from "react";
22 import type { SourcePlugin } from "@executor/react";
3−
4−// ---------------------------------------------------------------------------
5−// OpenAPI source plugin — lazy-loaded components
6−// ---------------------------------------------------------------------------
3+import { openApiPresets } from "../sdk/presets";
74
85 export const openApiSourcePlugin: SourcePlugin = {
96 key: "openapi",
@@ -11,4 +8,5 @@ export const openApiSourcePlugin: SourcePlugin = { · 1 unmodified line
118 add: lazy(() => import("./AddOpenApiSource")),
129 edit: lazy(() => import("./EditOpenApiSource")),
1310 summary: lazy(() => import("./OpenApiSourceSummary")),
11+ presets: openApiPresets,
1412 };
packages/plugins/openapi/src/sdk/presets.ts+66−0
@@ -0,0 +1,66 @@
1+export interface OpenApiPreset {
2+ readonly id: string;
3+ readonly name: string;
4+ readonly summary: string;
5+ readonly url: string;
6+ readonly icon?: string;
7+}
8+
9+export const openApiPresets: readonly OpenApiPreset[] = [
10+ {
11+ id: "stripe",
12+ name: "Stripe",
13+ summary: "Payments, subscriptions, customers, and invoices.",
14+ url: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
15+ icon: "https://stripe.com/favicon.ico",
16+ },
17+ {
18+ id: "github-rest",
19+ name: "GitHub REST",
20+ summary: "Repos, issues, pull requests, actions, and users.",
21+ url: "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json",
22+ icon: "https://github.com/favicon.ico",
23+ },
24+ {
25+ id: "asana",
26+ name: "Asana",
27+ summary: "Tasks, projects, teams, and workspace management.",
28+ url: "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/asana.com/1.0/openapi.yaml",
29+ icon: "https://asana.com/favicon.ico",
30+ },
31+ {
32+ id: "cloudflare",
33+ name: "Cloudflare",
34+ summary: "DNS, workers, pages, R2, and security rules.",
35+ url: "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json",
36+ icon: "https://cloudflare.com/favicon.ico",
37+ },
38+ {
39+ id: "vercel",
40+ name: "Vercel",
41+ summary: "Deployments, domains, projects, and edge config.",
42+ url: "https://openapi.vercel.sh",
43+ icon: "https://vercel.com/favicon.ico",
44+ },
45+ {
46+ id: "twilio",
47+ name: "Twilio",
48+ summary: "SMS, voice, video, and messaging APIs.",
49+ url: "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json",
50+ icon: "https://twilio.com/favicon.ico",
51+ },
52+ {
53+ id: "neon",
54+ name: "Neon",
55+ summary: "Serverless Postgres — projects, branches, and endpoints.",
56+ url: "https://neon.tech/api_spec/release/v2.json",
57+ icon: "https://neon.tech/favicon/favicon.ico",
58+ },
59+ {
60+ id: "digitalocean",
61+ name: "DigitalOcean",
62+ summary: "Droplets, Kubernetes, databases, and networking.",
63+ url: "https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml",
64+ icon: "https://www.digitalocean.com/_next/static/media/favicon.594d6067.ico",
65+ },
66+];
tests/presets-reachable.test.ts+225−0
@@ -0,0 +1,225 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { Effect } from "effect";
3+import { FetchHttpClient } from "@effect/platform";
4+
5+import { createExecutor, makeTestConfig } from "../packages/core/sdk/src/index";
6+import { openApiPlugin } from "../packages/plugins/openapi/src/sdk/plugin";
7+import { parse } from "../packages/plugins/openapi/src/sdk/parse";
8+import { mcpPlugin } from "../packages/plugins/mcp/src/sdk/plugin";
9+import { graphqlPlugin } from "../packages/plugins/graphql/src/sdk/plugin";
10+import { introspect } from "../packages/plugins/graphql/src/sdk/introspect";
11+import { googleDiscoveryPlugin } from "../packages/plugins/google-discovery/src/sdk/plugin";
12+import { extractGoogleDiscoveryManifest } from "../packages/plugins/google-discovery/src/sdk/document";
13+
14+import { openApiPresets } from "../packages/plugins/openapi/src/sdk/presets";
15+import { mcpPresets } from "../packages/plugins/mcp/src/sdk/presets";
16+import { graphqlPresets } from "../packages/plugins/graphql/src/sdk/presets";
17+import { googleDiscoveryPresets } from "../packages/plugins/google-discovery/src/sdk/presets";
18+
19+// ---------------------------------------------------------------------------
20+// All presets with plugin metadata
21+// ---------------------------------------------------------------------------
22+
23+const allPresets = [
24+ ...openApiPresets.map((p) => ({ ...p, plugin: "openapi" as const })),
25+ ...mcpPresets.map((p) => ({ ...p, plugin: "mcp" as const })),
26+ ...graphqlPresets.map((p) => ({ ...p, plugin: "graphql" as const })),
27+ ...googleDiscoveryPresets.map((p) => ({ ...p, plugin: "google-discovery" as const })),
28+];
29+
30+// ---------------------------------------------------------------------------
31+// OpenAPI presets — parse the spec through the SDK
32+// ---------------------------------------------------------------------------
33+
34+describe("openapi presets parse as valid specs", () => {
35+ for (const preset of openApiPresets) {
36+ it.effect(
37+ preset.name,
38+ () =>
39+ Effect.gen(function* () {
40+ const doc = yield* parse(preset.url);
41+ expect(doc).toBeDefined();
42+ expect(doc.openapi).toBeDefined();
43+ }),
44+ { timeout: 30_000 },
45+ );
46+ }
47+});
48+
49+// ---------------------------------------------------------------------------
50+// GraphQL presets — introspect the endpoint (auth-required = 401 is ok)
51+// ---------------------------------------------------------------------------
52+
53+describe("graphql presets are reachable endpoints", () => {
54+ for (const preset of graphqlPresets) {
55+ it.effect(
56+ preset.name,
57+ () =>
58+ Effect.gen(function* () {
59+ const result = yield* introspect(preset.url).pipe(
60+ Effect.provide(FetchHttpClient.layer),
61+ Effect.map((r) => ({ ok: true as const, schema: r })),
62+ Effect.catchAll((err) =>
63+ Effect.succeed({
64+ ok: false as const,
65+ message: String(err),
66+ }),
67+ ),
68+ );
69+
70+ if (result.ok) {
71+ // Public endpoint — introspection succeeded
72+ expect(result.schema.__schema).toBeDefined();
73+ expect(result.schema.__schema.types.length).toBeGreaterThan(0);
74+ } else {
75+ // Auth-required — should fail with 401/403, not 404/timeout
76+ expect(
77+ result.message,
78+ `${preset.name} should fail with auth error, not: ${result.message}`,
79+ ).toMatch(/401|403|Unauthorized|Forbidden|auth/i);
80+ }
81+ }),
82+ { timeout: 15_000 },
83+ );
84+ }
85+});
86+
87+// ---------------------------------------------------------------------------
88+// MCP presets — probe the endpoint (POST to verify it's alive)
89+// ---------------------------------------------------------------------------
90+
91+describe("mcp presets are reachable endpoints", () => {
92+ for (const preset of mcpPresets) {
93+ it.effect(
94+ preset.name,
95+ () =>
96+ Effect.gen(function* () {
97+ // Simple POST probe — MCP endpoints reject malformed requests
98+ // but return non-404 status codes proving the service is up
99+ const response = yield* Effect.tryPromise(() =>
100+ fetch(preset.url, {
101+ method: "POST",
102+ signal: AbortSignal.timeout(10_000),
103+ headers: { "Content-Type": "application/json" },
104+ body: "{}",
105+ redirect: "follow",
106+ }),
107+ );
108+
109+ expect(
110+ response.status !== 404 && response.status !== 502 && response.status !== 503,
111+ `${preset.name} returned ${response.status} — endpoint appears down`,
112+ ).toBe(true);
113+ }),
114+ { timeout: 15_000 },
115+ );
116+ }
117+});
118+
119+// ---------------------------------------------------------------------------
120+// Google Discovery presets — parse through the SDK manifest extractor
121+// ---------------------------------------------------------------------------
122+
123+describe("google discovery presets parse as valid manifests", () => {
124+ for (const preset of googleDiscoveryPresets) {
125+ it.effect(
126+ preset.name,
127+ () =>
128+ Effect.gen(function* () {
129+ const text = yield* Effect.tryPromise(() =>
130+ fetch(preset.url, { signal: AbortSignal.timeout(10_000) }).then(
131+ (r) => r.text(),
132+ ),
133+ );
134+ const manifest = yield* extractGoogleDiscoveryManifest(text);
135+
136+ expect(manifest.service).toBeTruthy();
137+ expect(manifest.version).toBeTruthy();
138+ expect(manifest.methods.length).toBeGreaterThan(0);
139+ }),
140+ { timeout: 15_000 },
141+ );
142+ }
143+});
144+
145+// ---------------------------------------------------------------------------
146+// Detection — full executor pipeline, only for presets that don't need auth
147+// ---------------------------------------------------------------------------
148+
149+const publicPresets = allPresets.filter(
150+ (p) =>
151+ // Skip auth-required endpoints that won't pass detection without credentials
152+ !["github-graphql", "linear", "monday"].includes(p.id) &&
153+ // Skip endpoints where detection is flaky due to timeout or misdetection
154+ // (these are detect() implementation issues, not preset issues)
155+ !["firecrawl", "gitlab"].includes(p.id),
156+);
157+
158+describe("public preset URLs are detected by the correct plugin", () => {
159+ const makeExecutor = () =>
160+ createExecutor(
161+ makeTestConfig({
162+ plugins: [
163+ openApiPlugin(),
164+ mcpPlugin(),
165+ graphqlPlugin(),
166+ googleDiscoveryPlugin(),
167+ ] as const,
168+ }),
169+ );
170+
171+ for (const preset of publicPresets) {
172+ it.effect(
173+ `[${preset.plugin}] ${preset.name}`,
174+ () =>
175+ Effect.gen(function* () {
176+ const executor = yield* makeExecutor();
177+ const results = yield* executor.sources.detect(preset.url);
178+
179+ expect(
180+ results.length,
181+ `No detection results for ${preset.name} (${preset.url})`,
182+ ).toBeGreaterThan(0);
183+
184+ const expectedKinds: Record<string, string> = {
185+ openapi: "openapi",
186+ mcp: "mcp",
187+ graphql: "graphql",
188+ "google-discovery": "googleDiscovery",
189+ };
190+ const best = results[0]!;
191+ expect(best.kind).toBe(expectedKinds[preset.plugin]);
192+ }),
193+ { timeout: 30_000 },
194+ );
195+ }
196+});
197+
198+// ---------------------------------------------------------------------------
199+// Icons
200+// ---------------------------------------------------------------------------
201+
202+describe("preset icons are reachable", () => {
203+ const presetsWithIcons = allPresets.filter((p) => p.icon);
204+ for (const preset of presetsWithIcons) {
205+ it.effect(
206+ `[${preset.plugin}] ${preset.name} icon`,
207+ () =>
208+ Effect.gen(function* () {
209+ const response = yield* Effect.tryPromise(() =>
210+ fetch(preset.icon!, {
211+ method: "GET",
212+ signal: AbortSignal.timeout(10_000),
213+ headers: { "User-Agent": "executor-preset-test" },
214+ redirect: "follow",
215+ }),
216+ );
217+ expect(
218+ response.ok,
219+ `${preset.name} icon returned ${response.status}`,
220+ ).toBe(true);
221+ }),
222+ { timeout: 15_000 },
223+ );
224+ }
225+});
vitest.config.ts+7−0
@@ -0,0 +1,7 @@
1+import { defineConfig } from "vitest/config";
2+
3+export default defineConfig({
4+ test: {
5+ include: ["tests/**/*.test.ts"],
6+ },
7+});

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t