clanker-agent/packages/coding-agent/test/extensions-input-event.test.ts
Harivansh Rathi 536241053c refactor: finish companion rename migration
Complete the remaining pi-to-companion rename across companion-os, web, vm-orchestrator, docker, and archived fixtures.

Verification:
- semantic rg sweeps for Pi/piConfig/getPi/.pi runtime references
- npm run check in apps/companion-os (fails in this worktree: biome not found)

Co-authored-by: Codex <noreply@openai.com>
2026-03-10 07:39:32 -05:00

148 lines
5.4 KiB
TypeScript

import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { discoverAndLoadExtensions } from "../src/core/extensions/loader.js";
import { ExtensionRunner } from "../src/core/extensions/runner.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
describe("Input Event", () => {
let tempDir: string;
let extensionsDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "companion-input-test-"));
extensionsDir = path.join(tempDir, "extensions");
fs.mkdirSync(extensionsDir);
// Clean globalThis test vars
delete (globalThis as any).testVar;
});
afterEach(() => fs.rmSync(tempDir, { recursive: true, force: true }));
async function createRunner(...extensions: string[]) {
// Clear and recreate extensions dir for clean state
fs.rmSync(extensionsDir, { recursive: true, force: true });
fs.mkdirSync(extensionsDir);
for (let i = 0; i < extensions.length; i++)
fs.writeFileSync(path.join(extensionsDir, `e${i}.ts`), extensions[i]);
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const sm = SessionManager.inMemory();
const mr = new ModelRegistry(
AuthStorage.create(path.join(tempDir, "auth.json")),
);
return new ExtensionRunner(
result.extensions,
result.runtime,
tempDir,
sm,
mr,
);
}
it("returns continue when no handlers, undefined return, or explicit continue", async () => {
// No handlers
expect(
(await (await createRunner()).emitInput("x", undefined, "interactive"))
.action,
).toBe("continue");
// Returns undefined
let r = await createRunner(
`export default p => p.on("input", async () => {});`,
);
expect((await r.emitInput("x", undefined, "interactive")).action).toBe(
"continue",
);
// Returns explicit continue
r = await createRunner(
`export default p => p.on("input", async () => ({ action: "continue" }));`,
);
expect((await r.emitInput("x", undefined, "interactive")).action).toBe(
"continue",
);
});
it("transforms text and preserves images when omitted", async () => {
const r = await createRunner(
`export default p => p.on("input", async e => ({ action: "transform", text: "T:" + e.text }));`,
);
const imgs = [
{ type: "image" as const, data: "orig", mimeType: "image/png" },
];
const result = await r.emitInput("hi", imgs, "interactive");
expect(result).toEqual({ action: "transform", text: "T:hi", images: imgs });
});
it("transforms and replaces images when provided", async () => {
const r = await createRunner(
`export default p => p.on("input", async () => ({ action: "transform", text: "X", images: [{ type: "image", data: "new", mimeType: "image/jpeg" }] }));`,
);
const result = await r.emitInput(
"hi",
[{ type: "image", data: "orig", mimeType: "image/png" }],
"interactive",
);
expect(result).toEqual({
action: "transform",
text: "X",
images: [{ type: "image", data: "new", mimeType: "image/jpeg" }],
});
});
it("chains transforms across multiple handlers", async () => {
const r = await createRunner(
`export default p => p.on("input", async e => ({ action: "transform", text: e.text + "[1]" }));`,
`export default p => p.on("input", async e => ({ action: "transform", text: e.text + "[2]" }));`,
);
const result = await r.emitInput("X", undefined, "interactive");
expect(result).toEqual({
action: "transform",
text: "X[1][2]",
images: undefined,
});
});
it("short-circuits on handled and skips subsequent handlers", async () => {
(globalThis as any).testVar = false;
const r = await createRunner(
`export default p => p.on("input", async () => ({ action: "handled" }));`,
`export default p => p.on("input", async () => { globalThis.testVar = true; });`,
);
expect(await r.emitInput("X", undefined, "interactive")).toEqual({
action: "handled",
});
expect((globalThis as any).testVar).toBe(false);
});
it("passes source correctly for all source types", async () => {
const r = await createRunner(
`export default p => p.on("input", async e => { globalThis.testVar = e.source; return { action: "continue" }; });`,
);
for (const source of ["interactive", "rpc", "extension"] as const) {
await r.emitInput("x", undefined, source);
expect((globalThis as any).testVar).toBe(source);
}
});
it("catches handler errors and continues", async () => {
const r = await createRunner(
`export default p => p.on("input", async () => { throw new Error("boom"); });`,
);
const errs: string[] = [];
r.onError((e) => errs.push(e.error));
const result = await r.emitInput("x", undefined, "interactive");
expect(result.action).toBe("continue");
expect(errs).toContain("boom");
});
it("hasHandlers returns correct value", async () => {
let r = await createRunner();
expect(r.hasHandlers("input")).toBe(false);
r = await createRunner(
`export default p => p.on("input", async () => {});`,
);
expect(r.hasHandlers("input")).toBe(true);
});
});