// @vitest-environment jsdom
import { act, render, waitFor } from "@testing-library/react";
import type { FC, PropsWithChildren } from "react";
import { describe, expect, it } from "vitest";
import {
RuntimeAdapterProvider,
useRemoteThreadListRuntime,
useRuntimeAdapters,
} from "@assistant-ui/core/react";
import { makeAdapter } from "./remote-thread-list-test-helpers";
import { useLocalRuntime } from "../legacy-runtime/runtime-cores/local/useLocalRuntime";
import { AssistantRuntimeProvider } from "../context";
import type { ChatModelAdapter, RemoteThreadListAdapter } from "../index";
import type {
RuntimeAdapters as RuntimeAdaptersShape,
ThreadHistoryAdapter,
} from "@assistant-ui/core";
type CapturedAdapters = RuntimeAdaptersShape | null;
const noOpAdapter: ChatModelAdapter = {
async *run() {},
};
const dummyHistory: ThreadHistoryAdapter = {
load: async () => ({ messages: [] }),
append: async () => {},
};
const makeRuntimeHook = (capture: { adapters: CapturedAdapters }) =>
function useTestRuntimeHook() {
capture.adapters = useRuntimeAdapters();
return useLocalRuntime(noOpAdapter);
};
async function renderAndWaitForBinder(
adapter: RemoteThreadListAdapter,
capture: { adapters: CapturedAdapters },
) {
const Inner: FC = () => {
const runtime = useRemoteThreadListRuntime({
runtimeHook: makeRuntimeHook(capture),
adapter,
});
return (
{null}
);
};
// first thread instance arrives asynchronously via switchToNewThread.
await act(async () => {
render();
});
await waitFor(() => expect(capture.adapters).not.toBeNull());
}
const wrapInRuntimeAdapterProvider = (
history: ThreadHistoryAdapter,
): FC =>
function Provider({ children }) {
return (
{children}
);
};
describe("RemoteThreadListAdapter.unstable_Provider", () => {
it("makes RuntimeAdapterProvider context visible to the runtime hook", async () => {
const capture: { adapters: CapturedAdapters } = { adapters: null };
const adapter = makeAdapter({
unstable_Provider: wrapInRuntimeAdapterProvider(dummyHistory),
});
await renderAndWaitForBinder(adapter, capture);
expect(capture.adapters?.history).toBe(dummyHistory);
});
it("preserves modelContext from the outer runtime-core RuntimeAdapterProvider", async () => {
const capture: { adapters: CapturedAdapters } = { adapters: null };
const adapter = makeAdapter({
unstable_Provider: wrapInRuntimeAdapterProvider(dummyHistory),
});
await renderAndWaitForBinder(adapter, capture);
expect(capture.adapters?.history).toBe(dummyHistory);
expect(capture.adapters?.modelContext).toBeDefined();
});
it("returns no history when no Provider is supplied", async () => {
const capture: { adapters: CapturedAdapters } = { adapters: null };
const adapter = makeAdapter();
await renderAndWaitForBinder(adapter, capture);
expect(capture.adapters?.history).toBeUndefined();
});
it("picks up a swapped Provider on re-render", async () => {
const capture: { adapters: CapturedAdapters } = { adapters: null };
const firstHistory: ThreadHistoryAdapter = {
load: async () => ({ messages: [] }),
append: async () => {},
};
const secondHistory: ThreadHistoryAdapter = {
load: async () => ({ messages: [] }),
append: async () => {},
};
const Inner: FC<{ history: ThreadHistoryAdapter }> = ({ history }) => {
const adapter = makeAdapter({
unstable_Provider: wrapInRuntimeAdapterProvider(history),
});
const runtime = useRemoteThreadListRuntime({
runtimeHook: makeRuntimeHook(capture),
adapter,
});
return (
{null}
);
};
let result: ReturnType;
await act(async () => {
result = render();
});
await waitFor(() => expect(capture.adapters?.history).toBe(firstHistory));
await act(async () => {
result!.rerender();
});
await waitFor(() => expect(capture.adapters?.history).toBe(secondHistory));
});
});