mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 10:05:14 +00:00
112 lines
5 KiB
TypeScript
112 lines
5 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(), "pi-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);
|
|
});
|
|
});
|