import { cp, mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai"; import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent"; import { expect, it } from "vitest"; import { createPiSession } from "../../../test/fixture/create-pi-session.ts"; it.skipIf(process.env["PI_EPHEMLETS_PACKAGE_TEST"] !== "1")( "relocated package compiles native source and runs delegated builder", async () => { const cwd = await mkdtemp(join(tmpdir(), "ephemlets-package-")); let fixture: Awaited> | undefined; try { const target = join(cwd, "relocated"); await cp( fileURLToPath(new URL("../../../dist/ephemlets", import.meta.url)), target, { recursive: true }, ); const pkg = JSON.parse( await readFile(join(target, "package.json"), "utf8"), ) as { pi: { extensions: string[] }; }; const entry = pkg.pi.extensions[0]; if (!entry) throw new Error("Ephemlets Pi extension entry missing"); fixture = await createPiSession({ cwd, extensionPaths: [join(target, entry)], }); expect(fixture.errors).toEqual([]); await fixture.session.bindExtensions({ mode: "tui", uiContext: { ...fixture.session.extensionRunner.getUIContext(), custom: async () => undefined, } as ExtensionUIContext, }); const owner = fixture; const tool = owner.extensions[0]?.tools.get("ephemlet")?.definition; if (!tool) throw new Error("Ephemlet tool missing"); const call = (params: Record) => tool.execute( "relocated", params, undefined, undefined, owner.session.extensionRunner.createToolContext( "relocated", undefined, ), ); const source = 'import { Text } from "@earendil-works/pi-tui"; export default (_ctx: unknown) => new Text("relocated", 0, 0)'; const put = await call({ action: "put", kind: "interactive", title: "Native", purpose: "Relocation", source, }); expect(put.content[0]?.type).toBe("text"); const listed = await call({ action: "list" }); if (listed.content[0]?.type !== "text") throw new Error("No list result"); const guidance = JSON.parse(listed.content[0].text).guidance as string; expect(guidance.startsWith(target)).toBe(true); expect(await readFile(guidance, "utf8")).toContain( "Native component contract", ); fixture.setResponses([ fauxAssistantMessage(fauxToolCall("submit_ephemlet", { source }), { stopReason: "toolUse", }), ]); const built = await call({ action: "build", title: "Delegated", purpose: "Relocation", brief: "Native Text component", wait: true, }); if (built.content[0]?.type !== "text") throw new Error("No build result"); expect(JSON.parse(built.content[0].text).status).toBe("ready"); } finally { await fixture?.dispose(); await rm(cwd, { recursive: true, force: true }); } }, 30_000, );