import { readdir, readFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { fauxAssistantMessage, fauxToolCall, getCurrentSystemPrompt, getCurrentTools, } from "@earendil-works/pi-ai"; import { convertResponsesTools } from "@earendil-works/pi-ai/api/openai-responses-shared"; import { type AgentToolResult, createAgentSession, DefaultResourceLoader, type ExtensionUIContext, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent"; import type { Component, TUI } from "@earendil-works/pi-tui"; import { Value } from "typebox/value"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createPiSession, type PiSession, } from "../../../test/fixture/create-pi-session.ts"; import type { EphemletParams } from "../runtime.ts"; let fixture: PiSession | undefined; const source = "export default (ctx) => ({render: () => [String(ctx.state)], invalidate() {}})"; afterEach(async () => { await fixture?.dispose(); fixture = undefined; }); async function setup(debug = false) { fixture = await createPiSession({ extensionPaths: [fileURLToPath(new URL("../index.ts", import.meta.url))], ...(debug ? { env: { PI_EPHEMLETS_DEBUG: "1" } } : {}), }); expect(fixture.errors).toEqual([]); await fixture.session.bindExtensions({ mode: "tui", uiContext: { ...fixture.session.extensionRunner.getUIContext(), custom: async () => undefined, } as ExtensionUIContext, }); expect(fixture.session.getActiveToolNames()).toContain("ephemlet"); const definition = fixture.extensions[0]!.tools.get("ephemlet")!.definition; expect(definition.annotations?.readOnlyHint).toBe(false); const session = fixture.session; const call = async ( params: EphemletParams, ): Promise> => definition.execute( "ephemlet-test", params, undefined, undefined, session.extensionRunner.createToolContext("ephemlet-test", undefined), ); return { fixture, call, session }; } function data(result: AgentToolResult): Record { const content = result.content[0]; if (content?.type !== "text") throw new Error("Missing text result"); return JSON.parse(content.text) as Record; } describe("ephemlets public Pi runtime", () => { it("passes the authoring guidance file to the delegated builder", async () => { const { call, fixture } = await setup(); fixture.setResponses([ (context) => { expect(getCurrentSystemPrompt(context.messages)).toContain( "Persisted source is loaded by jiti", ); return fauxAssistantMessage( fauxToolCall("submit_ephemlet", { source }), { stopReason: "toolUse" }, ); }, ]); const built = data( await call({ action: "build", title: "Guidance", purpose: "Verify builder guidance", brief: "Build a component", wait: true, }), ); expect(built["status"]).toBe("ready"); }); it("declares ephemlet to the main model and handles a real tool turn", async () => { const { call, session, fixture } = await setup(); fixture.setResponses([ (context) => { expect( getCurrentTools(context.messages).map((tool) => tool.name), ).toContain("ephemlet"); return fauxAssistantMessage( fauxToolCall("ephemlet", { action: "put", title: "Progress", purpose: "Track actual work", source: "# Verified progress", }), { stopReason: "toolUse" }, ); }, fauxAssistantMessage("Report created"), ]); await session.prompt("Create a living progress report"); expect(data(await call({ action: "list" }))["ephemlets"]).toHaveLength(1); }); it("declares provider-compatible recursive JSON state without narrowing stored values", async () => { const { call, fixture, session } = await setup(); const tool = fixture.extensions[0]!.tools.get("ephemlet")!.definition; const declaration = convertResponsesTools([tool])[0]!; if (declaration.type !== "function") throw new Error("Expected function tool"); expect(declaration.strict).toBe(false); expect(convertResponsesTools([tool], { strict: null })[0]).toMatchObject({ type: "function", strict: null, parameters: declaration.parameters, }); expect(declaration.parameters).toMatchObject({ type: "object", properties: { state: { $ref: "#/$defs/json" } }, $defs: { json: { anyOf: [ { type: "null" }, { type: "boolean" }, { type: "number" }, { type: "string" }, { type: "array", items: { $ref: "#/$defs/json" } }, { type: "object", additionalProperties: { $ref: "#/$defs/json" } }, ], }, }, }); const wireSchema = declaration.parameters as typeof tool.parameters; const created = data( await call({ action: "put", title: "JSON", purpose: "Schema contract", source: "# JSON", }), ); const id = created["id"] as string; for (const state of [ null, false, 0, "", [], {}, [1, { nested: [true, null] }], { nested: { values: ["A", false, null] } }, ]) { const params = { action: "state" as const, id, expectedRevision: 1, state, }; expect(Value.Check(wireSchema, params)).toBe(true); const updated = await call(params); expect(Value.Check(tool.outputSchema!, updated.structuredContent)).toBe( true, ); const snapshot = await call({ action: "get", id, include: ["state"] }); expect(data(snapshot)["state"]).toEqual(state); expect(Value.Check(tool.outputSchema!, snapshot.structuredContent)).toBe( true, ); } for (const state of [ { nested: undefined }, { nested: () => 1 }, { nested: 1n }, ]) { expect( Value.Check(wireSchema, { action: "state", id, expectedRevision: 1, state, }), ).toBe(false); } const state = { code: "001", nested: [null, false, 0, { value: "true" }] }; fixture.setResponses([ fauxAssistantMessage( fauxToolCall("ephemlet", { action: "state", id, expectedRevision: 1, state, }), { stopReason: "toolUse" }, ), fauxAssistantMessage("State updated"), ]); await session.prompt("Persist the JSON state"); expect( data(await call({ action: "get", id, include: ["state"] }))["state"], ).toEqual(state); }); it("uses native sequential execution, structured outputs, and interactive exposure", async () => { const { call, fixture } = await setup(); const tool = fixture.extensions[0]!.tools.get("ephemlet")!.definition; expect(tool.exposure).toBe("model-only"); expect(tool.executionMode).toBe("sequential"); expect(tool.constrainedSampling).toEqual({ type: "json_schema", strict: "prefer", }); expect(tool.renderCall).toBeTypeOf("function"); expect(tool.renderResult).toBeTypeOf("function"); const checked = async (params: EphemletParams) => { const result = await call(params); expect(result.structuredContent).toEqual(data(result)); expect(Value.Check(tool.outputSchema!, result.structuredContent)).toBe( true, ); return data(result); }; const created = await checked({ action: "put", title: "Report", purpose: "Track", source: "# Report", }); const id = created["id"] as string; await checked({ action: "list" }); await checked({ action: "get", id }); await checked({ action: "get", id, include: ["source", "state"] }); await checked({ action: "state", id, expectedRevision: 1, state: { selected: "A" }, }); await checked({ action: "hide" }); await checked({ action: "show", id }); await checked({ action: "delete", id }); }); it("bounds default data reads and keeps persisted content out of automatic model context", async () => { const { call, session, fixture } = await setup(); const oldSource = `private-original${"x".repeat(60000)}`; const newSource = `private-source${"y".repeat(60000)}`; const state = { choice: "A", payload: `private-state${"z".repeat(60000)}` }; const created = data( await call({ action: "put", title: "Bounded", purpose: "p".repeat(4096), source: oldSource, state, }), ); const id = created["id"] as string; await call({ action: "put", id, expectedRevision: 1, source: newSource }); const summary = data(await call({ action: "get", id })); expect(summary).toMatchObject({ id, revision: 2, status: "ready" }); expect(summary).not.toHaveProperty("source"); expect(summary).not.toHaveProperty("state"); expect(summary).not.toHaveProperty("fallback"); expect(summary["purpose"]).toBe(`${"p".repeat(159)}…`); expect(JSON.stringify(summary).length).toBeLessThan(1000); const list = data(await call({ action: "list" })); expect(JSON.stringify(list).length).toBeLessThan(2000); expect(list["ephemlets"]).toEqual([summary]); const withSource = data( await call({ action: "get", id, include: ["source"] }), ); expect(withSource["source"]).toBe(newSource); expect(withSource).not.toHaveProperty("state"); expect(withSource).not.toHaveProperty("fallback"); const withState = data( await call({ action: "get", id, include: ["state"] }), ); expect(withState["state"]).toEqual(state); expect(withState).not.toHaveProperty("source"); expect(withState).not.toHaveProperty("fallback"); fixture.setResponses([ (context) => { const prompt = getCurrentSystemPrompt(context.messages); expect(prompt).toContain("agent cannot see the rendered panel"); expect(prompt).toContain('include: ["state"]'); expect(prompt).toContain("do not start a new turn"); expect(prompt).toContain("exact snapshot"); const transcript = JSON.stringify(context.messages); for (const marker of [ "private-original", "private-source", "private-state", ]) expect(transcript).not.toContain(marker); return fauxAssistantMessage("OK"); }, ]); await session.prompt("Continue the task"); }); it.each(["ready", "failed"] as const)( "delivers background %s notices without starting a main-agent turn", async (status) => { const { call, session, fixture } = await setup(); const send = vi.spyOn(session, "sendCustomMessage"); fixture.setResponses([ status === "ready" ? fauxAssistantMessage(fauxToolCall("submit_ephemlet", { source }), { stopReason: "toolUse", }) : fauxAssistantMessage("No structured result"), ]); const pending = data( await call({ action: "build", title: "Background", purpose: "Monitor", brief: "Native counter", }), ); await vi.waitFor(async () => { expect( data(await call({ action: "get", id: pending["id"] as string }))[ "status" ], ).toBe(status); expect(send).toHaveBeenCalledWith( expect.objectContaining({ customType: "ephemlet-build", display: false, details: expect.objectContaining({ id: pending["id"], jobId: pending["jobId"], status, }), }), { triggerTurn: false }, ); }); expect(fixture.providerState.callCount).toBe(1); const notice = send.mock.calls[0]![0]; expect(JSON.stringify(notice)).not.toContain(source); fixture.setResponses([ (context) => { expect(JSON.stringify(context.messages)).toContain( status === "ready" ? "Ephemlet ready" : "Ephemlet build failed", ); return fauxAssistantMessage("Status received"); }, ]); await session.prompt("Check the background build"); send.mockRestore(); }, ); it("hides tools and leaves session data untouched in non-TUI modes", async () => { const { call, fixture, session } = await setup(); await call({ action: "put", title: "Report", purpose: "Track", source: "# Report", }); const before = session.sessionManager.getBranch().length; await session.bindExtensions({ mode: "print" }); expect( fixture.extensions[0]!.tools.get("ephemlet")!.definition.exposure, ).toBe("hidden"); expect(session.getActiveToolNames()).not.toContain("ephemlet"); const hidden = fixture.extensions[0]!.tools.get("ephemlet")!.definition; await expect( hidden.execute( "hidden", { action: "list" }, undefined, undefined, session.extensionRunner.createToolContext("hidden", undefined), ), ).rejects.toThrow("TUI"); expect(session.sessionManager.getBranch().length).toBe(before); }); it("registers tool/commands and persists reports through restore and deletion", async () => { const { call, session, fixture } = await setup(); expect(fixture.extensions[0]!.commands.has("ephemlets")).toBe(true); const first = data( await call({ action: "put", title: "Progress", purpose: "Track original request", source: "# Progress\n- Attempted\n- Verified", }), ); const id = first["id"] as string; await call({ action: "hide" }); await session.extensionRunner.emit({ type: "session_start", reason: "resume", }); expect( data(await call({ action: "get", id, include: ["source"] }))["source"], ).toContain("Verified"); expect(data(await call({ action: "list" }))["ephemlets"]).toHaveLength(1); await call({ action: "delete", id }); await session.extensionRunner.emit({ type: "session_start", reason: "resume", }); expect(data(await call({ action: "list" }))["ephemlets"]).toEqual([]); }); it("restores persisted native source/state into a new Pi session", async () => { const { call, fixture } = await setup(); const created = data( await call({ action: "put", title: "Counter", purpose: "Persist", kind: "interactive", source, state: 7, }), ); await call({ action: "hide" }); const manager = SessionManager.create( fixture.cwd, join(fixture.cwd, "saved-sessions"), ); manager.appendMessage({ role: "user", content: "Persistence fixture", timestamp: Date.now(), }); for (const entry of fixture.session.sessionManager.getBranch()) if (entry.type === "custom") manager.appendCustomEntry(entry.customType, entry.data); const path = manager.getSessionFile(); if (!path) throw new Error("No persisted file"); const settings = SettingsManager.inMemory({}); const loader = new DefaultResourceLoader({ cwd: fixture.cwd, agentDir: fixture.agentDir, settingsManager: settings, noExtensions: true, noSkills: true, noThemes: true, noPromptTemplates: true, noContextFiles: true, additionalExtensionPaths: [ fileURLToPath(new URL("../index.ts", import.meta.url)), ], }); await loader.reload(); const model = fixture.session.model; if (!model) throw new Error("Fixture model is unavailable"); const { session: reopened } = await createAgentSession({ cwd: fixture.cwd, modelRuntime: fixture.session.modelRuntime, model, settingsManager: settings, resourceLoader: loader, sessionManager: SessionManager.open(path), }); try { await reopened.bindExtensions({ mode: "tui", uiContext: { ...fixture.session.extensionRunner.getUIContext(), custom: async () => undefined, } as ExtensionUIContext, }); const tool = loader .getExtensions() .extensions[0]!.tools.get("ephemlet")!.definition; const restored = data( await tool.execute( "restored", { action: "get", id: created["id"], include: ["source", "state"] }, undefined, undefined, reopened.extensionRunner.createToolContext("restored", undefined), ), ); expect(restored).toMatchObject({ source, state: 7, revision: 1 }); fixture.setResponses([ (_context, options) => { expect(options?.apiKey).toBe("test-key"); return fauxAssistantMessage( fauxToolCall("submit_ephemlet", { source }), { stopReason: "toolUse" }, ); }, ]); await tool.execute( "linked-build", { action: "build", title: "Linked", purpose: "Native tracking", brief: "Native counter", wait: true, }, undefined, undefined, reopened.extensionRunner.createToolContext("linked-build", undefined), ); const files = await readdir(manager.getSessionDir()); const children = files.filter( (file) => file.endsWith(".jsonl") && join(manager.getSessionDir(), file) !== path, ); expect(children).toHaveLength(1); const entries = ( await readFile(join(manager.getSessionDir(), children[0]!), "utf8") ) .trim() .split("\n") .map((line) => JSON.parse(line)); expect(entries[0]).toMatchObject({ type: "session", parentSession: path, }); expect( entries.some( (entry) => entry.type === "message" && entry.message.role === "assistant", ), ).toBe(true); } finally { await reopened.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); reopened.dispose(); } }); it("Save queues a snapshot follow-up without interrupting ongoing main-agent work", async () => { const { call, session, fixture } = await setup(); let panel: Component | undefined; const tui = { terminal: { columns: 120, rows: 32 }, requestRender() {}, showOverlay(component: Component) { panel = component; return { hide() {}, setHidden() {}, focus() {} }; }, } as unknown as TUI; await session.bindExtensions({ mode: "tui", uiContext: { ...session.extensionRunner.getUIContext(), setWidget(_key: string, factory: unknown) { if (typeof factory === "function") factory(tui, { fg: (_color: string, text: string) => text }); }, } as ExtensionUIContext, }); const created = data( await call({ action: "put", title: "Report", purpose: "Persist", source: "# Report", state: 0, }), ); let release = () => {}; const gate = new Promise((resolve) => { release = resolve; }); let started = false; let savePrompt = ""; fixture.setResponses([ async () => { started = true; await gate; return fauxAssistantMessage("Finished original work"); }, (context) => { const latest = context.messages .filter((message) => message.role === "user") .at(-1); savePrompt = JSON.stringify(latest?.content); return fauxAssistantMessage("Saved"); }, ]); const running = session.prompt("Long-running work"); try { await vi.waitFor(() => expect(started).toBe(true)); if (!panel?.handleInput) throw new Error("Panel not mounted"); panel.handleInput("\x13"); await call({ action: "state", id: created["id"] as string, expectedRevision: 1, state: 9, }); expect(fixture.providerState.callCount).toBe(1); release(); await running; await vi.waitFor(() => expect(savePrompt).toContain("ephemlet-save-snapshot"), ); expect(savePrompt).toContain('\\"state\\":0'); expect(savePrompt).not.toContain('\\"state\\":9'); expect( data( await call({ action: "get", id: created["id"] as string, include: ["state"], }), )["state"], ).toBe(9); } finally { release(); await running; } }); it("stores interactive source/state and preserves revision on invalid source update", async () => { const { call } = await setup(); const first = data( await call({ action: "put", title: "Counter", purpose: "Explore", kind: "interactive", source, state: 0, }), ); const id = first["id"] as string; await expect( call({ action: "put", id, expectedRevision: 1, source: "export default function (", }), ).rejects.toThrow(); expect( data(await call({ action: "get", id, include: ["source"] })), ).toMatchObject({ source, revision: 1, }); await call({ action: "state", id, expectedRevision: 1, state: 8 }); expect( data(await call({ action: "get", id, include: ["state"] }))["state"], ).toBe(8); }); it("delegates to real child AgentSession via parent provider and returns usage", async () => { const { call, fixture } = await setup(); fixture.setResponses([ fauxAssistantMessage(fauxToolCall("submit_ephemlet", { source }), { stopReason: "toolUse", }), ]); const builtResult = await call({ action: "build", title: "Counter", purpose: "Interactive explanation", brief: "Native counter", state: 0, wait: true, }); const built = data(builtResult); expect(built["status"]).toBe("ready"); expect(built["revision"]).toBe(2); expect(builtResult.usage).toBeDefined(); expect(fixture.providerState.callCount).toBe(1); expect( data( await call({ action: "get", id: built["id"] as string, include: ["source"], }), )["source"], ).toBe(source); }); it("routes virtual builder models through the parent with real provider auth", async () => { const { call, session, fixture } = await setup(); const physical = session.model; if (!physical) throw new Error("No fixture model"); session.modelRuntime.registerVirtualModel({ provider: physical.provider, id: "ephemlet-router", name: "Ephemlet router", route: () => ({ model: physical, thinkingLevel: "off" }), }); const virtual = session.modelRuntime.getModel( physical.provider, "ephemlet-router", ); if (!virtual) throw new Error("Virtual fixture model missing"); await session.setModel(virtual); fixture.setResponses([ (_context, options) => { expect(options?.apiKey).toBe("test-key"); return fauxAssistantMessage( fauxToolCall("submit_ephemlet", { source }), { stopReason: "toolUse" }, ); }, ]); expect( data( await call({ action: "build", title: "Virtual", purpose: "Provider routing", brief: "Native counter", wait: true, }), )["status"], ).toBe("ready"); }); it("respects explicit tool deactivation across TUI rebind", async () => { const { session } = await setup(); session.setActiveToolsByName( session.getActiveToolNames().filter((name) => name !== "ephemlet"), ); await session.bindExtensions({ mode: "tui", uiContext: { ...session.extensionRunner.getUIContext(), custom: async () => undefined, } as ExtensionUIContext, }); expect(session.getActiveToolNames()).not.toContain("ephemlet"); }); it("background build returns stable ID before completion and Close prevents resurrection", async () => { const { call, fixture } = await setup(); let release = () => {}; const gate = new Promise((resolve) => { release = resolve; }); let started = false; let finished = false; let wasAborted = false; fixture.setResponses([ async (_context, options) => { started = true; await gate; wasAborted = options?.signal?.aborted ?? false; finished = true; return fauxAssistantMessage( fauxToolCall("submit_ephemlet", { source }), { stopReason: "toolUse" }, ); }, ]); const pending = data( await call({ action: "build", title: "Background", purpose: "Monitor", brief: "Create native monitor", }), ); expect(pending["status"]).toBe("building"); expect(typeof pending["jobId"]).toBe("string"); await vi.waitFor(() => expect(started).toBe(true)); await call({ action: "delete", id: pending["id"] as string }); release(); await vi.waitFor(() => expect(finished).toBe(true)); expect(wasAborted).toBe(true); expect(data(await call({ action: "list" }))["ephemlets"]).toEqual([]); }); it("failed rebuild retains prior source and exposes failure", async () => { const { call, fixture } = await setup(); const first = data( await call({ action: "put", title: "Counter", purpose: "Explore", kind: "interactive", source, }), ); fixture.setResponses([fauxAssistantMessage("No structured result")]); await expect( call({ action: "build", id: first["id"] as string, expectedRevision: 1, brief: "Adapt it", wait: true, }), ).rejects.toThrow("without submitting"); expect( data( await call({ action: "get", id: first["id"] as string, include: ["source"], }), ), ).toMatchObject({ source, status: "failed" }); }); it("marks interrupted restored build failed rather than leaving permanent spinner", async () => { const { call, session } = await setup(); const first = data( await call({ action: "put", title: "Pending", purpose: "Monitor", kind: "interactive", source, }), ); const existing = data( await call({ action: "get", id: first["id"] as string, include: ["source", "state"], }), ); session.sessionManager.appendCustomEntry("ephemlets", { op: "put", ephemlet: { ...existing, status: "building", jobId: "lost" }, }); await session.extensionRunner.emit({ type: "session_start", reason: "resume", }); expect( data(await call({ action: "get", id: first["id"] as string })), ).toMatchObject({ status: "failed", jobId: null }); }); it("enabled diagnostics use one owned writer and omit task content", async () => { const { call, fixture, session } = await setup(true); await call({ action: "put", title: "private-marker-do-not-log", purpose: "private-marker-do-not-log", source: "private-marker-do-not-log", }); await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); const directory = join( fixture.cwd, ".test-state", "pi-ext", "debug", "ephemlets", ); const files = await readdir(directory); expect(files).toHaveLength(1); const text = await readFile(join(directory, files[0]!), "utf8"); const records = text .trim() .split("\n") .map((line) => JSON.parse(line)); expect(records.map((record) => record.event)).toEqual([ "session.start", "ephemlet.put", "session.shutdown", ]); expect(text).not.toContain("private-marker-do-not-log"); }); it("has schema-bound diagnostics and defaults to no files", async () => { const { call, fixture } = await setup(); await call({ action: "put", title: "Report", purpose: "Report", source: "done", }); expect(fixture.errors).toEqual([]); const { readdir } = await import("node:fs/promises"); await expect( readdir(`${fixture.cwd}/.test-state/pi-ext/debug/ephemlets`), ).rejects.toMatchObject({ code: "ENOENT" }); }); });