754 lines · 20.5 KB · TypeScript
RawDownload
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("<!doctype html><html><body></body></html>", {  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<void>;}; type ApiServer = RunningServer & {  runtime: ControlPlaneRuntime;}; type HookHarness<T> = {  current: T | null;  unmount: () => Promise<void>;}; type CreateHarnessState = {  sources: Loadable<ReadonlyArray<Source>>;  createSource: ReturnType<typeof useCreateSource>;}; type SourceHarnessState = {  sources: Loadable<ReadonlyArray<Source>>;  source: Loadable<Source>;  inspection: ReturnType<typeof useSourceInspection>;  updateSource: ReturnType<typeof useUpdateSource>;  removeSource: ReturnType<typeof useRemoveSource>;}; const closeScope = (scope: Scope.CloseableScope) =>  Scope.close(scope, Exit.void).pipe(Effect.orDie); const startControlPlaneServer = async (): Promise<ApiServer> => {  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<DelayRule>;}): Promise<RunningServer> => {  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<Uint8Array> = [];      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<void>((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<void>((resolve, reject) => {        server.close((error) => {          if (error) {            reject(error);            return;          }           resolve();        });      }),  };}; type OpenApiSpecServer = RunningServer & {  specUrl: string;}; const startOpenApiSpecServer = async (): Promise<OpenApiSpecServer> => {  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<void>((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<void>((resolve, reject) => {        server.close((error) => {          if (error) {            reject(error);            return;          }           resolve();        });      }),  };}; async function requestJson<T>(input: {  baseUrl: string;  path: string;  method?: "GET" | "POST" | "PATCH" | "DELETE";  payload?: unknown;  accountId?: string;}): Promise<T> {  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<T>;} const getInstallation = (baseUrl: string) =>  requestJson<LocalInstallation>({    baseUrl,    path: "/v1/local/installation",  }); const seedStoredOpenApiSource = async (input: {  server: ApiServer;  installation: LocalInstallation;  name: string;  specUrl?: string;}): Promise<Source> => {  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<Source>({    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<T>(useValue: () => T): Promise<HookHarness<T>> {  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(      <ExecutorReactProvider>        <Probe />      </ExecutorReactProvider>,    );  });   return {    get current() {      return snapshot.current;    },    unmount: async () => {      await React.act(async () => {        root.unmount();      });      container.remove();    },  };} const renderCreateHarness = () =>  renderExecutorHarness<CreateHarnessState>(() => ({    sources: useSources(),    createSource: useCreateSource(),  })); const renderSourceHarness = (sourceId: string) =>  renderExecutorHarness<SourceHarnessState>(() => ({    sources: useSources(),    source: useSource(sourceId),    inspection: useSourceInspection(sourceId),    updateSource: useUpdateSource(),    removeSource: useRemoveSource(),  })); async function waitForValue<T>(  read: () => T | null,  predicate: (value: T) => boolean,  timeoutMs = 10_000,): Promise<T> {  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<T>(  loadable: Loadable<T>,): loadable is { status: "ready"; data: T } {  return loadable.status === "ready";} function getReadyData<T>(loadable: Loadable<T>): 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({

Lines 1–500 of 754.