› cavi-ai/
GitHub ↗

Verified example · TSX

React answer.test

examples/react-answer.test.tsx

@cavi-ai/api-clientv0.21.007f0ee45
TSX · complete source
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from "vitest";
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { createApiClient, createRuntimeProviderRegistry } from "@cavi-ai/api-client";
import { createCodexProviderModule } from "@cavi-ai/api-client/providers/codex/runtime";
import { AnswerForm } from "./react-answer.js";
import { createRunHandler } from "./server-handler.js";

(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const mounted: { root: Root; element: HTMLDivElement }[] = [];
const clients: { dispose(): Promise<void> }[] = [];

afterEach(async () => {
  for (const { root, element } of mounted.splice(0)) {
    await act(async () => { root.unmount(); });
    element.remove();
  }
  for (const client of clients.splice(0)) await client.dispose();
});

async function mount(fetchImpl: typeof fetch, endpoint = "/answer") {
  const element = document.createElement("div");
  document.body.append(element);
  const root = createRoot(element);
  mounted.push({ root, element });
  const render = async (url: string) => {
    await act(async () => { root.render(<React.StrictMode><AnswerForm endpoint={url} fetchImpl={fetchImpl} /></React.StrictMode>); });
  };
  await render(endpoint);
  const submit = (input = "Question") => {
    element.querySelector("textarea")!.value = input;
    element.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
  };
  const cancel = () => element.querySelector<HTMLButtonElement>('button[type="button"]')!.click();
  return { element, root, render, submit, cancel };
}

function deferred() {
  let resolve!: (response: Response) => void;
  const promise = new Promise<Response>((done) => { resolve = done; });
  return { promise, resolve };
}

function server(reply: () => Response) {
  const providerBodies: unknown[] = [];
  const browserBodies: unknown[] = [];
  const client = createApiClient("codex", {
    registry: createRuntimeProviderRegistry({ modules: [createCodexProviderModule({ apiKey: "server-fixture-key" })] }),
    fetchImpl: async (_url, init) => {
      providerBodies.push(JSON.parse(String(init?.body)));
      return reply();
    },
  });
  clients.push(client);
  const handle = createRunHandler(client, "server-model", () => {});
  let response: Promise<Response>;
  const fetchImpl: typeof fetch = (url, init) => {
    browserBodies.push(JSON.parse(String(init?.body)));
    response = handle(new Request(new URL(String(url), "https://application.example"), init));
    return response;
  };
  return { fetchImpl, providerBodies, browserBodies, response: () => response };
}

describe("React answer workflow", () => {
  it("renders the real handler's answer and keeps model and credentials on the server", async () => {
    const app = server(() => Response.json({ id: "run-1", status: "completed", output_text: "Answer" }));
    const ui = await mount(app.fetchImpl);
    await act(async () => { ui.submit(); await app.response(); });
    expect(ui.element.querySelector("pre")?.textContent).toBe("Answer");
    expect(ui.element.textContent).toContain("Completed run run-1");
    expect(app.browserBodies).toEqual([{ input: "Question" }]);
    expect(app.providerBodies).toMatchObject([{ input: "Question", model: "server-model" }]);
  });

  it("keeps an active run pending without polling or displaying a completed answer", async () => {
    const app = server(() => Response.json({ id: "run-1", status: "queued" }));
    const ui = await mount(app.fetchImpl);
    await act(async () => { ui.submit(); await app.response(); });
    expect(ui.element.textContent).toContain("Pending run run-1 (started)");
    expect(ui.element.querySelector("pre")).toBeNull();
    expect(app.browserBodies).toHaveLength(1);
  });

  it("renders controlled failures without exposing provider diagnostics", async () => {
    const app = server(() => Response.json({ error: { message: "Private credential diagnostic" } }, { status: 401 }));
    const ui = await mount(app.fetchImpl);
    await act(async () => { ui.submit(); await app.response(); });
    expect(ui.element.querySelector('[role="alert"]')?.textContent).toContain("runtime is unavailable");
    expect(ui.element.textContent).not.toContain("Private");
    expect(ui.element.textContent).not.toContain("server-fixture-key");
  });

  it("rejects missing text and mismatched success states, preserving a failed run ID", async () => {
    let reply = Response.json({ runId: "run-1", status: "completed" });
    const ui = await mount(async () => reply);
    for (const response of [reply, Response.json({ runId: "run-1", status: "completed", text: "Wrong state" }, { status: 202 })]) {
      reply = response;
      await act(async () => { ui.submit(); });
      expect(ui.element.querySelector('[role="alert"]')).not.toBeNull();
      expect(ui.element.querySelector("pre")).toBeNull();
    }
    reply = Response.json({ runId: "failed-1", status: "failed", error: "Private diagnostic" }, { status: 502 });
    await act(async () => { ui.submit(); });
    expect(ui.element.textContent).toContain("failed-1");
    expect(ui.element.textContent).not.toContain("Private diagnostic");
  });

  it("ignores an older response after local cancellation and a newer submission", async () => {
    const older = deferred();
    const newer = deferred();
    const signals: AbortSignal[] = [];
    const ui = await mount(async (_url, init) => {
      signals.push(init!.signal!);
      return signals.length === 1 ? older.promise : newer.promise;
    });
    await act(async () => { ui.submit("Old"); });
    expect(ui.element.querySelector<HTMLButtonElement>('button[type="submit"]')!.disabled).toBe(true);
    await act(async () => { ui.cancel(); ui.submit("New"); });
    expect(signals[0].aborted).toBe(true);
    await act(async () => { newer.resolve(Response.json({ runId: "new-1", status: "completed", text: "New answer" })); });
    await act(async () => { older.resolve(Response.json({ runId: "old-1", status: "completed", text: "Old answer" })); });
    expect(ui.element.querySelector("pre")?.textContent).toBe("New answer");
  });

  it("keeps local cancellation visible when an ignored abort still produces a response", async () => {
    const pending = deferred();
    let signal: AbortSignal | undefined;
    let calls = 0;
    const ui = await mount(async (_url, init) => { calls += 1; signal = init!.signal!; return pending.promise; });
    await act(async () => { ui.submit(); });
    await act(async () => { ui.cancel(); });
    expect(signal!.aborted).toBe(true);
    expect(ui.element.textContent).toContain("Stopped waiting locally");
    await act(async () => { pending.resolve(Response.json({ runId: "late-1", status: "completed", text: "Late answer" })); });
    expect(ui.element.textContent).not.toContain("Late answer");
    expect(ui.element.textContent).toContain("Server work may continue");
    expect(calls).toBe(1);
  });

  it("aborts and invalidates requests when the endpoint changes or the component unmounts", async () => {
    const pending = deferred();
    const signals: AbortSignal[] = [];
    const ui = await mount(async (_url, init) => { signals.push(init!.signal!); return pending.promise; });
    await act(async () => { ui.submit(); });
    await ui.render("/other-answer");
    expect(signals[0].aborted).toBe(true);
    expect(ui.element.textContent).toContain("Ask a question to start");
    await act(async () => { pending.resolve(Response.json({ runId: "stale-1", status: "completed", text: "Stale answer" })); });
    expect(ui.element.querySelector("pre")).toBeNull();
    const last = deferred();
    const unmounted = await mount(async (_url, init) => { signals.push(init!.signal!); return last.promise; });
    await act(async () => { unmounted.submit(); });
    await act(async () => { unmounted.root.unmount(); });
    mounted.splice(mounted.findIndex((entry) => entry.root === unmounted.root), 1);
    expect(signals.at(-1)!.aborted).toBe(true);
    await act(async () => { last.resolve(Response.json({ runId: "late-1", status: "completed", text: "Late answer" })); });
    expect(unmounted.element.textContent).toBe("");
    unmounted.element.remove();
  });
});

Package imports

Referenced by

Artifact provenance
Package
@cavi-ai/api-client@0.21.0
Release
v0.21.0
Commit
07f0ee45424c267278f7b0c89a7dcea4e2492cc1
Source
examples/react-answer.test.tsx
Tarball SHA-256
0924b5d3a7a40869bd44fedc47a9b8eb9844c2af1fd5c7ca77469d8b7c0b8558
Content SHA-256
24eff463c41582eaf5e70c58f42e5ae13bd12d02fa14ae2a886afa3abf5cdf48