import { createServer } from "node:http";
import { HttpApiBuilder, HttpServer, OpenApi } from "@effect/platform";
import { NodeHttpServer } from "@effect/platform-node";
import { describe, expect, it } from "@effect/vitest";
import {
createControlPlaneApiLayer,
createControlPlaneRuntime,
type LocalInstallation,
type Source,
type ControlPlaneRuntime,
} from "@executor/control-plane";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as Scope from "effect/Scope";
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
import * as Schema from "effect/Schema";
import { JSDOM } from "jsdom";
import * as React from "react";
import { createRoot } from "react-dom/client";
import {
ExecutorReactProvider,
setExecutorApiBaseUrl,
type Loadable,
useCreateSource,
useRemoveSource,
useSource,
useSourceInspection,
useSources,
useUpdateSource,
} from "./index";
const dom = new JSDOM("
", {
url: "http://127.0.0.1/",
});
globalThis.window = dom.window as unknown as typeof globalThis.window;
globalThis.document = dom.window.document;
Object.defineProperty(globalThis, "navigator", {
value: dom.window.navigator,
configurable: true,
});
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.Node = dom.window.Node;
globalThis.MutationObserver = dom.window.MutationObserver;
globalThis.Event = dom.window.Event;
globalThis.EventTarget = dom.window.EventTarget;
globalThis.requestAnimationFrame = (callback: FrameRequestCallback) =>
setTimeout(() => callback(Date.now()), 0) as unknown as number;
globalThis.cancelAnimationFrame = (handle: number) => {
clearTimeout(handle);
};
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
type DelayRule = {
method: string;
pathname: RegExp;
durationMs: number;
};
type RunningServer = {
baseUrl: string;
close: () => Promise;
};
type ApiServer = RunningServer & {
runtime: ControlPlaneRuntime;
};
type HookHarness = {
current: T | null;
unmount: () => Promise;
};
type CreateHarnessState = {
sources: Loadable>;
createSource: ReturnType;
};
type SourceHarnessState = {
sources: Loadable>;
source: Loadable;
inspection: ReturnType;
updateSource: ReturnType;
removeSource: ReturnType;
};
const closeScope = (scope: Scope.CloseableScope) =>
Scope.close(scope, Exit.void).pipe(Effect.orDie);
const startControlPlaneServer = async (): Promise => {
const runtime = await Effect.runPromise(
createControlPlaneRuntime({
workspaceRoot: mkdtempSync(join(tmpdir(), "executor-react-test-")),
}),
);
const scope = await Effect.runPromise(Scope.make());
try {
const serverLayer = HttpApiBuilder.serve().pipe(
Layer.provide(createControlPlaneApiLayer(runtime.runtimeLayer)),
Layer.provideMerge(NodeHttpServer.layerTest),
);
const context = await Effect.runPromise(Layer.buildWithScope(serverLayer, scope));
const server = Context.get(context, HttpServer.HttpServer);
return {
runtime,
baseUrl: HttpServer.formatAddress(server.address),
close: async () => {
await Effect.runPromise(closeScope(scope));
await runtime.close();
},
};
} catch (error) {
await Effect.runPromise(closeScope(scope));
await runtime.close();
throw error;
}
};
const startProxyServer = async (input: {
targetBaseUrl: string;
delays?: ReadonlyArray;
}): Promise => {
const server = createServer(async (req, res) => {
try {
const method = req.method ?? "GET";
const url = new URL(req.url ?? "/", "http://127.0.0.1");
const rule = input.delays?.find(
(candidate) =>
candidate.method === method && candidate.pathname.test(url.pathname),
);
if (rule) {
await new Promise((resolve) => setTimeout(resolve, rule.durationMs));
}
const headers = new Headers();
for (const [name, value] of Object.entries(req.headers)) {
if (value === undefined) {
continue;
}
if (Array.isArray(value)) {
value.forEach((item) => headers.append(name, item));
continue;
}
headers.set(name, value);
}
headers.delete("host");
const bodyChunks: Array = [];
for await (const chunk of req) {
bodyChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
}
const targetUrl = new URL(`${url.pathname}${url.search}`, input.targetBaseUrl);
const response = await fetch(targetUrl, {
method,
headers,
body: bodyChunks.length > 0 ? Buffer.concat(bodyChunks) : undefined,
});
res.statusCode = response.status;
response.headers.forEach((value, name) => {
res.setHeader(name, value);
});
res.end(Buffer.from(await response.arrayBuffer()));
} catch (error) {
res.statusCode = 500;
res.end(error instanceof Error ? error.message : String(error));
}
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => resolve());
});
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Failed to bind proxy server");
}
const baseUrl = `http://127.0.0.1:${address.port}`;
return {
baseUrl,
close: () =>
new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
}),
};
};
type OpenApiSpecServer = RunningServer & {
specUrl: string;
};
const startOpenApiSpecServer = async (): Promise => {
const spec = {
openapi: "3.0.3",
info: {
title: "Hook Test API",
version: "1.0.0",
},
paths: {
"/ping": {
get: {
operationId: "ping",
responses: {
200: {
description: "ok",
content: {
"application/json": {
schema: {
type: "object",
properties: {
ok: { type: "boolean" },
},
},
},
},
},
},
},
},
},
};
const server = createServer((req, res) => {
if (req.url === "/openapi.json") {
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(spec));
return;
}
if (req.url === "/ping") {
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ ok: true }));
return;
}
res.statusCode = 404;
res.end("not found");
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => resolve());
});
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Failed to bind OpenAPI spec server");
}
const baseUrl = `http://127.0.0.1:${address.port}`;
return {
baseUrl,
specUrl: `${baseUrl}/openapi.json`,
close: () =>
new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
}),
};
};
async function requestJson(input: {
baseUrl: string;
path: string;
method?: "GET" | "POST" | "PATCH" | "DELETE";
payload?: unknown;
accountId?: string;
}): Promise {
const response = await fetch(new URL(input.path, input.baseUrl), {
method: input.method ?? "GET",
headers: {
...(input.accountId ? { "x-executor-account-id": input.accountId } : {}),
...(input.payload !== undefined ? { "content-type": "application/json" } : {}),
},
...(input.payload !== undefined
? {
body: JSON.stringify(input.payload),
}
: {}),
});
if (!response.ok) {
throw new Error(await response.text());
}
return response.json() as Promise;
}
const getInstallation = (baseUrl: string) =>
requestJson({
baseUrl,
path: "/v1/local/installation",
});
const seedStoredOpenApiSource = async (input: {
server: ApiServer;
installation: LocalInstallation;
name: string;
specUrl?: string;
}): Promise => {
const sourceDocumentText = JSON.stringify({
openapi: "3.0.3",
info: {
title: input.name,
version: "1.0.0",
},
paths: {
"/ping": {
get: {
operationId: "ping",
responses: {
200: {
description: "ok",
content: {
"application/json": {
schema: {
type: "object",
properties: {
ok: { type: "boolean" },
},
},
},
},
},
},
},
},
},
});
const specUrl = input.specUrl ?? `data:application/json,${encodeURIComponent(sourceDocumentText)}`;
return requestJson({
baseUrl: input.server.baseUrl,
path: `/v1/workspaces/${input.installation.workspaceId}/sources`,
method: "POST",
accountId: input.installation.accountId,
payload: {
name: input.name,
kind: "openapi",
endpoint: "https://example.com/api",
status: "connected",
enabled: true,
namespace: "hooks-test",
binding: {
specUrl,
},
auth: {
kind: "none",
},
},
});
};
async function renderExecutorHarness(useValue: () => T): Promise> {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
const snapshot: { current: T | null } = { current: null };
const Probe = () => {
const value = useValue();
React.useLayoutEffect(() => {
snapshot.current = value;
}, [value]);
return null;
};
await React.act(async () => {
root.render(
,
);
});
return {
get current() {
return snapshot.current;
},
unmount: async () => {
await React.act(async () => {
root.unmount();
});
container.remove();
},
};
}
const renderCreateHarness = () =>
renderExecutorHarness(() => ({
sources: useSources(),
createSource: useCreateSource(),
}));
const renderSourceHarness = (sourceId: string) =>
renderExecutorHarness(() => ({
sources: useSources(),
source: useSource(sourceId),
inspection: useSourceInspection(sourceId),
updateSource: useUpdateSource(),
removeSource: useRemoveSource(),
}));
async function waitForValue(
read: () => T | null,
predicate: (value: T) => boolean,
timeoutMs = 10_000,
): Promise {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const value = read();
if (value !== null && predicate(value)) {
return value;
}
await React.act(async () => {
await new Promise((resolve) => setTimeout(resolve, 20));
});
}
throw new Error("Timed out waiting for test state");
}
function isReady(
loadable: Loadable,
): loadable is { status: "ready"; data: T } {
return loadable.status === "ready";
}
function getReadyData(loadable: Loadable): T {
if (!isReady(loadable)) {
throw new Error("Expected loadable data to be ready");
}
return loadable.data;
}
describe("executor-react source hooks", () => {
it.effect("rejects invalid source creation before applying optimistic state", () =>
Effect.promise(async () => {
const apiServer = await startControlPlaneServer();
const proxyServer = await startProxyServer({
targetBaseUrl: apiServer.baseUrl,
delays: [
{
method: "POST",
pathname: /^\/v1\/workspaces\/[^/]+\/sources$/,
durationMs: 150,
},
],
});
setExecutorApiBaseUrl(proxyServer.baseUrl);
const harness = await renderCreateHarness();
try {
await waitForValue(
() => harness.current,
(value) => isReady(value.sources) && value.sources.data.length === 0,
);
let mutationPromise!: Promise<{ ok: true; value: Source } | { ok: false; error: unknown }>;
await React.act(async () => {
mutationPromise = harness.current!.createSource.mutateAsync({
name: "" as never,
kind: "openapi",
endpoint: "https://example.com",
binding: {
specUrl: "https://example.com/openapi.json",
},
}).then(
(value) => ({ ok: true as const, value }),
(error) => ({ ok: false as const, error }),
);
await Promise.resolve();
});
const failed = await waitForValue(
() => harness.current,
(value) =>
value.createSource.status === "error"
&& isReady(value.sources)
&& value.sources.data.length === 0,
);
const result = await mutationPromise;
expect(result.ok).toBe(false);
if (result.ok) {
throw new Error("Expected invalid source creation to fail");
}
expect(result.error).toBeInstanceOf(Error);
expect(failed.createSource.error).toBeInstanceOf(Error);
} finally {
await harness.unmount();
await proxyServer.close();
await apiServer.close();
}
}),
60_000,
);
it.effect("applies optimistic source updates and refreshes live inspection data after success", () =>
Effect.promise(async () => {
const apiServer = await startControlPlaneServer();
const proxyServer = await startProxyServer({
targetBaseUrl: apiServer.baseUrl,
delays: [
{
method: "PATCH",
pathname: /^\/v1\/workspaces\/[^/]+\/sources\/[^/]+$/,
durationMs: 150,
},
],
});
const specServer = await startOpenApiSpecServer();
setExecutorApiBaseUrl(proxyServer.baseUrl);
try {
const installation = await getInstallation(apiServer.baseUrl);
const source = await seedStoredOpenApiSource({
server: apiServer,
installation,
name: "Original Source",
specUrl: specServer.specUrl,
});
const harness = await renderSourceHarness(source.id);
try {
await waitForValue(
() => harness.current,
(value) =>
isReady(value.sources)
&& isReady(value.source)
&& isReady(value.inspection)
&& value.source.data.name === "Original Source"
&& value.inspection.data.source.name === "Original Source",
);
let mutationPromise!: Promise;
await React.act(async () => {
mutationPromise = harness.current!.updateSource.mutateAsync({
sourceId: source.id,
payload: {
name: "Renamed Source",
},
});
await Promise.resolve();
});
const optimistic = await waitForValue(
() => harness.current,
(value) =>
value.updateSource.status === "pending"
&& isReady(value.sources)
&& isReady(value.source)
&& isReady(value.inspection)
&& value.sources.data.some((item) => item.id === source.id && item.name === "Renamed Source")
&& value.source.data.name === "Renamed Source"
&& value.inspection.data.source.name === "Original Source",
);
expect(getReadyData(optimistic.source).name).toBe("Renamed Source");
expect(getReadyData(optimistic.inspection).source.name).toBe("Original Source");
let updated!: Source;
await React.act(async () => {
updated = await mutationPromise;
});
expect(updated.name).toBe("Renamed Source");
const refreshed = await waitForValue(
() => harness.current,
(value) =>
value.updateSource.status === "success"
&& isReady(value.source)
&& isReady(value.inspection)
&& value.source.data.name === "Renamed Source"
&& value.inspection.data.source.name === "Renamed Source",
);
expect(getReadyData(refreshed.inspection).source.name).toBe("Renamed Source");
} finally {
await harness.unmount();
}
} finally {
await specServer.close();
await proxyServer.close();
await apiServer.close();
}
}),
60_000,
);
it.effect("optimistically removes deleted sources and invalidates mounted source queries", () =>
Effect.promise(async () => {
const apiServer = await startControlPlaneServer();
const proxyServer = await startProxyServer({
targetBaseUrl: apiServer.baseUrl,
delays: [
{
method: "DELETE",
pathname: /^\/v1\/workspaces\/[^/]+\/sources\/[^/]+$/,
durationMs: 150,
},
],
});
setExecutorApiBaseUrl(proxyServer.baseUrl);
try {
const installation = await getInstallation(apiServer.baseUrl);
const source = await seedStoredOpenApiSource({
server: apiServer,
installation,
name: "Disposable Source",
});
const harness = await renderSourceHarness(source.id);
try {
await waitForValue(
() => harness.current,
(value) =>
isReady(value.sources)
&& isReady(value.source)
&& isReady(value.inspection)
&& value.sources.data.some((item) => item.id === source.id),
);
let mutationPromise!: Promise<{ removed: boolean }>;
await React.act(async () => {
mutationPromise = harness.current!.removeSource.mutateAsync(source.id);
await Promise.resolve();
});
const optimistic = await waitForValue(
() => harness.current,
(value) =>
value.removeSource.status === "pending"
&& isReady(value.sources)
&& !value.sources.data.some((item) => item.id === source.id),
);
expect(
getReadyData(optimistic.sources).some((item: Source) => item.id === source.id),
).toBe(false);
let removed!: { removed: boolean };
await React.act(async () => {
removed = await mutationPromise;
});
expect(removed.removed).toBe(true);
const invalidated = await waitForValue(
() => harness.current,
(value) =>
value.removeSource.status === "success"
&& isReady(value.sources)
&& value.sources.data.length === 0
&& value.source.status === "error"
&& value.inspection.status === "error",
);
expect(invalidated.source.status).toBe("error");
expect(invalidated.inspection.status).toBe("error");
} finally {
await harness.unmount();
}
} finally {
await proxyServer.close();
await apiServer.close();
}
}),
60_000,
);
it.effect("surfaces missing sources as errors instead of staying loading", () =>
Effect.promise(async () => {
const apiServer = await startControlPlaneServer();
setExecutorApiBaseUrl(apiServer.baseUrl);
try {
const harness = await renderSourceHarness("src_missing");
try {
const missing = await waitForValue(
() => harness.current,
(value) =>
isReady(value.sources)
&& value.sources.data.length === 0
&& value.source.status === "error"
&& value.inspection.status === "error",
);
expect(missing.source.status).toBe("error");
expect(missing.inspection.status).toBe("error");
if (missing.source.status === "error") {
expect(missing.source.error.message).toContain("Source not found");
}
if (missing.inspection.status === "error") {
expect(missing.inspection.error.message).toContain("Source not found");
}
} finally {
await harness.unmount();
}
} finally {
await apiServer.close();
}
}),
60_000,
);
});
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";