repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/session-rename/__tests__/contract.test.ts
Rawimport { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai";
import {
type ExtensionCommandContext,
initTheme,
} from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createPiSession,
type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
function extensionPath(): string {
return fileURLToPath(new URL("../index.ts", import.meta.url));
}
describe("session-rename contract", () => {
let session: PiSession | undefined;
afterEach(async () => {
await session?.dispose();
session = undefined;
vi.restoreAllMocks();
});
it("loads in real Pi extension runtime", async () => {
const extPath = extensionPath();
session = await createPiSession({ extensionPaths: [extPath] });
expect(session.extensions).toHaveLength(1);
expect(session.extensions[0]!.path).toBe(extPath);
});
it("writes its opt-in lifecycle trace inside the fixture sandbox", async () => {
const root = mkdtempSync(
path.join(tmpdir(), "pi-ext-session-rename-debug-"),
);
try {
session = await createPiSession({
cwd: root,
extensionPaths: [extensionPath()],
env: { PI_SESSION_RENAME_DEBUG: "1" },
});
const runner = session.session.extensionRunner;
await runner.emit({ type: "session_start", reason: "startup" });
await runner.emit({ type: "session_shutdown", reason: "quit" });
const stateHome = session.env["XDG_STATE_HOME"];
if (!stateHome) throw new Error("missing sandbox state home");
const directory = path.join(
stateHome,
"pi-ext",
"debug",
"session-rename",
);
const [file] = readdirSync(directory);
const events = readFileSync(path.join(directory, file!), "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line).event);
expect(events).toEqual(
expect.arrayContaining(["session.start", "session.shutdown"]),
);
} finally {
await session?.dispose();
session = undefined;
rmSync(root, { recursive: true, force: true });
}
});
it("routes naming through the active custom provider", async () => {
session = await createPiSession({ extensionPaths: [extensionPath()] });
const runner = session.session.extensionRunner;
const base = runner.createContext();
const faux = fauxProvider({
provider: "session-rename-test",
models: [{ id: "worker", maxTokens: 4_096 }],
});
faux.setResponses([fauxAssistantMessage("Klaus routing fix")]);
const stream = vi.spyOn(faux.provider, "stream");
base.modelRegistry.registerProvider(faux.provider);
const model = base.modelRegistry.find("session-rename-test", "worker");
if (!model) throw new Error("missing custom model");
const manager = base.sessionManager as any;
const sessionFile = path.join(session.cwd, "session.jsonl");
const sessionManager = Object.create(manager) as typeof manager;
sessionManager.getSessionFile = () => sessionFile;
const ctx = Object.create(base) as typeof base;
Object.defineProperties(ctx, {
model: { value: model },
sessionManager: { value: sessionManager },
});
manager.appendMessage({
role: "user",
content: "Fix Klaus routing",
timestamp: Date.now(),
});
manager.appendMessage(fauxAssistantMessage("Inspecting custom providers."));
const command = session.extensions[0]!.commands.get("session-rename");
if (!command) throw new Error("missing session-rename command");
await command.handler("", ctx as unknown as ExtensionCommandContext);
expect(stream).toHaveBeenCalledOnce();
expect(manager.getSessionName()).toBe("klaus routing fix");
});
it("reports naming failures through Pi UI", async () => {
session = await createPiSession({
extensionPaths: [extensionPath()],
env: { PI_SESSION_RENAME_DEBUG: "1" },
});
const runner = session.session.extensionRunner;
const base = runner.createContext();
const faux = fauxProvider({
provider: "session-rename-error",
models: [{ id: "worker", maxTokens: 4_096 }],
});
base.modelRegistry.registerProvider(faux.provider);
const model = base.modelRegistry.find("session-rename-error", "worker");
if (!model) throw new Error("missing custom model");
vi.spyOn(base.modelRegistry, "complete").mockRejectedValue(
new Error("provider exploded"),
);
const manager = base.sessionManager as any;
const sessionFile = path.join(session.cwd, "session.jsonl");
const sessionManager = Object.create(manager) as typeof manager;
sessionManager.getSessionFile = () => sessionFile;
const ctx = Object.create(base) as typeof base;
Object.defineProperties(ctx, {
hasUI: { value: true },
model: { value: model },
sessionManager: { value: sessionManager },
});
manager.appendMessage({
role: "user",
content: "Trigger rename failure",
timestamp: Date.now(),
});
manager.appendMessage(fauxAssistantMessage("Ready."));
initTheme(undefined, false);
const notify = vi.spyOn(ctx.ui, "notify");
const command = session.extensions[0]!.commands.get("session-rename");
if (!command) throw new Error("missing session-rename command");
await command.handler("", ctx as unknown as ExtensionCommandContext);
expect(notify).toHaveBeenCalledWith(
"session-rename failed: provider exploded",
"error",
);
await runner.emit({ type: "session_shutdown", reason: "quit" });
const stateHome = session.env["XDG_STATE_HOME"];
if (!stateHome) throw new Error("missing sandbox state home");
const directory = path.join(stateHome, "pi-ext", "debug", "session-rename");
const [file] = readdirSync(directory);
const records = readFileSync(path.join(directory, file!), "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line));
expect(records).toContainEqual(
expect.objectContaining({ event: "name.outcome", outcome: "failed" }),
);
});
});