Files
oh-my-pi-telegram/tests/model.test.ts
evandrodevbr 2aefc7ef5a
Validate / validate (push) Successful in 9m53s
fix: adapt the bridge to omp 18.4.8 host differences
- resolve the agent dir through the host getAgentDir() (registered by the composition root) instead of sniffing execPath/argv
- synthesize agent_settled from the first terminal agent_end on omp
- read the live model through ctx.models.current() and prefer it over the stored model on omp
- adapt omp Settings (reloadFromDisk, global enabledModels) at the SDK boundary and drop scoped-model persistence on omp
2026-10-01 10:07:47 -03:00

499 lines
15 KiB
TypeScript

/**
* Regression tests for Telegram model control helpers
* Exercises model selection state/resolution plus in-flight model-switch orchestration
*/
import assert from "node:assert/strict";
import test from "node:test";
import {
buildTelegramModelSwitchContinuationText,
buildTelegramModelSwitchContinuationTurn,
canRestartTelegramTurnForModelSwitch,
createCurrentModelRuntime,
createCurrentModelStore,
createCurrentModelUpdateRuntime,
createPendingModelSwitchStore,
createTelegramModelSwitchContinuationQueue,
createTelegramModelSwitchContinuationQueueRuntime,
createTelegramModelSwitchContinuationTurnBuilder,
createTelegramModelSwitchController,
createTelegramModelSwitchControllerRuntime,
getCanonicalModelId,
isThinkingLevel,
type MenuModel,
modelsMatch,
parseTelegramCliScopedModelPatterns,
parseTelegramScopedModelPatternList,
resolveScopedModelPatterns,
restartTelegramModelSwitchContinuation,
shouldTriggerPendingTelegramModelSwitchAbort,
sortScopedModels,
THINKING_LEVELS,
} from "../lib/model.ts";
import type { PendingTelegramTurn } from "../lib/queue.ts";
function createModelTestModel(
provider = "openai",
id = "gpt-5",
extra: Partial<MenuModel> = {},
): MenuModel {
return { provider, id, ...extra };
}
function createModelTestSelection(
model = createModelTestModel(),
thinkingLevel?: "high",
): { model: MenuModel; thinkingLevel?: "high" } {
return thinkingLevel === undefined ? { model } : { model, thinkingLevel };
}
function createModelTestTurn(
overrides: Partial<PendingTelegramTurn> = {},
): PendingTelegramTurn {
return {
kind: "prompt",
chatId: 7,
replyToMessageId: 8,
sourceMessageIds: [8],
queueOrder: 0,
queueLane: "default",
laneOrder: 0,
queuedAttachments: [],
content: [],
historyText: "active",
statusSummary: "active",
...overrides,
};
}
test("Current model store prefers explicit Telegram model over context fallback", () => {
const fallback = { provider: "fallback", id: "f", name: "Fallback" };
const selected = { provider: "selected", id: "s", name: "Selected" };
const store = createCurrentModelStore<
{ model?: typeof fallback },
typeof fallback
>((ctx) => ctx.model);
assert.equal(store.get({ model: fallback }), fallback);
assert.equal(store.getStored(), undefined);
store.set(selected);
assert.equal(store.get({ model: fallback }), selected);
assert.equal(store.getStored(), selected);
store.set(undefined);
assert.equal(store.get({ model: fallback }), fallback);
});
test("Current model store follows the live context model on hosts without model_select", () => {
const stale = { provider: "stale", id: "a", name: "Stale" };
const live = { provider: "live", id: "b", name: "Live" };
const store = createCurrentModelStore<{ model?: typeof live }, typeof live>(
(ctx) => ctx.model,
true,
);
store.set(stale);
assert.equal(store.get({ model: live }), live);
assert.equal(store.get({ model: undefined }), stale);
});
test("Current model runtime combines store fallback and status updates", () => {
const fallback = { provider: "fallback", id: "f", name: "Fallback" };
const selected = { provider: "selected", id: "s", name: "Selected" };
const statuses: string[] = [];
const runtime = createCurrentModelRuntime<
{ id: string; model?: typeof fallback },
typeof fallback
>({
getContextModel: (ctx) => ctx.model,
updateStatus: (ctx) => {
statuses.push(ctx.id);
},
});
assert.equal(runtime.get({ id: "fallback", model: fallback }), fallback);
runtime.setCurrentModel(selected, { id: "direct" });
assert.equal(runtime.get({ id: "fallback", model: fallback }), selected);
runtime.onModelSelect({ model: undefined }, { id: "event" });
assert.equal(runtime.get({ id: "fallback", model: fallback }), fallback);
assert.deepEqual(statuses, ["direct", "event"]);
});
test("Current model update runtime stores selections and refreshes status", () => {
const selected = { provider: "selected", id: "s", name: "Selected" };
let stored: typeof selected | undefined;
const statuses: string[] = [];
const runtime = createCurrentModelUpdateRuntime<
{ id: string },
typeof selected
>({
setCurrentModel: (model) => {
stored = model;
},
updateStatus: (ctx) => {
statuses.push(ctx.id);
},
});
runtime.setCurrentModel(selected, { id: "direct" });
assert.equal(stored, selected);
runtime.onModelSelect({ model: selected }, { id: "event" });
assert.deepEqual(statuses, ["direct", "event"]);
});
test("Model helpers match models, detect thinking levels, and expose constants", () => {
assert.deepEqual(THINKING_LEVELS, [
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
]);
assert.equal(
modelsMatch(createModelTestModel(), createModelTestModel()),
true,
);
assert.equal(
modelsMatch(
createModelTestModel(),
createModelTestModel("anthropic", "gpt-5"),
),
false,
);
assert.equal(getCanonicalModelId(createModelTestModel()), "openai/gpt-5");
assert.equal(isThinkingLevel("high"), true);
assert.equal(isThinkingLevel("impossible"), false);
});
test("Model helpers parse scoped model pattern lists and CLI flags", () => {
assert.deepEqual(parseTelegramScopedModelPatternList("a, b:high, "), [
"a",
"b:high",
]);
assert.deepEqual(
parseTelegramCliScopedModelPatterns(["--models", "a,b:high"]),
["a", "b:high"],
);
assert.deepEqual(parseTelegramCliScopedModelPatterns(["--models=a, b "]), [
"a",
"b",
]);
assert.equal(
parseTelegramCliScopedModelPatterns(["--models", " "]),
undefined,
);
assert.equal(
parseTelegramCliScopedModelPatterns(["--other", "a"]),
undefined,
);
});
test("Model helpers resolve scoped model patterns and sort current models first", () => {
const models: MenuModel[] = [
createModelTestModel("openai", "gpt-5", { name: "GPT 5" }),
createModelTestModel("openai", "gpt-5-latest", { name: "GPT 5 Latest" }),
createModelTestModel("anthropic", "claude-sonnet-20250101", {
name: "Claude Sonnet",
}),
];
const resolved = resolveScopedModelPatterns(
["gpt-5:high", "anthropic/*:low"],
models,
);
assert.deepEqual(
resolved.map((entry) => ({
id: entry.model.id,
thinking: entry.thinkingLevel,
})),
[
{ id: "gpt-5", thinking: "high" },
{ id: "claude-sonnet-20250101", thinking: "low" },
],
);
const sorted = sortScopedModels(resolved, models[0]);
assert.equal(sorted[0]?.model.id, "gpt-5");
});
test("Pending model-switch store owns selection state helpers", () => {
const store = createPendingModelSwitchStore<{ id: string }>();
assert.equal(store.has(), false);
assert.equal(store.get(), undefined);
store.set({ id: "next" });
assert.equal(store.has(), true);
assert.deepEqual(store.get(), { id: "next" });
store.clear();
assert.equal(store.has(), false);
});
test("In-flight model switch is allowed only for active Telegram turns with abort support", () => {
assert.equal(
canRestartTelegramTurnForModelSwitch({
isIdle: false,
hasActiveTelegramTurn: true,
hasAbortHandler: true,
}),
true,
);
assert.equal(
canRestartTelegramTurnForModelSwitch({
isIdle: true,
hasActiveTelegramTurn: true,
hasAbortHandler: true,
}),
false,
);
assert.equal(
canRestartTelegramTurnForModelSwitch({
isIdle: false,
hasActiveTelegramTurn: false,
hasAbortHandler: true,
}),
false,
);
assert.equal(
canRestartTelegramTurnForModelSwitch({
isIdle: false,
hasActiveTelegramTurn: true,
hasAbortHandler: false,
}),
false,
);
});
test("Pending model switch abort waits until no tool executions remain", () => {
assert.equal(
shouldTriggerPendingTelegramModelSwitchAbort({
hasPendingModelSwitch: true,
hasActiveTelegramTurn: true,
hasAbortHandler: true,
activeToolExecutions: 0,
}),
true,
);
assert.equal(
shouldTriggerPendingTelegramModelSwitchAbort({
hasPendingModelSwitch: true,
hasActiveTelegramTurn: true,
hasAbortHandler: true,
activeToolExecutions: 1,
}),
false,
);
assert.equal(
shouldTriggerPendingTelegramModelSwitchAbort({
hasPendingModelSwitch: false,
hasActiveTelegramTurn: true,
hasAbortHandler: true,
activeToolExecutions: 0,
}),
false,
);
});
test("Model-switch continuation restart queues before abort when state is present", () => {
const events: string[] = [];
assert.equal(
restartTelegramModelSwitchContinuation({
activeTurn: { id: 1 },
abort: () => {
events.push("abort");
},
selection: createModelTestSelection(),
queueContinuation: (turn, selection) => {
events.push(`queue:${turn.id}:${selection.model.id}`);
},
}),
true,
);
assert.deepEqual(events, ["queue:1:gpt-5", "abort"]);
assert.equal(
restartTelegramModelSwitchContinuation({
activeTurn: undefined,
abort: () => {},
selection: createModelTestSelection(),
queueContinuation: () => {
events.push("unexpected");
},
}),
false,
);
});
test("Model-switch controller centralizes pending abort and continuation queueing", () => {
const events: string[] = [];
let pendingSelection: { model: { provider: string; id: string } } | undefined;
const activeTurn = createModelTestTurn({
chatId: 1,
replyToMessageId: 2,
sourceMessageIds: [2],
queueOrder: 3,
laneOrder: 3,
historyText: "previous",
statusSummary: "previous",
});
const controller = createTelegramModelSwitchController({
isIdle: () => false,
getPendingModelSwitch: () => pendingSelection,
setPendingModelSwitch: (selection) => {
pendingSelection = selection;
},
getActiveTurn: () => activeTurn,
getAbortHandler: () => {
return () => {
events.push("abort");
};
},
hasAbortHandler: () => true,
getActiveToolExecutions: () => 0,
queueContinuation: (turn, selection) => {
events.push(`queue:${turn.replyToMessageId}:${selection.model.id}`);
},
updateStatus: () => {
events.push("status");
},
});
assert.equal(controller.canOfferInFlightSwitch({}), true);
controller.stagePendingSwitch(createModelTestSelection(), {});
assert.deepEqual(events, ["status"]);
assert.equal(controller.triggerPendingAbort({}), true);
assert.deepEqual(events, ["status", "queue:2:gpt-5", "abort"]);
assert.equal(pendingSelection, undefined);
});
test("Model-switch controller runtime binds continuation queue construction", () => {
const appended: PendingTelegramTurn[] = [];
let pending: { model: { provider: string; id: string } } | undefined =
createModelTestSelection();
let aborted = false;
const activeTurn = createModelTestTurn();
const controller = createTelegramModelSwitchControllerRuntime({
isIdle: () => false,
getPendingModelSwitch: () => pending,
setPendingModelSwitch: (selection) => {
pending = selection;
},
getActiveTurn: () => activeTurn,
getAbortHandler: () => {
return () => {
aborted = true;
};
},
hasAbortHandler: () => true,
getActiveToolExecutions: () => 0,
telegramPrefix: "[telegram]",
allocateItemOrder: () => 5,
allocateControlOrder: () => 6,
appendQueuedItem: (item) => {
appended.push(item);
},
updateStatus: () => {},
});
assert.equal(controller.triggerPendingAbort(undefined), true);
assert.equal(aborted, true);
assert.equal(pending, undefined);
assert.equal(appended[0]?.queueOrder, 5);
assert.equal(appended[0]?.queueLane, "control");
});
test("Model-switch continuation turn stays control-lane and resume-oriented", () => {
const createContinuationTurn =
createTelegramModelSwitchContinuationTurnBuilder({
allocateItemOrder: () => 5,
allocateControlOrder: () => 6,
});
const builtTurn = createContinuationTurn({
turn: { chatId: 1, replyToMessageId: 2 },
selection: createModelTestSelection(),
});
assert.equal(builtTurn.queueOrder, 5);
assert.equal(builtTurn.laneOrder, 6);
const sourceTarget = { chatId: 1, threadId: 42 };
const turn = buildTelegramModelSwitchContinuationTurn({
turn: { chatId: 1, replyToMessageId: 2, target: sourceTarget },
selection: createModelTestSelection(createModelTestModel(), "high"),
telegramPrefix: "[telegram]",
queueOrder: 3,
laneOrder: 4,
});
assert.equal(turn.kind, "prompt");
assert.equal(turn.queueLane, "control");
assert.equal(turn.queueOrder, 3);
assert.equal(turn.laneOrder, 4);
assert.equal(turn.chatId, 1);
assert.equal(turn.replyToMessageId, 2);
assert.deepEqual(turn.target, { chatId: 1, threadId: 42 });
assert.notEqual(turn.target, sourceTarget);
assert.deepEqual(turn.sourceMessageIds, []);
assert.equal(
turn.historyText,
"Continue interrupted Telegram request on openai/gpt-5",
);
assert.equal(turn.statusSummary, "↻ continue on gpt-5");
assert.match(String(turn.content[0]?.type), /text/);
assert.match(
String((turn.content[0] as { text?: string } | undefined)?.text ?? ""),
/Continue the interrupted previous Telegram request/,
);
});
test("Model-switch continuation queue runtime builds and appends control-lane turns", () => {
const appended: PendingTelegramTurn[] = [];
const queueContinuation = createTelegramModelSwitchContinuationQueueRuntime({
allocateItemOrder: () => 3,
allocateControlOrder: () => 4,
appendQueuedItem: (item) => {
appended.push(item);
},
});
queueContinuation(
createModelTestTurn(),
createModelTestSelection(),
undefined,
);
assert.equal(appended[0]?.queueOrder, 3);
assert.equal(appended[0]?.laneOrder, 4);
assert.equal(appended[0]?.queueLane, "control");
assert.match(
String((appended[0]?.content[0] as { text?: string } | undefined)?.text),
/^\[telegram\] Continue/,
);
});
test("Model-switch continuation queue creates and appends continuation turns", () => {
const appended: Array<{ item: PendingTelegramTurn; ctx: { id: string } }> =
[];
const queueContinuation = createTelegramModelSwitchContinuationQueue({
createContinuationTurn: ({ turn, selection }) => ({
...turn,
kind: "prompt" as const,
queueOrder: 1,
queueLane: "control" as const,
laneOrder: 2,
sourceMessageIds: [],
queuedAttachments: [],
content: [{ type: "text" as const, text: selection.model.id }],
historyText: "continue",
statusSummary: "continue",
}),
appendQueuedItem: (item, ctx: { id: string }) => {
appended.push({ item, ctx });
},
});
queueContinuation(createModelTestTurn(), createModelTestSelection(), {
id: "ctx",
});
assert.equal(appended[0]?.item.content[0]?.type, "text");
assert.equal(appended[0]?.item.content[0]?.text, "gpt-5");
assert.deepEqual(appended[0]?.ctx, { id: "ctx" });
});
test("Continuation prompt stays Telegram-scoped and resume-oriented", () => {
const text = buildTelegramModelSwitchContinuationText(
"[telegram]",
createModelTestModel(),
"high",
);
assert.match(text, /^\[telegram\]/);
assert.match(text, /Continue the interrupted previous Telegram request/);
assert.match(text, /openai\/gpt-5/);
assert.match(text, /thinking level \(high\)/);
});