Files
oh-my-pi-telegram/tests/pi.test.ts
evandrodevbr 057281a3bb feat: human error states, operator toasts and a queue/origin view in the omp UI
- classify transport and API failures into a short state with the next step
- count undelivered messages and show them next to the sent counter
- toast on owner paired, connection recovered and reply not delivered (throttled)
- second status line previews the next queued turns
- host spinner names the Telegram prompt while its turn runs
- sanitize every Telegram-controlled string that reaches the widget
2026-10-01 17:33:37 -03:00

195 lines
6.4 KiB
TypeScript

/**
* Regression tests for the pi SDK adapter boundary
* Covers narrow bridge-facing helpers over concrete pi context contracts
*/
import assert from "node:assert/strict";
import test from "node:test";
import {
canStartPollingInExtensionContext,
compactExtensionContext,
createExtensionApiRuntimePorts,
createScopedModelPatternPersister,
type ExtensionContext,
getExtensionContextCwd,
formatPollingStartBlockedByRunMode,
getExtensionContextMode,
getExtensionContextModel,
hasExtensionContextPendingMessages,
isExtensionContextIdle,
isExtensionContextPassiveRunMode,
notifyExtensionContext,
setExtensionWorkingMessage,
} from "../lib/pi.ts";
type PiRuntimeApiHarness = Parameters<
typeof createExtensionApiRuntimePorts
>[0] & {
events: string[];
};
type PiRuntimeModel = Parameters<PiRuntimeApiHarness["setModel"]>[0];
function createHarnessModel(id: string): PiRuntimeModel {
return { id } as PiRuntimeModel;
}
function getHarnessModelId(model: PiRuntimeModel): string {
return String(Reflect.get(Object(model), "id"));
}
test("Pi context mode helpers feature-detect passive run modes", () => {
assert.equal(getExtensionContextMode({ mode: "print" }), "print");
assert.equal(getExtensionContextMode({ mode: "bogus" }), undefined);
assert.equal(isExtensionContextPassiveRunMode({ mode: "print" }), true);
assert.equal(isExtensionContextPassiveRunMode({ mode: "json" }), true);
assert.equal(isExtensionContextPassiveRunMode({ mode: "rpc" }), false);
assert.equal(isExtensionContextPassiveRunMode({}), false);
assert.equal(canStartPollingInExtensionContext({ mode: "tui" }), true);
assert.equal(canStartPollingInExtensionContext({ mode: "rpc" }), true);
assert.equal(canStartPollingInExtensionContext({ mode: "json" }), false);
assert.equal(canStartPollingInExtensionContext({ mode: "print" }), false);
assert.equal(canStartPollingInExtensionContext({}), true);
assert.equal(
formatPollingStartBlockedByRunMode({ mode: "json" }),
"Telegram polling is unavailable in Pi json mode. Use /telegram-connect from a long-lived Pi session.",
);
});
test("Pi API runtime ports bind methods without losing receiver context", async () => {
const api: PiRuntimeApiHarness = {
events: [],
sendUserMessage(content, options) {
this.events.push(
`send:${String(content)}:${options?.deliverAs ?? "default"}`,
);
},
async exec(command, args) {
this.events.push(`exec:${command}:${args.join(",")}`);
return { stdout: "ok", stderr: "", code: 0, killed: false };
},
getCommands() {
this.events.push("commands");
return [];
},
getThinkingLevel() {
this.events.push("get-thinking");
return "high";
},
setThinkingLevel(level) {
this.events.push(`thinking:${String(level)}`);
},
async setModel(model) {
this.events.push(`model:${getHarnessModelId(model)}`);
return true;
},
};
const runtime = createExtensionApiRuntimePorts(api);
runtime.sendUserMessage("hello", { deliverAs: "followUp" });
assert.deepEqual(await runtime.exec("cmd", ["arg"]), {
stdout: "ok",
stderr: "",
code: 0,
killed: false,
});
assert.deepEqual(runtime.getCommands(), []);
assert.equal(runtime.getThinkingLevel(), "high");
runtime.setThinkingLevel("low");
assert.equal(await runtime.setModel(createHarnessModel("gpt-5")), true);
assert.deepEqual(api.events, [
"send:hello:followUp",
"exec:cmd:arg",
"commands",
"get-thinking",
"thinking:low",
"model:gpt-5",
]);
});
test("Pi scoped model persister invalidates cached inputs without clearing live menus", async () => {
const events: string[] = [];
const persist = createScopedModelPatternPersister({
createSettingsManager: (cwd) => ({
reload: async () => {},
flush: async () => {
events.push("flush");
},
getEnabledModels: () => undefined,
setEnabledModels: (patterns) => {
events.push(`set:${cwd}:${patterns?.join(",") ?? "all"}`);
},
}),
clearCachedModelMenuInputs: () => {
events.push("clear-cache");
},
});
await persist(["openai/gpt-5"], { cwd: "/tmp/project" } as ExtensionContext);
assert.deepEqual(events, [
"set:/tmp/project:openai/gpt-5",
"flush",
"clear-cache",
]);
});
test("Pi context helpers expose model, idle, pending-message, and compact adapters", () => {
const model = { provider: "openai", id: "gpt-5", name: "GPT-5" };
const events: string[] = [];
const ctx = {
model,
isIdle: () => true,
hasPendingMessages: () => false,
cwd: "/tmp/project",
compact: (callbacks: { onComplete: () => void }) => {
events.push("compact");
callbacks.onComplete();
},
} as unknown as ExtensionContext;
compactExtensionContext(ctx, {
onComplete: () => {
events.push("complete");
},
onError: () => {
events.push("error");
},
});
assert.equal(getExtensionContextModel(ctx), model);
assert.equal(getExtensionContextCwd(ctx), "/tmp/project");
assert.equal(isExtensionContextIdle(ctx), true);
assert.equal(hasExtensionContextPendingMessages(ctx), false);
assert.deepEqual(events, ["compact", "complete"]);
});
test("Pi context model prefers omp's live models facade over a frozen ctx.model", () => {
const frozen = { provider: "fake", id: "fake-1" };
const live = { provider: "fake", id: "fake-2" };
const ompCtx = {
model: frozen,
models: { current: () => live },
} as unknown as ExtensionContext;
const piCtx = { model: frozen } as unknown as ExtensionContext;
assert.equal(getExtensionContextModel(ompCtx), live);
assert.equal(getExtensionContextModel(piCtx), frozen);
});
test("Host UI helpers forward to the context and tolerate a host without a spinner API", () => {
const calls: string[] = [];
const full = {
ui: {
notify: (message: string, level: string) => {
calls.push(`notify:${level}:${message}`);
},
setWorkingMessage: (message?: string) => {
calls.push(`working:${message}`);
},
},
} as unknown as ExtensionContext;
notifyExtensionContext(full, "hello", "warning");
setExtensionWorkingMessage(full, "busy");
setExtensionWorkingMessage(full, undefined);
assert.deepEqual(calls, ["notify:warning:hello", "working:busy", "working:undefined"]);
const withoutSpinner = { ui: {} } as unknown as ExtensionContext;
assert.doesNotThrow(() => setExtensionWorkingMessage(withoutSpinner, "busy"));
});