import { describe, expect, it, jest } from "bun:test"; import { encodeSchema } from "@noya-app/noya-schemas"; import { Type } from "@sinclair/typebox"; import { indexedDB } from "fake-indexeddb"; import { NoyaManager } from "../NoyaManager"; import { ClientToServerMessage } from "../multiplayer"; import { ParentToEmbeddedMessage } from "../sync/parentFrameMessages"; import { createEmbeddedHost } from "../sync/embeddedHost"; import { RouteEmbeddedMultiplayerOptions } from "../sync/parentMultiplayerRouter"; globalThis.indexedDB = indexedDB; function createTestManager() { return new NoyaManager(null); } describe("createEmbeddedHost", () => { it("routes child multiplayer messages through remote connection and echoes responses to child", () => { const noyaManager = createTestManager(); const sendToChild = jest.fn(); const remoteSend = jest.fn(); const remoteDestroy = jest.fn(); const createRemoteConnection = jest.fn(() => ({ sendMessage: remoteSend, destroy: remoteDestroy, })); const routeEmbeddedMultiplayerMessage = jest.fn( ( _manager: NoyaManager, payload: ClientToServerMessage, options: RouteEmbeddedMultiplayerOptions ) => { options.sendRemoteMessage?.(payload); const response: ParentToEmbeddedMessage = { type: "connection", state: "connected", }; options.sendToChild(response); } ); const host = createEmbeddedHost( noyaManager, { multiplayerUrl: "https://example.com", offlineStorageKey: "local-key", sendToChild, }, { createRemoteConnection, routeEmbeddedMultiplayerMessage, } ); const payload: ClientToServerMessage = { type: "ping", id: "abc" }; host.handleChildMultiplayer(payload); expect(routeEmbeddedMultiplayerMessage).toHaveBeenCalledWith( noyaManager, payload, expect.objectContaining({ isRemoteActive: true, localStorageKey: "local-key", sendRemoteMessage: expect.any(Function), sendToChild, }) ); expect(remoteSend).toHaveBeenCalledWith(payload); expect(sendToChild).toHaveBeenCalledWith({ type: "connection", state: "connected", }); host.destroy(); expect(remoteDestroy).toHaveBeenCalled(); }); it("bridges streamed AI generation through the offline parent", async () => { const noyaManager = createTestManager(); const sendToChild = jest.fn(); const host = createEmbeddedHost(noyaManager, { offlineStorageKey: "local-key", sendToChild, }); const schema = Type.Object({ title: Type.String({ default: "Local todo" }), completed: Type.Boolean(), }); await host.bridgeChildRpc({ id: "ai-request", url: "/api/ai/generate", streaming: true, options: { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: "Create a todo", schema, encodedSchema: encodeSchema(schema), stream: true, }), }, }); const messages = sendToChild.mock.calls .map(([message]) => message) .filter((message) => message.id === "ai-request"); expect(messages.map((message) => message.type)).toEqual([ "chunk", "chunk", "chunk", "chunk", "end", ]); expect( messages.slice(0, -1).map((message) => JSON.parse(message.chunk)) ).toEqual([ { type: "partial-object", object: {} }, { type: "partial-object", object: { title: "Local todo" } }, { type: "partial-object", object: { title: "Local todo", completed: false }, }, { type: "object", object: { title: "Local todo", completed: false }, }, ]); expect(JSON.parse(messages.at(-1).response.body)).toEqual({ object: { title: "Local todo", completed: false }, }); host.destroy(); }); });