Files
Evandro Fonseca Junior d1d04001d0
Release / release (push) Failing after 5s
Validate / validate (push) Failing after 5m21s
chore: fork @llblab/[email protected] as @evandrodevbr/oh-my-pi-telegram
Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a
drop-in extension for can1357/oh-my-pi (omp >= 16.5.2).

Changes vs upstream:
- Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram
- Bump version: 0.22.0 -> 0.22.0-evandro.1
- Update repository/homepage/bugs URLs to evandrodevbr
- Add omp field in package.json (displayName + compatibleWith)
- README: prepend 'Oh My Pi (omp) Compatibility' section with install
  commands, verified-components table, upstream lineage, and rationale
- CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased
- docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram
- tests/public-api.test.ts: rename package import paths

Gates re-run on the fork:
- npm run typecheck  -> tsc --noEmit (success)
- npm test           -> 1229 pass / 0 fail / 1 platform-only skip
- npm run audit      -> found 0 vulnerabilities
- npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz

Verified:
- omp install --link ./pi-telegram -> Linked OK
- omp plugin list -> @evandrodevbr/[email protected].0-evandro.1

Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
2026-07-15 14:18:16 -03:00

3982 lines
113 KiB
TypeScript

/**
* Regression tests for Telegram queue and runtime decision helpers
* Exercises queue ordering, mutation, dispatch planning, lifecycle plans, and model-switch guard behavior
*/
import assert from "node:assert/strict";
import test from "node:test";
import {
createTelegramButtonActionStore,
createTelegramOutboundReplyPlanner,
} from "../lib/outbound.ts";
import {
appendTelegramPromptTurnOnce,
appendTelegramQueueItem,
assertTelegramQueueItemAdmissionValid,
buildPendingTelegramControlItem,
buildTelegramAgentEndPlan,
buildTelegramAgentStartPlan,
buildTelegramSessionShutdownState,
buildTelegramSessionStartState,
canDispatchTelegramTurnState,
clearTelegramQueueItemsRuntime,
clearTelegramQueuePromptPriority,
clearTelegramQueuePromptPriorityRuntime,
compareTelegramQueueItems,
createTelegramActiveTurnStore,
createTelegramAgentEndHook,
createTelegramAgentLifecycleHooks,
createTelegramAgentStartHook,
createTelegramControlItemBuilder,
createTelegramControlQueueController,
createTelegramDeferredQueueDispatchRuntime,
createTelegramDispatchReadinessChecker,
createTelegramPromptEnqueueController,
createTelegramQueueDispatchController,
createTelegramQueueDispatchRuntime,
createTelegramQueueDispatchWatchdogRuntime,
createTelegramQueueMutationController,
createTelegramQueueStore,
createTelegramTransportStampedQueueStore,
createTelegramTransportStampRuntime,
createTelegramSessionLifecycleHooks,
createTelegramSessionLifecycleRuntime,
createTelegramSessionStateApplier,
createTelegramToolExecutionHooks,
enqueueTelegramPromptTurnRuntime,
executeTelegramControlItemRuntime,
executeTelegramQueueDispatchPlan,
formatQueuedTelegramItemsStatus,
getNextTelegramToolExecutionCount,
getTelegramQueueItemAdmissionMode,
getTelegramQueueLaneContract,
handleTelegramAgentEndRuntime,
handleTelegramAgentStartRuntime,
handleTelegramToolExecutionEndRuntime,
handleTelegramToolExecutionStartRuntime,
isTelegramQueueItemAdmissionValid,
partitionTelegramQueueItemsForHistory,
planNextTelegramQueueAction,
planTelegramPromptEnqueue,
prioritizeTelegramQueuePrompt,
prioritizeTelegramQueuePromptRuntime,
removeTelegramQueueItemsByMessageIds,
removeTelegramQueueItemsByMessageIdsRuntime,
shouldDispatchAfterTelegramAgentEnd,
shutdownTelegramSessionRuntime,
startTelegramSessionRuntime,
TELEGRAM_QUEUE_LANE_CONTRACTS,
type PendingTelegramControlItem,
type PendingTelegramTurn,
type TelegramDispatchRuntimeDeps,
type TelegramQueueItem,
} from "../lib/queue.ts";
function createQueueTestModel() {
return { provider: "openai", id: "gpt-5" };
}
function createQueueTestPromptTurn(
overrides: Partial<PendingTelegramTurn> = {},
): PendingTelegramTurn {
return {
kind: "prompt",
chatId: 1,
replyToMessageId: 2,
sourceMessageIds: [2],
queueOrder: 1,
queueLane: "default",
laneOrder: 1,
queuedAttachments: [],
content: [{ type: "text", text: "prompt" }],
historyText: "prompt",
statusSummary: "prompt",
...overrides,
};
}
function createQueueTestControlItem<TContext = unknown>(
overrides: Partial<PendingTelegramControlItem<TContext>> = {},
): PendingTelegramControlItem<TContext> {
return {
kind: "control",
controlType: "status",
chatId: 1,
replyToMessageId: 2,
queueOrder: 2,
queueLane: "control",
laneOrder: 0,
statusSummary: "control",
execute: async () => {},
...overrides,
};
}
test("Queue store owns queued item state helpers", () => {
const item: PendingTelegramTurn = createQueueTestPromptTurn({
queueOrder: 3,
laneOrder: 3,
statusSummary: "hello",
content: [{ type: "text", text: "hello" }],
historyText: "",
});
const store = createTelegramQueueStore([item]);
assert.deepEqual(store.getQueuedItems(), [item]);
assert.equal(store.hasQueuedItems(), true);
store.setQueuedItems([]);
assert.deepEqual(store.getQueuedItems(), []);
assert.equal(store.hasQueuedItems(), false);
});
test("Transport stamp runtime owns profile and token generations", () => {
let profile: string | undefined;
let botToken: string | undefined = "token-a";
const runtime = createTelegramTransportStampRuntime({
getProfileName: () => profile,
getBotToken: () => botToken,
});
const initial = runtime.getStamp();
assert.deepEqual(initial, { profile: "default", generation: "1" });
assert.equal(runtime.isActive(initial), true);
assert.equal(runtime.isActive(undefined), false);
profile = "work";
const afterProfileChange = runtime.getStamp();
assert.deepEqual(afterProfileChange, { profile: "work", generation: "2" });
assert.equal(runtime.isActive(initial), false);
botToken = "token-b";
const afterTokenChange = runtime.getStamp();
assert.deepEqual(afterTokenChange, { profile: "work", generation: "3" });
assert.equal(runtime.isActive(afterProfileChange), false);
assert.equal(runtime.isActive(afterTokenChange), true);
});
test("Transport-stamped queue store preserves admitted generations", () => {
const runtime = createTelegramTransportStampRuntime({
getProfileName: () => "work",
getBotToken: () => "token",
});
const rawStore = createTelegramQueueStore<string>();
const store = createTelegramTransportStampedQueueStore(
rawStore,
runtime.getStamp,
);
const unstamped = createQueueTestPromptTurn({ queueOrder: 1, laneOrder: 1 });
const preserved = createQueueTestPromptTurn({
queueOrder: 2,
laneOrder: 2,
transportStamp: { profile: "old", generation: "9" },
});
store.setQueuedItems([unstamped, preserved]);
assert.deepEqual(store.getQueuedItems()[0]?.transportStamp, {
profile: "work",
generation: "1",
});
assert.deepEqual(store.getQueuedItems()[1]?.transportStamp, {
profile: "old",
generation: "9",
});
});
test("Active turn store owns active turn state helpers", () => {
const store = createTelegramActiveTurnStore();
const turn: PendingTelegramTurn = createQueueTestPromptTurn({
chatId: 7,
target: { chatId: 7, threadId: 70 },
replyToMessageId: 8,
statusSummary: "hello",
sourceMessageIds: [8, 9],
content: [{ type: "text", text: "hello" }],
historyText: "",
});
assert.equal(store.has(), false);
assert.equal(store.get(), undefined);
store.set(turn);
turn.chatId = 99;
assert.equal(store.has(), true);
assert.equal(store.get()?.chatId, 7);
assert.equal(store.getChatId(), 7);
assert.deepEqual(store.getTarget(), { chatId: 7, threadId: 70 });
const target = store.getTarget();
if (target) target.threadId = 99;
assert.deepEqual(store.getTarget(), { chatId: 7, threadId: 70 });
assert.equal(store.getReplyToMessageId(), 8);
assert.deepEqual(store.getSourceMessageIds(), [8, 9]);
store.clear();
assert.equal(store.has(), false);
assert.equal(store.getChatId(), undefined);
assert.equal(store.getTarget(), undefined);
});
test("Control item builder creates control-lane queue items", () => {
const execute = async (): Promise<void> => {};
const createControlItem = createTelegramControlItemBuilder({
allocateItemOrder: () => 7,
allocateControlOrder: () => 8,
});
assert.deepEqual(
createControlItem({
chatId: 1,
replyToMessageId: 2,
controlType: "status",
statusSummary: "status",
execute,
}),
{
kind: "control",
controlType: "status",
chatId: 1,
replyToMessageId: 2,
queueOrder: 7,
queueLane: "control",
laneOrder: 8,
statusSummary: "status",
execute,
},
);
assert.deepEqual(
buildPendingTelegramControlItem({
chatId: 1,
replyToMessageId: 2,
controlType: "status",
queueOrder: 3,
laneOrder: 4,
statusSummary: "⚡ status",
execute,
}),
{
kind: "control",
chatId: 1,
replyToMessageId: 2,
controlType: "status",
queueOrder: 3,
queueLane: "control",
laneOrder: 4,
statusSummary: "⚡ status",
execute,
},
);
});
test("Queue lane contracts define admission modes and dispatch order", () => {
assert.deepEqual(
TELEGRAM_QUEUE_LANE_CONTRACTS.map((contract) => ({
lane: contract.lane,
admissionMode: contract.admissionMode,
dispatchRank: contract.dispatchRank,
allowedKinds: [...contract.allowedKinds],
})),
[
{
lane: "control",
admissionMode: "control-queue",
dispatchRank: 0,
allowedKinds: ["control", "prompt"],
},
{
lane: "priority",
admissionMode: "priority-queue",
dispatchRank: 1,
allowedKinds: ["prompt"],
},
{
lane: "default",
admissionMode: "default-queue",
dispatchRank: 2,
allowedKinds: ["prompt"],
},
],
);
assert.equal(getTelegramQueueLaneContract("priority").dispatchRank, 1);
assert.equal(
getTelegramQueueItemAdmissionMode({ queueLane: "control" }),
"control-queue",
);
assert.equal(
isTelegramQueueItemAdmissionValid({ kind: "prompt", queueLane: "control" }),
true,
);
assert.equal(
isTelegramQueueItemAdmissionValid({
kind: "control",
queueLane: "default",
}),
false,
);
assert.throws(
() =>
assertTelegramQueueItemAdmissionValid({
kind: "control",
queueLane: "default",
}),
{
message:
"Invalid Telegram queue admission: control item cannot use default lane",
},
);
});
test("Queue planning rejects invalid queue admission", () => {
assert.throws(
() =>
planNextTelegramQueueAction(
[
{
kind: "control",
controlType: "status",
chatId: 1,
replyToMessageId: 1,
queueOrder: 1,
queueLane: "default",
laneOrder: 1,
statusSummary: "invalid",
execute: async () => {},
},
],
true,
),
{
message:
"Invalid Telegram queue admission: control item cannot use default lane",
},
);
});
test("Queue prompt append-once deduplicates repeated callback prompts", () => {
const callbackTurn = createQueueTestPromptTurn({
queueLane: "priority",
laneOrder: 0,
content: [{ type: "text", text: "[callback] approve" }],
historyText: "approve",
});
const first = appendTelegramPromptTurnOnce([], callbackTurn);
assert.equal(first.appended, true);
const duplicate = appendTelegramPromptTurnOnce(first.items, {
...callbackTurn,
queueOrder: 99,
laneOrder: 99,
});
assert.equal(duplicate.appended, false);
assert.equal(duplicate.items.length, 1);
const distinct = appendTelegramPromptTurnOnce(first.items, {
...callbackTurn,
content: [{ type: "text", text: "[callback] reject" }],
historyText: "reject",
});
assert.equal(distinct.appended, true);
assert.equal(distinct.items.length, 2);
});
test("Control-lane items sort before priority and default prompt items", () => {
const defaultPrompt: TelegramQueueItem = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [1],
queueOrder: 10,
laneOrder: 10,
content: [{ type: "text", text: "default" }],
historyText: "default",
statusSummary: "default",
});
const priorityPrompt: TelegramQueueItem = createQueueTestPromptTurn({
queueOrder: 11,
queueLane: "priority",
laneOrder: 0,
content: [{ type: "text", text: "priority" }],
historyText: "priority",
statusSummary: "priority",
});
const controlItem: TelegramQueueItem = createQueueTestControlItem({
replyToMessageId: 3,
queueOrder: 12,
});
const items = [defaultPrompt, controlItem, priorityPrompt].sort(
compareTelegramQueueItems,
);
assert.deepEqual(
items.map((item) => item?.statusSummary),
["control", "priority", "default"],
);
});
test("Queue mutation helpers remove prompt items by Telegram message id", () => {
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [11, 12],
historyText: "prompt history",
});
const controlItem: TelegramQueueItem = createQueueTestControlItem();
const result = removeTelegramQueueItemsByMessageIds(
[promptItem, controlItem],
[12],
);
assert.equal(result.removedCount, 1);
assert.deepEqual(
result.items.map((item) => item.statusSummary),
["control"],
);
});
test("Queue mutation controller binds queue accessors to runtime mutations", () => {
const events: string[] = [];
const promptItem: PendingTelegramTurn = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [11],
queueOrder: 2,
laneOrder: 2,
historyText: "prompt history",
});
const controlItem = buildPendingTelegramControlItem<string>({
chatId: 1,
replyToMessageId: 3,
controlType: "status",
queueOrder: 1,
laneOrder: 0,
statusSummary: "control",
execute: async () => {},
});
let queuedItems: TelegramQueueItem<string>[] = [promptItem, controlItem];
let nextPriorityOrder = 7;
const controller = createTelegramQueueMutationController<string>({
getQueuedItems: () => queuedItems,
setQueuedItems: (items) => {
queuedItems = items;
},
getNextPriorityReactionOrder: () => nextPriorityOrder,
incrementNextPriorityReactionOrder: () => {
nextPriorityOrder += 1;
},
updateStatus: (ctx) => {
events.push(ctx);
},
});
controller.reorder("a");
assert.deepEqual(
queuedItems.map((item) => item.statusSummary),
["control", "prompt"],
);
controller.append(
{
...promptItem,
replyToMessageId: 12,
sourceMessageIds: [12],
queueOrder: 2,
laneOrder: 2,
statusSummary: "appended",
},
"append",
);
assert.deepEqual(
queuedItems.map((item) => item.statusSummary),
["control", "prompt", "appended"],
);
assert.equal(controller.prioritizeByMessageId(11, "b", "❤"), true);
assert.equal(nextPriorityOrder, 8);
const reprioritized = queuedItems.find((item) => item.replyToMessageId === 1);
assert.equal(
reprioritized?.kind === "prompt" ? reprioritized.priorityEmoji : undefined,
"❤",
);
assert.equal(controller.clearPriorityByMessageId(11, "c"), true);
assert.equal(controller.removeByMessageIds([11], "d"), 1);
assert.equal(controller.clear("e"), 2);
assert.deepEqual(queuedItems, []);
assert.deepEqual(events, ["a", "append", "b", "c", "d", "e"]);
});
test("Queue mutation runtime removes, sorts, and reprioritizes prompts", () => {
const events: string[] = [];
const promptItem: PendingTelegramTurn = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [11],
});
const priorityPrompt: PendingTelegramTurn = {
...promptItem,
replyToMessageId: 2,
sourceMessageIds: [22],
queueOrder: 2,
queueLane: "priority",
laneOrder: 0,
statusSummary: "priority",
};
const controlItem = buildPendingTelegramControlItem<string>({
chatId: 1,
replyToMessageId: 3,
controlType: "status",
queueOrder: 3,
laneOrder: 0,
statusSummary: "control",
execute: async () => {},
});
let queuedItems: TelegramQueueItem<string>[] = [
promptItem,
controlItem,
priorityPrompt,
];
let nextPriorityOrder = 5;
const deps = {
ctx: "ctx",
getQueuedItems: () => queuedItems,
setQueuedItems: (items: TelegramQueueItem<string>[]) => {
queuedItems = items;
events.push(`items:${items.map((item) => item.statusSummary).join(",")}`);
},
getNextPriorityReactionOrder: () => nextPriorityOrder,
incrementNextPriorityReactionOrder: () => {
nextPriorityOrder += 1;
events.push(`order:${nextPriorityOrder}`);
},
updateStatus: (ctx: string) => {
events.push(`status:${ctx}`);
},
};
assert.equal(clearTelegramQueuePromptPriorityRuntime<string>(22, deps), true);
assert.deepEqual(
queuedItems.map((item) => item.statusSummary),
["control", "prompt", "priority"],
);
assert.equal(prioritizeTelegramQueuePromptRuntime<string>(11, deps), true);
assert.equal(nextPriorityOrder, 6);
assert.deepEqual(
queuedItems.map((item) => item.statusSummary),
["control", "prompt", "priority"],
);
assert.equal(
removeTelegramQueueItemsByMessageIdsRuntime<string>([11], deps),
1,
);
assert.deepEqual(
queuedItems.map((item) => item.statusSummary),
["control", "priority"],
);
assert.equal(clearTelegramQueueItemsRuntime<string>(deps), 2);
assert.deepEqual(queuedItems, []);
assert.equal(clearTelegramQueueItemsRuntime<string>(deps), 0);
assert.deepEqual(events, [
"items:prompt,control,priority",
"items:control,prompt,priority",
"status:ctx",
"items:control,prompt,priority",
"order:6",
"items:control,prompt,priority",
"status:ctx",
"items:control,priority",
"status:ctx",
"items:",
"status:ctx",
]);
});
test("Queue mutation runtime swallows only stale context status errors", () => {
let queuedItems: TelegramQueueItem<string>[] = [createQueueTestPromptTurn()];
const baseDeps = {
ctx: "ctx",
getQueuedItems: () => queuedItems,
setQueuedItems: (items: TelegramQueueItem<string>[]) => {
queuedItems = items;
},
};
assert.doesNotThrow(() =>
clearTelegramQueueItemsRuntime<string>({
...baseDeps,
updateStatus: () => {
throw new Error("ctx is stale after session reload");
},
}),
);
queuedItems = [createQueueTestPromptTurn()];
assert.throws(
() =>
clearTelegramQueueItemsRuntime<string>({
...baseDeps,
updateStatus: () => {
throw new Error("status broke");
},
}),
/status broke/,
);
});
test("Queue mutation helpers scope message-id mutations by chat and thread", () => {
const privateTurn = createQueueTestPromptTurn({
chatId: 1,
sourceMessageIds: [10],
statusSummary: "private",
});
const threadTurn = createQueueTestPromptTurn({
chatId: 1,
target: { chatId: 1, threadId: 2 },
sourceMessageIds: [10],
statusSummary: "thread",
});
const otherChatTurn = createQueueTestPromptTurn({
chatId: 2,
sourceMessageIds: [10],
statusSummary: "other",
});
assert.deepEqual(
removeTelegramQueueItemsByMessageIds(
[privateTurn, threadTurn, otherChatTurn],
[10],
{ chatId: 1, threadId: 2 },
).items.map((item) => item.statusSummary),
["private", "other"],
);
const prioritized = prioritizeTelegramQueuePrompt(
[privateTurn, otherChatTurn],
10,
5,
"⚡",
{ chatId: 2 },
).items;
assert.equal(
prioritized[0]?.kind === "prompt" ? prioritized[0].queueLane : undefined,
"default",
);
assert.equal(
prioritized[1]?.kind === "prompt" ? prioritized[1].queueLane : undefined,
"priority",
);
const cleared = clearTelegramQueuePromptPriority(prioritized, 10, {
chatId: 2,
}).items;
assert.equal(
cleared[1]?.kind === "prompt" ? cleared[1].queueLane : undefined,
"default",
);
});
test("Queue mutation helpers apply and clear prompt priority without touching control items", () => {
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [11],
queueOrder: 4,
laneOrder: 4,
historyText: "prompt history",
});
const controlItem: TelegramQueueItem = createQueueTestControlItem({
queueOrder: 5,
});
const prioritized = prioritizeTelegramQueuePrompt(
[promptItem, controlItem],
11,
0,
"🕊",
);
assert.equal(prioritized.changed, true);
assert.equal(prioritized.items[0]?.queueLane, "priority");
assert.equal(
prioritized.items[0]?.kind === "prompt"
? prioritized.items[0].priorityEmoji
: undefined,
"🕊",
);
const cleared = clearTelegramQueuePromptPriority(prioritized.items, 11);
assert.equal(cleared.changed, true);
assert.equal(cleared.items[0]?.queueLane, "default");
assert.equal(
cleared.items[0]?.kind === "prompt"
? cleared.items[0].priorityEmoji
: "unexpected",
undefined,
);
assert.equal(cleared.items[1]?.queueLane, "control");
});
test("Queue priority reactions apply to attachment-only prompt turns", () => {
const attachmentPrompt: TelegramQueueItem = createQueueTestPromptTurn({
sourceMessageIds: [21],
queuedAttachments: [{ path: "/tmp/voice.ogg", fileName: "voice.ogg" }],
content: [{ type: "text", text: "[telegram] voice transcript" }],
historyText: "voice transcript",
statusSummary: "📎 voice.ogg",
});
const prioritized = prioritizeTelegramQueuePrompt([attachmentPrompt], 21, 0);
assert.equal(prioritized.changed, true);
assert.equal(prioritized.items[0]?.queueLane, "priority");
assert.equal(prioritized.items[0]?.statusSummary, "📎 voice.ogg");
});
test("Queued status formatting keeps the terminal status bar compact", () => {
const priorityPrompt: TelegramQueueItem = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [11],
queueOrder: 4,
queueLane: "priority",
laneOrder: 0,
priorityEmoji: "❤",
historyText: "prompt history",
});
const defaultPrompt: TelegramQueueItem = createQueueTestPromptTurn({
sourceMessageIds: [12],
queueOrder: 5,
laneOrder: 5,
content: [{ type: "text", text: "default" }],
historyText: "default history",
statusSummary: "default",
});
const controlItem: TelegramQueueItem = createQueueTestControlItem({
replyToMessageId: 3,
queueOrder: 6,
statusSummary: "⚡ status",
});
assert.equal(
formatQueuedTelegramItemsStatus([
controlItem,
priorityPrompt,
defaultPrompt,
]),
" +3",
);
});
test("Queue enqueue planning folds queued prompts into history when requested", () => {
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [11],
historyText: "prompt history",
});
const controlItem: TelegramQueueItem = createQueueTestControlItem({
controlType: "model",
});
assert.deepEqual(planTelegramPromptEnqueue([promptItem], false), {
historyTurns: [],
remainingItems: [promptItem],
});
const plan = planTelegramPromptEnqueue([promptItem, controlItem], true);
assert.deepEqual(plan.historyTurns, [promptItem]);
assert.deepEqual(plan.remainingItems, [controlItem]);
assert.deepEqual(appendTelegramQueueItem(plan.remainingItems, promptItem), [
controlItem,
promptItem,
]);
assert.throws(
() =>
appendTelegramQueueItem(plan.remainingItems, {
...controlItem,
queueLane: "default",
}),
{
message:
"Invalid Telegram queue admission: control item cannot use default lane",
},
);
});
test("History partition keeps control items queued and extracts prompt items", () => {
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [1],
historyText: "prompt history",
});
const controlItem: TelegramQueueItem = createQueueTestControlItem();
const result = partitionTelegramQueueItemsForHistory([
promptItem,
controlItem,
]);
assert.deepEqual(
result.historyTurns.map((item) => item.statusSummary),
["prompt"],
);
assert.deepEqual(
result.remainingItems.map((item) => item.statusSummary),
["control"],
);
});
test("Dispatch planning returns the prompt item when dispatch is allowed", () => {
const controlItem: TelegramQueueItem = createQueueTestControlItem({
replyToMessageId: 1,
queueOrder: 1,
});
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
queueOrder: 2,
laneOrder: 2,
historyText: "prompt history",
});
const result = planNextTelegramQueueAction([promptItem, controlItem], true);
assert.equal(result.kind, "prompt");
assert.equal(
result.kind === "prompt" ? result.item.statusSummary : "",
"prompt",
);
assert.deepEqual(
result.remainingItems.map((item) => item.statusSummary),
["prompt", "control"],
);
});
test("Dispatch planning runs control items before normal prompts", () => {
const controlItem: TelegramQueueItem = createQueueTestControlItem({
replyToMessageId: 1,
queueOrder: 1,
});
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
queueOrder: 2,
laneOrder: 2,
historyText: "prompt history",
});
const result = planNextTelegramQueueAction([controlItem, promptItem], true);
assert.equal(result.kind, "control");
assert.equal(
result.kind === "control" ? result.item.statusSummary : "",
"control",
);
assert.deepEqual(
result.remainingItems.map((item) => item.statusSummary),
["prompt"],
);
});
test("Dispatch planning returns none when dispatch is blocked", () => {
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
queueOrder: 2,
laneOrder: 2,
historyText: "prompt history",
});
const result = planNextTelegramQueueAction([promptItem], false);
assert.equal(result.kind, "none");
assert.deepEqual(
result.remainingItems.map((item) => item.statusSummary),
["prompt"],
);
});
test("Control-item dispatch sequencing hands off to the next prompt", () => {
const controlItem: TelegramQueueItem = createQueueTestControlItem({
replyToMessageId: 1,
queueOrder: 1,
});
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
queueOrder: 2,
laneOrder: 2,
historyText: "prompt history",
});
const firstStep = planNextTelegramQueueAction(
[controlItem, promptItem],
true,
);
assert.equal(firstStep.kind, "control");
const secondStep = planNextTelegramQueueAction(
firstStep.remainingItems,
true,
);
assert.equal(secondStep.kind, "prompt");
assert.equal(
secondStep.kind === "prompt" ? secondStep.item.statusSummary : "",
"prompt",
);
});
test("Abort-history leaves queued prompts waiting for explicit continuation", () => {
assert.equal(
shouldDispatchAfterTelegramAgentEnd({
hasTurn: true,
stopReason: "aborted",
foldQueuedPromptsIntoHistory: true,
}),
false,
);
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
queueOrder: 2,
laneOrder: 2,
historyText: "prompt history",
});
const blockedDispatch = planNextTelegramQueueAction(
[promptItem],
shouldDispatchAfterTelegramAgentEnd({
hasTurn: true,
stopReason: "aborted",
foldQueuedPromptsIntoHistory: true,
}),
);
assert.equal(blockedDispatch.kind, "none");
assert.deepEqual(
blockedDispatch.remainingItems.map((item) => item.statusSummary),
["prompt"],
);
});
test("Agent end dispatch policy resumes after success and error, but not abort-history", () => {
assert.equal(
shouldDispatchAfterTelegramAgentEnd({
hasTurn: false,
foldQueuedPromptsIntoHistory: false,
}),
true,
);
assert.equal(
shouldDispatchAfterTelegramAgentEnd({
hasTurn: true,
stopReason: "error",
foldQueuedPromptsIntoHistory: false,
}),
true,
);
assert.equal(
shouldDispatchAfterTelegramAgentEnd({
hasTurn: true,
stopReason: "aborted",
foldQueuedPromptsIntoHistory: false,
}),
true,
);
assert.equal(
shouldDispatchAfterTelegramAgentEnd({
hasTurn: true,
stopReason: "aborted",
foldQueuedPromptsIntoHistory: true,
}),
false,
);
});
test("Agent end runtime resets state, finalizes replies, sends attachments, and dispatches", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = createQueueTestPromptTurn({
queuedAttachments: [{ path: "/tmp/demo.txt", fileName: "demo.txt" }],
});
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "final" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
waitForTypingIdle: async () => {
events.push("typing-idle");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
clearPreview: async (chatId) => {
events.push(`clear:${chatId}`);
},
setPreviewPendingText: (text) => {
events.push(`preview:${text}`);
},
finalizeMarkdownPreview: async (_chatId, markdown) => {
events.push(`finalize:${markdown}`);
return false;
},
sendMarkdownReply: async (_chatId, _replyToMessageId, markdown) => {
events.push(`markdown:${markdown}`);
},
sendTextReply: async (_chatId, _replyToMessageId, text) => {
events.push(`text:${text}`);
},
sendQueuedAttachments: async (nextTurn) => {
events.push(`attachments:${nextTurn.queuedAttachments.length}`);
},
});
assert.deepEqual(events, [
"reset",
"typing-idle",
"status",
"preview:final",
"finalize:final",
"clear:1",
"markdown:final",
"attachments:1",
"dispatch",
]);
});
test("Agent end runtime delivers one Guest Mode attachment instead of a text article", async () => {
const events: unknown[] = [];
const turn: PendingTelegramTurn = createQueueTestPromptTurn({
chatId: 0,
replyToMessageId: 0,
guestQueryId: "guest-1",
queuedAttachments: [{ path: "/tmp/demo.txt", fileName: "demo.txt" }],
});
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "final caption" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => events.push("reset"),
updateStatus: () => events.push("status"),
dispatchNextQueuedTelegramTurn: () => events.push("dispatch"),
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => false,
sendMarkdownReply: async () => {},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {
events.push("unexpected:ordinary");
},
sendGuestReply: async () => {
events.push("unexpected:text");
},
sendGuestAttachment: async (nextTurn, attachment, caption) => {
events.push([
"guest-attachment",
nextTurn.guestQueryId,
attachment.path,
caption,
]);
},
});
assert.deepEqual(events, [
"reset",
"status",
["guest-attachment", "guest-1", "/tmp/demo.txt", "final caption"],
"dispatch",
]);
});
test("Agent end runtime routes Guest Mode voice markup through one media result", async () => {
const events: unknown[] = [];
const turn: PendingTelegramTurn = createQueueTestPromptTurn({
chatId: 0,
replyToMessageId: 0,
guestQueryId: "guest-1",
});
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "voice markup" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {},
updateStatus: () => {},
dispatchNextQueuedTelegramTurn: () => events.push("dispatch"),
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => false,
sendMarkdownReply: async () => {},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {
events.push("unexpected:attachment");
},
sendGuestReply: async () => {
events.push("unexpected:text");
},
planOutboundReply: () => ({
markdown: "visible caption",
voiceText: "spoken result",
}),
sendGuestVoiceReply: async (nextTurn, plan, caption) => {
events.push([
"guest-voice",
nextTurn.guestQueryId,
plan.voiceText,
caption,
]);
},
});
assert.deepEqual(events, [
["guest-voice", "guest-1", "spoken result", "visible caption"],
"dispatch",
]);
});
test("Agent end runtime records Guest Mode attachment failure without a second answer", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = createQueueTestPromptTurn({
chatId: 0,
replyToMessageId: 0,
guestQueryId: "guest-1",
queuedAttachments: [{ path: "/tmp/demo.txt", fileName: "demo.txt" }],
});
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "final caption" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {},
updateStatus: () => {},
dispatchNextQueuedTelegramTurn: () => events.push("dispatch"),
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => false,
sendMarkdownReply: async () => {},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
answerGuestQuery: async () => {
events.push("unexpected:fallback");
},
sendGuestReply: async () => {
events.push("unexpected:text");
},
sendGuestAttachment: async () => {
events.push("attachment");
throw new Error("ambiguous answer failure");
},
recordRuntimeEvent: (_category, _error, details) => {
events.push(`record:${details?.phase}`);
},
});
assert.deepEqual(events, [
"attachment",
"record:guest-attachment",
"dispatch",
]);
});
test("Agent end runtime can schedule active-turn final delivery without blocking", async () => {
const events: string[] = [];
let scheduledTask: (() => Promise<void>) | undefined;
const turn: PendingTelegramTurn = createQueueTestPromptTurn({
queuedAttachments: [{ path: "/tmp/demo.txt", fileName: "demo.txt" }],
});
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "final" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
scheduleActiveTurnDelivery: (task) => {
events.push("scheduled");
scheduledTask = task;
},
clearPreview: async (chatId) => {
events.push(`clear:${chatId}`);
},
setPreviewPendingText: (text) => {
events.push(`preview:${text}`);
},
finalizeMarkdownPreview: async (_chatId, markdown) => {
events.push(`finalize:${markdown}`);
return true;
},
sendMarkdownReply: async () => {
events.push("unexpected:markdown");
},
sendTextReply: async () => {
events.push("unexpected:text");
},
sendQueuedAttachments: async (nextTurn) => {
events.push(`attachments:${nextTurn.queuedAttachments.length}`);
},
});
assert.deepEqual(events, ["reset", "status", "scheduled"]);
assert.ok(scheduledTask);
await scheduledTask();
assert.deepEqual(events, [
"reset",
"status",
"scheduled",
"preview:final",
"finalize:final",
"attachments:1",
"dispatch",
]);
});
test("Agent end scheduled delivery stops after session generation loss", async () => {
const events: string[] = [];
let active = true;
let scheduledTask: (() => Promise<void>) | undefined;
await handleTelegramAgentEndRuntime({
turn: createQueueTestPromptTurn(),
assistant: { text: "stale final" },
foldQueuedPromptsIntoHistory: false,
isSessionActive: () => active,
resetRuntimeState: () => events.push("reset"),
updateStatus: () => events.push("status"),
dispatchNextQueuedTelegramTurn: () => events.push("dispatch"),
scheduleActiveTurnDelivery: (task) => {
scheduledTask = task;
},
clearPreview: async () => {
events.push("clear");
},
setPreviewPendingText: () => events.push("preview"),
finalizeMarkdownPreview: async () => {
events.push("finalize");
return true;
},
sendMarkdownReply: async () => events.push("send"),
sendTextReply: async () => events.push("text"),
sendQueuedAttachments: async () => {
events.push("attachments");
},
});
active = false;
await scheduledTask?.();
assert.deepEqual(events, ["reset", "status"]);
});
test("Agent end stops old-profile delivery after preview finalization yields", async () => {
const events: string[] = [];
let transportActive = true;
const turn = createQueueTestPromptTurn();
turn.transportStamp = { profile: "a", generation: "epoch-a" };
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "old profile final" },
foldQueuedPromptsIntoHistory: false,
isTurnTransportActive: () => transportActive,
resetRuntimeState: () => events.push("reset"),
updateStatus: () => events.push("status"),
dispatchNextQueuedTelegramTurn: () => events.push("dispatch"),
clearPreview: async () => {
events.push("clear");
},
setPreviewPendingText: () => events.push("preview"),
finalizeMarkdownPreview: async () => {
events.push("finalize");
transportActive = false;
return false;
},
sendMarkdownReply: async () => events.push("send"),
sendTextReply: async () => events.push("text"),
sendQueuedAttachments: async () => {
events.push("attachments");
},
});
assert.deepEqual(events, ["reset", "status", "preview", "finalize"]);
});
test("Agent end runtime keeps plain notices in the active turn target", async () => {
const replies: Array<{ text: string; target?: unknown }> = [];
const target = { chatId: 1, threadId: 42 };
const baseDeps = {
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {},
updateStatus: () => {},
dispatchNextQueuedTelegramTurn: () => {},
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => false,
sendMarkdownReply: async () => {},
sendTextReply: async (
_chatId: number,
_replyToMessageId: number,
text: string,
options?: { target?: unknown },
) => {
replies.push({ text, target: options?.target });
},
sendQueuedAttachments: async () => {},
};
await handleTelegramAgentEndRuntime({
...baseDeps,
turn: createQueueTestPromptTurn({ target }),
assistant: { stopReason: "error", errorMessage: "boom" },
});
await handleTelegramAgentEndRuntime({
...baseDeps,
turn: createQueueTestPromptTurn({
target,
queuedAttachments: [{ path: "/tmp/demo.txt", fileName: "demo.txt" }],
}),
assistant: {},
});
assert.deepEqual(replies, [
{ text: "boom", target },
{ text: "Attached requested file(s).", target },
]);
});
test("Agent end runtime records final delivery failures and dispatches", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = createQueueTestPromptTurn();
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "final" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
clearPreview: async () => {
events.push("clear");
},
setPreviewPendingText: (text) => {
events.push(`preview:${text}`);
},
finalizeMarkdownPreview: async () => {
throw new Error("fetch failed");
},
sendMarkdownReply: async () => {
events.push("unexpected:markdown");
},
sendTextReply: async () => {
events.push("unexpected:text");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
recordRuntimeEvent: (category, _error, details) => {
events.push(`${category}:${details?.phase}`);
},
});
assert.deepEqual(events, [
"reset",
"status",
"preview:final",
"delivery:final-text",
"attachments",
"dispatch",
]);
});
test("Agent end runtime sends proactive local result", async () => {
const events: string[] = [];
await handleTelegramAgentEndRuntime({
turn: undefined,
assistant: { text: "done" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => false,
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
events.push(
`markdown:${chatId}:${replyToMessageId}:${options?.target?.threadId ?? "none"}:${markdown}`,
);
},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
getDefaultChatId: () => 7,
getDefaultTarget: () => ({ chatId: 7, threadId: 42 }),
isProactivePushEnabled: () => true,
canSendProactivePush: () => true,
});
assert.deepEqual(events, [
"reset",
"status",
"markdown:7:undefined:42:done",
"dispatch",
]);
});
test("Agent end runtime skips proactive local result without ownership", async () => {
const events: string[] = [];
await handleTelegramAgentEndRuntime({
turn: undefined,
assistant: { text: "done" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => false,
sendMarkdownReply: async () => {
events.push("unexpected:markdown");
},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
getDefaultChatId: () => 7,
isProactivePushEnabled: () => true,
canSendProactivePush: () => false,
recordRuntimeEvent: (category, error, details) => {
const message = error instanceof Error ? error.message : String(error);
events.push(`${category}:${details?.phase}:${message}`);
},
});
assert.deepEqual(events, [
"reset",
"status",
"proactive-push:ownership:Proactive push skipped because this instance does not own Telegram polling.",
"dispatch",
]);
});
test("Agent end runtime keeps queued Telegram turn delivery independent from polling ownership", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = createQueueTestPromptTurn({
queuedAttachments: [{ path: "/tmp/demo.txt", fileName: "demo.txt" }],
});
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "final" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
clearPreview: async (chatId) => {
events.push(`clear:${chatId}`);
},
setPreviewPendingText: (text) => {
events.push(`preview:${text}`);
},
finalizeMarkdownPreview: async (chatId, markdown) => {
events.push(`finalize:${chatId}:${markdown}`);
return true;
},
sendMarkdownReply: async () => {
events.push("unexpected:markdown");
},
sendTextReply: async () => {
events.push("unexpected:text");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
sendOutboundReplyArtifacts: async () => {
events.push("unexpected:voice");
},
});
assert.deepEqual(events, [
"reset",
"status",
"preview:final",
"finalize:1:final",
"attachments",
"dispatch",
]);
});
test("Agent end runtime plans assistant button comments for active Telegram replies", async () => {
const events: unknown[] = [];
const turn: PendingTelegramTurn = createQueueTestPromptTurn({
target: { chatId: 1, threadId: 42 },
});
await handleTelegramAgentEndRuntime({
turn,
assistant: {
text: 'Choose one:\n\n<!-- telegram_button label="Continue" prompt="Continue with context." -->',
},
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
clearPreview: async (chatId) => {
events.push(`clear:${chatId}`);
},
setPreviewPendingText: (text) => {
events.push(`preview:${text}`);
},
finalizeMarkdownPreview: async (_chatId, markdown, _replyTo, options) => {
events.push({
finalize: markdown,
replyMarkup: options?.replyMarkup,
target: options?.target,
});
return true;
},
sendMarkdownReply: async () => {
events.push("unexpected:markdown");
},
sendTextReply: async () => {
events.push("unexpected:text");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
planOutboundReply: createTelegramOutboundReplyPlanner(
createTelegramButtonActionStore(),
),
});
assert.equal(events[0], "reset");
assert.equal(events[1], "status");
assert.equal(events[2], "preview:Choose one:");
assert.equal(events[4], "attachments");
assert.equal(events[5], "dispatch");
const finalDelivery = events[3] as {
finalize: string;
target?: { chatId: number; threadId?: number };
replyMarkup?: {
inline_keyboard?: Array<Array<{ text: string; callback_data: string }>>;
};
};
assert.equal(finalDelivery.finalize, "Choose one:");
assert.deepEqual(finalDelivery.target, { chatId: 1, threadId: 42 });
assert.equal(
finalDelivery.replyMarkup?.inline_keyboard?.[0]?.[0]?.text,
"Continue",
);
assert.match(
finalDelivery.replyMarkup?.inline_keyboard?.[0]?.[0]?.callback_data ?? "",
/^tgbtn:/,
);
});
test("Agent end runtime passes assistant button markup to final text delivery", async () => {
const events: unknown[] = [];
const replyMarkup = {
inline_keyboard: [[{ text: "Continue", callback_data: "btn:1" }]],
};
const turn: PendingTelegramTurn = createQueueTestPromptTurn();
await handleTelegramAgentEndRuntime({
turn,
assistant: {
text: 'Answer\n\n<!-- telegram_button label="Continue"\nContinue\n-->',
},
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
clearPreview: async (chatId) => {
events.push(`clear:${chatId}`);
},
setPreviewPendingText: (text) => {
events.push(`preview:${text}`);
},
finalizeMarkdownPreview: async (_chatId, markdown, _replyTo, options) => {
events.push({ finalize: markdown, replyMarkup: options?.replyMarkup });
return true;
},
sendMarkdownReply: async () => {
events.push("unexpected:markdown");
},
sendTextReply: async () => {
events.push("unexpected:text");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
planOutboundReply: () => ({ markdown: "Answer", replyMarkup }),
});
assert.deepEqual(events, [
"reset",
"status",
"preview:Answer",
{ finalize: "Answer", replyMarkup },
"attachments",
"dispatch",
]);
});
test("Agent end runtime splits assistant voice markup into text and voice delivery", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = createQueueTestPromptTurn();
await handleTelegramAgentEndRuntime({
turn,
assistant: {
text: [
"Full technical text.",
"",
"<!-- telegram_voice lang=ru rate=+20%",
"Short voice summary.",
"-->",
].join("\n"),
},
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
clearPreview: async (chatId) => {
events.push(`clear:${chatId}`);
},
setPreviewPendingText: (text) => {
events.push(`preview:${text}`);
},
finalizeMarkdownPreview: async (_chatId, markdown) => {
events.push(`finalize:${markdown}`);
return true;
},
sendMarkdownReply: async () => {
events.push("unexpected:markdown");
},
sendTextReply: async () => {
events.push("unexpected:text");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
planOutboundReply: (markdown) => ({
markdown: "Full technical text.",
voiceText: markdown.includes("telegram_voice")
? "Short voice summary."
: undefined,
lang: "ru",
rate: "+20%",
}),
sendOutboundReplyArtifacts: async (_turn, plan, options) => {
events.push(
`voice:${plan.voiceText}:${plan.lang}:${plan.rate}:${options?.replyToPrompt}`,
);
},
});
assert.deepEqual(events, [
"reset",
"status",
"preview:Full technical text.",
"finalize:Full technical text.",
"voice:Short voice summary.:ru:+20%:false",
"attachments",
"dispatch",
]);
});
test("Agent end transparently intercepts text reply for voice-preferred turn", async () => {
const events: unknown[] = [];
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyPreferred: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "Hello from voice reply" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
clearPreview: async () => {
events.push("clear");
},
setPreviewPendingText: () => {
events.push("preview");
},
finalizeMarkdownPreview: async () => {
events.push("finalize");
return true;
},
sendMarkdownReply: async () => {
events.push("text");
},
sendTextReply: async () => {
events.push("fallback");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
planOutboundReply: (markdown) => ({
markdown,
}),
sendOutboundReplyArtifacts: async (_turn, plan, options) => {
events.push(`voice:${plan.voiceText}:${options?.replyToPrompt}`);
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
});
assert.deepEqual(events, [
"reset",
"status",
"clear",
"voice:Hello from voice reply:true",
"attachments",
"dispatch",
]);
});
test("Agent end falls back to text when voice handler throws in always mode", async () => {
const events: unknown[] = [];
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyRequired: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "Fallback text reply" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
clearPreview: async () => {
events.push("clear");
},
setPreviewPendingText: () => {
events.push("preview");
},
finalizeMarkdownPreview: async () => {
events.push("finalize");
return true;
},
sendMarkdownReply: async (_chatId, _replyToMessageId, markdown) => {
events.push(`text:${markdown}`);
},
sendTextReply: async () => {
events.push("fallback");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
planOutboundReply: (markdown) => ({ markdown }),
sendOutboundReplyArtifacts: async () => {
throw new Error("TTS service unavailable");
},
recordRuntimeEvent: (category, error, _details) => {
events.push(`error:${category}:${(error as Error).message}`);
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
});
assert.deepEqual(events, [
"reset",
"status",
"clear",
"error:delivery:TTS service unavailable",
"text:Fallback text reply",
"attachments",
"dispatch",
]);
});
test("Agent end falls back to text when voice handler throws in voice-received mode", async () => {
const events: unknown[] = [];
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyPreferred: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "Fallback text reply" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
clearPreview: async () => {
events.push("clear");
},
setPreviewPendingText: () => {
events.push("preview");
},
finalizeMarkdownPreview: async () => {
events.push("finalize");
return true;
},
sendMarkdownReply: async (_chatId, _replyToMessageId, markdown) => {
events.push(`text:${markdown}`);
},
sendTextReply: async () => {
events.push("fallback");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
planOutboundReply: (markdown) => ({ markdown }),
sendOutboundReplyArtifacts: async () => {
throw new Error("TTS service unavailable");
},
recordRuntimeEvent: (category, error, _details) => {
events.push(`error:${category}:${(error as Error).message}`);
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
});
assert.deepEqual(events, [
"reset",
"status",
"clear",
"error:delivery:TTS service unavailable",
"text:Fallback text reply",
"attachments",
"dispatch",
]);
});
test("Agent end does not intercept when explicit voice markup exists", async () => {
const events: unknown[] = [];
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyRequired: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "Hello with explicit voice" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
clearPreview: async () => {
events.push("clear");
},
setPreviewPendingText: () => {
events.push("preview");
},
finalizeMarkdownPreview: async () => {
events.push("finalize");
return true;
},
sendMarkdownReply: async () => {
events.push("text");
},
sendTextReply: async () => {
events.push("fallback");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
planOutboundReply: () => ({
markdown: "",
voiceText: "Explicit voice text",
}),
sendOutboundReplyArtifacts: async (_turn, plan, options) => {
events.push(`voice:${plan.voiceText}:${options?.replyToPrompt}`);
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
});
assert.deepEqual(events, [
"reset",
"status",
"clear",
"voice:Explicit voice text:true",
"attachments",
"dispatch",
]);
});
test("Agent end does not intercept without planOutboundReply", async () => {
let voiceArtifactsCalled = false;
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyRequired: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "Hello with raw markup" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {},
updateStatus: () => {},
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => true,
sendMarkdownReply: async () => {},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
sendOutboundReplyArtifacts: async () => {
voiceArtifactsCalled = true;
},
recordRuntimeEvent: () => {},
dispatchNextQueuedTelegramTurn: () => {},
});
assert.equal(voiceArtifactsCalled, false);
});
test("Agent end sends text as reply and voice without reply when both exist", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyRequired: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "Hello world" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {},
updateStatus: () => {},
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async (_chatId, _text, replyToMessageId) => {
events.push(`finalize:${replyToMessageId}`);
return true;
},
planOutboundReply: (text) => ({
markdown: text,
voiceText: text,
}),
sendMarkdownReply: async () => {},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
sendOutboundReplyArtifacts: async (_turn, _reply, options) => {
events.push(`voice:replyToPrompt=${options?.replyToPrompt}`);
},
recordRuntimeEvent: () => {},
dispatchNextQueuedTelegramTurn: () => {},
});
assert.ok(events.some((e) => e.startsWith("finalize:")));
assert.ok(events.some((e) => e === "voice:replyToPrompt=false"));
});
test("Agent end does not intercept when planOutboundReply returns voiceReplies", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyPreferred: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "Hello world" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {},
updateStatus: () => {},
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => true,
sendMarkdownReply: async () => {},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
planOutboundReply: (text) => ({
markdown: text,
voiceReplies: [{ text, lang: "en" }],
}),
sendOutboundReplyArtifacts: async (_turn, reply, options) => {
events.push(
`voiceReplies:${reply.voiceReplies?.length}:replyToPrompt=${options?.replyToPrompt}`,
);
},
recordRuntimeEvent: () => {},
dispatchNextQueuedTelegramTurn: () => {},
});
assert.ok(events.some((e) => e.startsWith("voiceReplies:1")));
assert.ok(events.some((e) => e.includes("replyToPrompt=false")));
});
test("Agent end records event when voice fallback text delivery also fails", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyRequired: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "Fallback text reply" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {},
updateStatus: () => {},
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => true,
sendMarkdownReply: async () => {
throw new Error("Text delivery also failed");
},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
planOutboundReply: (markdown) => ({ markdown }),
sendOutboundReplyArtifacts: async () => {
throw new Error("TTS service unavailable");
},
recordRuntimeEvent: (category, error, details) => {
events.push(
`error:${category}:${(error as Error).message}:${details?.phase ?? "none"}`,
);
},
dispatchNextQueuedTelegramTurn: () => {},
});
assert.ok(events.some((e) => e.includes("voice-artifacts")));
assert.ok(events.some((e) => e.includes("voice-fallback-text")));
});
test("Agent end does not intercept when rawFinalText is whitespace only", async () => {
let voiceArtifactsCalled = false;
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyPreferred: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: " \n\t " },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {},
updateStatus: () => {},
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => true,
sendMarkdownReply: async () => {},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
sendOutboundReplyArtifacts: async () => {
voiceArtifactsCalled = true;
},
recordRuntimeEvent: () => {},
dispatchNextQueuedTelegramTurn: () => {},
});
assert.equal(voiceArtifactsCalled, false);
});
test("Agent end uses rawFinalText when plannedReply is undefined", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = {
...createQueueTestPromptTurn(),
voiceReplyPreferred: true,
};
await handleTelegramAgentEndRuntime({
turn,
assistant: { text: "Raw text fallback" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {},
updateStatus: () => {},
clearPreview: async () => {},
setPreviewPendingText: () => {},
finalizeMarkdownPreview: async () => true,
sendMarkdownReply: async () => {},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
planOutboundReply: () => undefined as any,
sendOutboundReplyArtifacts: async (_turn, reply, options) => {
events.push(
`voice:${reply.voiceText}:replyToPrompt=${options?.replyToPrompt}`,
);
},
recordRuntimeEvent: () => {},
dispatchNextQueuedTelegramTurn: () => {},
});
assert.ok(events.some((e) => e.includes("voice:Raw text fallback")));
assert.ok(events.some((e) => e.includes("replyToPrompt=true")));
});
test("Agent end hook binds assistant extraction and runtime ports", async () => {
const events: string[] = [];
const turn: PendingTelegramTurn = createQueueTestPromptTurn();
const hook = createTelegramAgentEndHook<
PendingTelegramTurn,
{ id: string },
string
>({
getActiveTurn: () => turn,
extractAssistant: (messages) => {
events.push(`extract:${messages.join(",")}`);
return { text: "final" };
},
getFoldQueuedPromptsIntoHistory: () => false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: (ctx) => {
events.push(`status:${ctx.id}`);
},
dispatchNextQueuedTelegramTurn: (ctx) => {
events.push(`dispatch:${ctx.id}`);
},
requestDeferredDispatchNextQueuedTelegramTurn: (dispatch) => {
setTimeout(() => dispatch({ id: "ctx" }), 0);
},
clearPreview: async (chatId) => {
events.push(`clear:${chatId}`);
},
setPreviewPendingText: (text) => {
events.push(`preview:${text}`);
},
finalizeMarkdownPreview: async (_chatId, markdown) => {
events.push(`finalize:${markdown}`);
return true;
},
sendMarkdownReply: async () => {
events.push("unexpected:markdown");
},
sendTextReply: async () => {
events.push("unexpected:text");
},
sendQueuedAttachments: async () => {
events.push("attachments");
},
});
await hook({ messages: ["a", "b"] }, { id: "ctx" });
assert.deepEqual(events, [
"extract:a,b",
"reset",
"status:ctx",
"preview:final",
"finalize:final",
"attachments",
]);
await new Promise((resolve) => setTimeout(resolve, 0));
assert.deepEqual(events, [
"extract:a,b",
"reset",
"status:ctx",
"preview:final",
"finalize:final",
"attachments",
"dispatch:ctx",
]);
});
test("Agent end runtime reports errors and dispatches next turn", async () => {
const events: string[] = [];
await handleTelegramAgentEndRuntime({
turn: {
kind: "prompt",
chatId: 1,
replyToMessageId: 2,
sourceMessageIds: [2],
queueOrder: 1,
queueLane: "default",
laneOrder: 1,
queuedAttachments: [],
content: [{ type: "text", text: "prompt" }],
historyText: "prompt",
statusSummary: "prompt",
},
assistant: { stopReason: "error", errorMessage: "boom" },
foldQueuedPromptsIntoHistory: false,
resetRuntimeState: () => {
events.push("reset");
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
clearPreview: async (chatId) => {
events.push(`clear:${chatId}`);
},
setPreviewPendingText: () => {
events.push("unexpected:preview");
},
finalizeMarkdownPreview: async () => true,
sendMarkdownReply: async () => {
events.push("unexpected:markdown");
},
sendTextReply: async (_chatId, _replyToMessageId, text) => {
events.push(`text:${text}`);
},
sendQueuedAttachments: async () => {
events.push("unexpected:attachments");
},
});
assert.deepEqual(events, [
"reset",
"status",
"clear:1",
"text:boom",
"dispatch",
]);
});
test("Agent end plan classifies turn outcomes correctly", () => {
const noTurnPlan = buildTelegramAgentEndPlan({
hasTurn: false,
foldQueuedPromptsIntoHistory: false,
hasFinalText: false,
hasQueuedAttachments: false,
});
assert.equal(noTurnPlan.kind, "no-turn");
assert.equal(noTurnPlan.shouldDispatchNext, true);
const abortedPlan = buildTelegramAgentEndPlan({
hasTurn: true,
stopReason: "aborted",
foldQueuedPromptsIntoHistory: true,
hasFinalText: false,
hasQueuedAttachments: true,
});
assert.equal(abortedPlan.kind, "aborted");
assert.equal(abortedPlan.shouldClearPreview, true);
assert.equal(abortedPlan.shouldDispatchNext, false);
const errorPlan = buildTelegramAgentEndPlan({
hasTurn: true,
stopReason: "error",
foldQueuedPromptsIntoHistory: false,
hasFinalText: false,
hasQueuedAttachments: false,
});
assert.equal(errorPlan.kind, "error");
assert.equal(errorPlan.shouldSendErrorMessage, true);
const attachmentPlan = buildTelegramAgentEndPlan({
hasTurn: true,
foldQueuedPromptsIntoHistory: false,
hasFinalText: false,
hasQueuedAttachments: true,
});
assert.equal(attachmentPlan.kind, "attachments-only");
assert.equal(attachmentPlan.shouldSendAttachmentNotice, true);
const textPlan = buildTelegramAgentEndPlan({
hasTurn: true,
foldQueuedPromptsIntoHistory: false,
hasFinalText: true,
hasQueuedAttachments: false,
});
assert.equal(textPlan.kind, "text");
assert.equal(textPlan.shouldClearPreview, false);
});
test("Agent start runtime consumes dispatched prompts and initializes active preview", () => {
const events: string[] = [];
const prompt: PendingTelegramTurn = createQueueTestPromptTurn();
let queuedItems: TelegramQueueItem[] = [prompt];
let activeTurn: PendingTelegramTurn | undefined;
let dispatchPending = true;
handleTelegramAgentStartRuntime({
queuedItems,
hasPendingDispatch: dispatchPending,
hasActiveTurn: false,
resetToolExecutions: () => {
events.push("tools");
},
resetPendingModelSwitch: () => {
events.push("switch");
},
setQueuedItems: (items) => {
queuedItems = items;
events.push(`items:${items.length}`);
},
clearDispatchPending: () => {
dispatchPending = false;
events.push("dispatch:false");
},
setFoldQueuedPromptsIntoHistory: (fold) => {
events.push(`fold:${fold}`);
},
setActiveTurn: (turn) => {
activeTurn = turn;
events.push(`turn:${turn.replyToMessageId}`);
},
createPreviewState: () => {
events.push("preview");
},
startTypingLoop: () => {
events.push("typing");
},
updateStatus: () => {
events.push("status");
},
});
assert.equal(dispatchPending, false);
assert.deepEqual(queuedItems, []);
assert.equal(activeTurn?.replyToMessageId, 2);
assert.deepEqual(events, [
"tools",
"switch",
"items:0",
"dispatch:false",
"turn:2",
"preview",
"typing",
"status",
]);
});
test("Agent lifecycle hooks bind start, end, and tool lifecycle ports", async () => {
const events: string[] = [];
let activeToolExecutions = 0;
const turn: PendingTelegramTurn = createQueueTestPromptTurn({
chatId: 7,
replyToMessageId: 8,
sourceMessageIds: [8],
content: [],
historyText: "turn",
statusSummary: "turn",
});
let activeTurn: PendingTelegramTurn | undefined;
const hooks = createTelegramAgentLifecycleHooks<
PendingTelegramTurn,
string,
{ role: string; content?: unknown[] }
>({
setAbortHandler: (ctx) => {
events.push(`abort:set:${ctx}`);
},
getQueuedItems: () => [turn],
hasPendingDispatch: () => true,
hasActiveTurn: () => !!activeTurn,
resetToolExecutions: () => {
activeToolExecutions = 0;
events.push("tools:reset");
},
resetPendingModelSwitch: () => {
events.push("switch:reset");
},
setQueuedItems: (items) => {
events.push(`queued:${items.length}`);
},
clearDispatchPending: () => {
events.push("dispatch:clear");
},
setFoldQueuedPromptsIntoHistory: (fold) => {
events.push(`fold:${fold}`);
},
setActiveTurn: (nextTurn) => {
activeTurn = nextTurn;
events.push(`active:${nextTurn.chatId}`);
},
createPreviewState: () => {
events.push("preview:create");
},
startTypingLoop: (ctx) => {
events.push(`typing:${ctx}`);
},
updateStatus: (ctx) => {
events.push(`status:${ctx}`);
},
getActiveTurn: () => activeTurn,
extractAssistant: () => ({ text: "done" }),
getFoldQueuedPromptsIntoHistory: () => false,
resetRuntimeState: () => {
activeTurn = undefined;
events.push("runtime:reset");
},
dispatchNextQueuedTelegramTurn: (ctx) => {
events.push(`dispatch:${ctx}`);
},
requestDeferredDispatchNextQueuedTelegramTurn: (dispatch) => {
setTimeout(() => dispatch("ctx"), 0);
},
clearPreview: async () => {
events.push("preview:clear");
},
setPreviewPendingText: (text) => {
events.push(`pending:${text}`);
},
finalizeMarkdownPreview: async () => false,
sendMarkdownReply: async (_chatId, _replyToMessageId, text) => {
events.push(`markdown:${text}`);
},
sendTextReply: async () => {},
sendQueuedAttachments: async () => {},
getActiveToolExecutions: () => activeToolExecutions,
setActiveToolExecutions: (count) => {
activeToolExecutions = count;
events.push(`tools:${count}`);
},
triggerPendingModelSwitchAbort: (ctx) => {
events.push(`switch:abort:${ctx}`);
},
});
await hooks.onAgentStart(undefined, "ctx");
hooks.onToolExecutionStart();
hooks.onToolExecutionEnd(undefined, "ctx");
await hooks.onAgentEnd({ messages: [] }, "ctx");
assert.deepEqual(events, [
"abort:set:ctx",
"tools:reset",
"switch:reset",
"queued:0",
"dispatch:clear",
"active:7",
"preview:create",
"typing:ctx",
"status:ctx",
"tools:1",
"tools:0",
"switch:abort:ctx",
"runtime:reset",
"status:ctx",
"pending:done",
"preview:clear",
"markdown:done",
]);
await new Promise((resolve) => setTimeout(resolve, 0));
assert.deepEqual(events, [
"abort:set:ctx",
"tools:reset",
"switch:reset",
"queued:0",
"dispatch:clear",
"active:7",
"preview:create",
"typing:ctx",
"status:ctx",
"tools:1",
"tools:0",
"switch:abort:ctx",
"runtime:reset",
"status:ctx",
"pending:done",
"preview:clear",
"markdown:done",
"dispatch:ctx",
]);
});
test("Agent start hook binds abort handler and runtime ports", async () => {
const events: string[] = [];
const prompt: PendingTelegramTurn = createQueueTestPromptTurn();
let queuedItems: TelegramQueueItem<{ abort: () => void }>[] = [prompt];
const hook = createTelegramAgentStartHook<
PendingTelegramTurn,
{ abort: () => void }
>({
setAbortHandler: (ctx) => {
ctx.abort();
events.push("abort-handler");
},
getQueuedItems: () => queuedItems,
hasPendingDispatch: () => true,
hasActiveTurn: () => false,
resetToolExecutions: () => {
events.push("tools");
},
resetPendingModelSwitch: () => {
events.push("switch");
},
setQueuedItems: (items) => {
queuedItems = items;
events.push(`items:${items.length}`);
},
clearDispatchPending: () => {
events.push("dispatch:false");
},
setFoldQueuedPromptsIntoHistory: (fold) => {
events.push(`fold:${fold}`);
},
setActiveTurn: (turn) => {
events.push(`turn:${turn.replyToMessageId}`);
},
createPreviewState: () => {
events.push("preview");
},
startTypingLoop: () => {
events.push("typing");
},
updateStatus: () => {
events.push("status");
},
});
await hook({}, { abort: () => events.push("abort") });
assert.deepEqual(events, [
"abort",
"abort-handler",
"tools",
"switch",
"items:0",
"dispatch:false",
"turn:2",
"preview",
"typing",
"status",
]);
});
test("Agent start plan consumes a dispatched prompt and resets transient flags", () => {
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
queueOrder: 2,
laneOrder: 2,
historyText: "prompt history",
});
const plan = buildTelegramAgentStartPlan({
queuedItems: [promptItem],
hasPendingDispatch: true,
hasActiveTurn: false,
});
assert.equal(plan.activeTurn?.statusSummary, "prompt");
assert.equal(plan.shouldClearDispatchPending, true);
assert.equal(plan.shouldClearAbortHistory, false);
assert.equal(plan.shouldResetPendingModelSwitch, true);
assert.equal(plan.shouldResetToolExecutions, true);
assert.deepEqual(plan.remainingItems, []);
});
test("Agent start plan clears stale abort-history mode for local prompts", () => {
const promptItem: TelegramQueueItem = createQueueTestPromptTurn({
queueOrder: 2,
laneOrder: 2,
historyText: "prompt history",
});
const plan = buildTelegramAgentStartPlan({
queuedItems: [promptItem],
hasPendingDispatch: false,
hasActiveTurn: false,
});
assert.equal(plan.activeTurn, undefined);
assert.equal(plan.shouldClearDispatchPending, false);
assert.equal(plan.shouldClearAbortHistory, true);
assert.deepEqual(plan.remainingItems, [promptItem]);
});
test("Tool execution runtimes update counts and trigger delayed aborts", () => {
const events: string[] = [];
let count = 0;
handleTelegramToolExecutionStartRuntime({
hasActiveTurn: () => true,
getActiveToolExecutions: () => count,
setActiveToolExecutions: (nextCount) => {
count = nextCount;
events.push(`count:${nextCount}`);
},
});
handleTelegramToolExecutionEndRuntime({
hasActiveTurn: () => true,
getActiveToolExecutions: () => count,
setActiveToolExecutions: (nextCount) => {
count = nextCount;
events.push(`count:${nextCount}`);
},
triggerPendingModelSwitchAbort: () => {
events.push("abort");
},
});
assert.equal(count, 0);
assert.deepEqual(events, ["count:1", "count:0", "abort"]);
});
test("Tool execution hooks bind counter and pending model-switch abort ports", () => {
let count = 0;
const events: string[] = [];
const hooks = createTelegramToolExecutionHooks<{ id: string }>({
hasActiveTurn: () => true,
getActiveToolExecutions: () => count,
setActiveToolExecutions: (nextCount) => {
count = nextCount;
events.push(`count:${nextCount}`);
},
triggerPendingModelSwitchAbort: (ctx) => {
events.push(`abort:${ctx.id}`);
},
});
hooks.onToolExecutionStart();
hooks.onToolExecutionEnd({}, { id: "ctx" });
assert.equal(count, 0);
assert.deepEqual(events, ["count:1", "count:0", "abort:ctx"]);
});
test("Tool execution count helper respects active-turn presence", () => {
assert.equal(
getNextTelegramToolExecutionCount({
hasActiveTurn: true,
currentCount: 0,
event: "start",
}),
1,
);
assert.equal(
getNextTelegramToolExecutionCount({
hasActiveTurn: true,
currentCount: 1,
event: "end",
}),
0,
);
assert.equal(
getNextTelegramToolExecutionCount({
hasActiveTurn: false,
currentCount: 3,
event: "end",
}),
3,
);
});
test("Dispatch readiness checker binds live guard ports", () => {
let compactionInProgress = false;
let activeTurn = false;
let dispatchPending = false;
const canDispatch = createTelegramDispatchReadinessChecker<{
idle: boolean;
pending: boolean;
}>({
isCompactionInProgress: () => compactionInProgress,
hasActiveTurn: () => activeTurn,
hasDispatchPending: () => dispatchPending,
isIdle: (ctx) => ctx.idle,
hasPendingMessages: (ctx) => ctx.pending,
});
assert.equal(canDispatch({ idle: true, pending: false }), true);
dispatchPending = true;
assert.equal(canDispatch({ idle: true, pending: false }), false);
dispatchPending = false;
compactionInProgress = true;
assert.equal(canDispatch({ idle: true, pending: false }), false);
compactionInProgress = false;
activeTurn = true;
assert.equal(canDispatch({ idle: true, pending: false }), false);
});
test("Dispatch is allowed only when every guard is clear", () => {
assert.equal(
canDispatchTelegramTurnState({
compactionInProgress: false,
hasActiveTelegramTurn: false,
hasPendingTelegramDispatch: false,
isIdle: true,
hasPendingMessages: false,
}),
true,
);
});
test("Dispatch is blocked during compaction", () => {
assert.equal(
canDispatchTelegramTurnState({
compactionInProgress: true,
hasActiveTelegramTurn: false,
hasPendingTelegramDispatch: false,
isIdle: true,
hasPendingMessages: false,
}),
false,
);
});
test("Dispatch is blocked while a Telegram turn is active or pending", () => {
assert.equal(
canDispatchTelegramTurnState({
compactionInProgress: false,
hasActiveTelegramTurn: true,
hasPendingTelegramDispatch: false,
isIdle: true,
hasPendingMessages: false,
}),
false,
);
assert.equal(
canDispatchTelegramTurnState({
compactionInProgress: false,
hasActiveTelegramTurn: false,
hasPendingTelegramDispatch: true,
isIdle: true,
hasPendingMessages: false,
}),
false,
);
});
test("Dispatch is blocked when pi is busy or has pending messages", () => {
assert.equal(
canDispatchTelegramTurnState({
compactionInProgress: false,
hasActiveTelegramTurn: false,
hasPendingTelegramDispatch: false,
isIdle: false,
hasPendingMessages: false,
}),
false,
);
assert.equal(
canDispatchTelegramTurnState({
compactionInProgress: false,
hasActiveTelegramTurn: false,
hasPendingTelegramDispatch: false,
isIdle: true,
hasPendingMessages: true,
}),
false,
);
});
test("Session state applier syncs start and shutdown state through live stores", () => {
const events: string[] = [];
const applier = createTelegramSessionStateApplier<string, { id: string }>({
setQueuedItems: (items) => {
events.push(`items:${items.join(",")}`);
},
setCurrentModel: (model) => {
events.push(`model:${model?.id ?? "none"}`);
},
setPendingModelSwitch: (selection) => {
events.push(`pending:${selection ?? "none"}`);
},
syncCounters: (state) => {
events.push(`counters:${state.nextQueuedTelegramItemOrder ?? "none"}`);
},
syncFlags: (state) => {
events.push(`flags:${state.telegramTurnDispatchPending}`);
},
});
applier.applyStartState({
currentTelegramModel: { id: "model" },
activeTelegramToolExecutions: 0,
pendingTelegramModelSwitch: undefined,
nextQueuedTelegramItemOrder: 3,
nextQueuedTelegramControlOrder: 4,
telegramTurnDispatchPending: false,
compactionInProgress: false,
});
applier.applyShutdownState({
queuedTelegramItems: ["a", "b"],
nextQueuedTelegramItemOrder: 5,
nextQueuedTelegramControlOrder: 6,
nextPriorityReactionOrder: 7,
currentTelegramModel: undefined,
activeTelegramToolExecutions: 0,
pendingTelegramModelSwitch: undefined,
telegramTurnDispatchPending: true,
compactionInProgress: false,
foldQueuedPromptsIntoHistory: false,
});
assert.deepEqual(events, [
"model:model",
"pending:none",
"counters:3",
"flags:false",
"items:a,b",
"counters:5",
"flags:true",
"model:none",
"pending:none",
]);
});
test("Session runtime helper runs shutdown side effects in order", async () => {
const events: string[] = [];
await shutdownTelegramSessionRuntime<string>({
unbindDeferredDispatchContext: () => {
events.push("unbind");
},
applyState: (state) => {
events.push(`state:${state.queuedTelegramItems.length}`);
},
clearPendingMediaGroups: () => {
events.push("media");
},
clearModelMenuState: () => {
events.push("menus");
},
getActiveTurnChatId: () => 42,
clearPreview: async (chatId) => {
events.push(`preview:${chatId}`);
},
clearActiveTurn: () => {
events.push("turn");
},
clearAbort: () => {
events.push("abort");
},
stopPolling: async () => {
events.push("polling");
},
});
assert.deepEqual(events, [
"unbind",
"polling",
"state:0",
"media",
"menus",
"preview:42",
"turn",
"abort",
]);
});
test("Session shutdown does not clear replacement state after polling await", async () => {
const events: string[] = [];
let active = true;
await shutdownTelegramSessionRuntime<string>({
isSessionActive: () => active,
unbindDeferredDispatchContext: () => events.push("unbind"),
applyState: () => events.push("state"),
clearPendingMediaGroups: () => events.push("media"),
clearModelMenuState: () => events.push("menus"),
getActiveTurnChatId: () => 42,
clearPreview: async () => {
events.push("preview");
},
clearActiveTurn: () => events.push("turn"),
clearAbort: () => events.push("abort"),
stopPolling: async () => {
events.push("polling");
active = false;
},
});
assert.deepEqual(events, ["unbind", "polling"]);
});
test("Session shutdown bounds a stuck preview clear", async () => {
const events: string[] = [];
await shutdownTelegramSessionRuntime<string>({
applyState: () => events.push("state"),
clearPendingMediaGroups: () => events.push("media"),
clearModelMenuState: () => events.push("menus"),
getActiveTurnChatId: () => 42,
clearPreview: () => new Promise<void>(() => {}),
previewShutdownTimeoutMs: 5,
clearActiveTurn: () => events.push("turn"),
clearAbort: () => events.push("abort"),
stopPolling: async () => {
events.push("polling");
},
});
assert.deepEqual(events, [
"polling",
"state",
"media",
"menus",
"turn",
"abort",
]);
});
test("Control queue controller appends and dispatches control items", () => {
const events: string[] = [];
const execute = async (): Promise<void> => {};
const item = buildPendingTelegramControlItem({
chatId: 1,
replyToMessageId: 2,
queueOrder: 3,
laneOrder: 4,
controlType: "status",
statusSummary: "status",
execute,
});
const controller = createTelegramControlQueueController<string>({
appendControlItem: (nextItem, ctx) => {
events.push(`append:${nextItem.controlType}:${ctx}`);
},
dispatchNextQueuedTelegramTurn: (ctx) => {
events.push(`dispatch:${ctx}`);
},
});
controller.enqueue(item, "ctx");
assert.deepEqual(events, ["append:status:ctx", "dispatch:ctx"]);
});
test("Prompt enqueue controller binds runtime ports to context", async () => {
const events: string[] = [];
let items: TelegramQueueItem<string>[] = [];
const controller = createTelegramPromptEnqueueController<number, string>({
getQueuedItems: () => items,
setQueuedItems: (nextItems) => {
items = nextItems;
events.push(`items:${nextItems.length}`);
},
getFoldQueuedPromptsIntoHistory: () => false,
setFoldQueuedPromptsIntoHistory: (fold) => {
events.push(`fold:${fold}`);
},
createTurn: async ([message]) => ({
kind: "prompt",
chatId: 1,
replyToMessageId: 2,
queueOrder: message ?? 0,
queueLane: "default",
laneOrder: message ?? 0,
statusSummary: `message ${message}`,
sourceMessageIds: [message ?? 0],
queuedAttachments: [],
content: [{ type: "text", text: String(message) }],
historyText: "",
}),
updateStatus: (ctx) => {
events.push(`status:${ctx}`);
},
dispatchNextQueuedTelegramTurn: (ctx) => {
events.push(`dispatch:${ctx}`);
},
});
await controller.enqueue([7], "ctx");
assert.deepEqual(events, [
"fold:false",
"items:1",
"status:ctx",
"dispatch:ctx",
]);
});
test("Prompt enqueue runtime folds queued prompts into history", async () => {
const events: string[] = [];
const historyPrompt: PendingTelegramTurn = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [1],
queueLane: "default" as const,
content: [{ type: "text" as const, text: "history" }],
historyText: "history",
statusSummary: "history",
});
const controlItem = buildPendingTelegramControlItem({
chatId: 1,
replyToMessageId: 2,
controlType: "status",
queueOrder: 2,
laneOrder: 0,
statusSummary: "control",
execute: async () => {},
});
const newPrompt = {
...historyPrompt,
replyToMessageId: 3,
sourceMessageIds: [3],
queueOrder: 3,
laneOrder: 3,
historyText: "new",
statusSummary: "new",
};
let queuedItems: TelegramQueueItem[] = [historyPrompt, controlItem];
let foldHistory = true;
await enqueueTelegramPromptTurnRuntime(["message"], {
getQueuedItems: () => queuedItems,
setQueuedItems: (items) => {
queuedItems = items;
events.push(`items:${items.map((item) => item.statusSummary).join(",")}`);
},
getFoldQueuedPromptsIntoHistory: () => foldHistory,
setFoldQueuedPromptsIntoHistory: (fold) => {
foldHistory = fold;
events.push(`fold:${fold}`);
},
createTurn: async (_messages, historyTurns) => {
events.push(
`history:${historyTurns.map((turn) => turn.historyText).join(",")}`,
);
return newPrompt;
},
updateStatus: () => {
events.push("status");
},
dispatchNextQueuedTelegramTurn: () => {
events.push("dispatch");
},
});
assert.equal(foldHistory, false);
assert.deepEqual(
queuedItems.map((item) => item.statusSummary),
["control", "new"],
);
assert.deepEqual(events, [
"fold:false",
"history:history",
"items:control,new",
"status",
"dispatch",
]);
});
test("Local agent start prevents stale abort-history mode from absorbing old queue", async () => {
const events: string[] = [];
const oldPrompt: PendingTelegramTurn = createQueueTestPromptTurn({
replyToMessageId: 1,
sourceMessageIds: [1],
queueOrder: 1,
laneOrder: 1,
content: [{ type: "text", text: "old" }],
historyText: "old",
statusSummary: "old",
});
const newPrompt: PendingTelegramTurn = createQueueTestPromptTurn({
replyToMessageId: 2,
sourceMessageIds: [2],
queueOrder: 2,
laneOrder: 2,
content: [{ type: "text", text: "new" }],
historyText: "new",
statusSummary: "new",
});
let queuedItems: TelegramQueueItem[] = [oldPrompt];
let foldHistory = true;
handleTelegramAgentStartRuntime({
queuedItems,
hasPendingDispatch: false,
hasActiveTurn: false,
resetToolExecutions: () => {},
resetPendingModelSwitch: () => {},
setQueuedItems: (items) => {
queuedItems = items;
},
clearDispatchPending: () => {},
setFoldQueuedPromptsIntoHistory: (fold) => {
foldHistory = fold;
events.push(`fold:${fold}`);
},
setActiveTurn: () => {},
createPreviewState: () => {},
startTypingLoop: () => {},
updateStatus: () => {},
});
await enqueueTelegramPromptTurnRuntime(["message"], {
getQueuedItems: () => queuedItems,
setQueuedItems: (items) => {
queuedItems = items;
events.push(`items:${items.map((item) => item.statusSummary).join(",")}`);
},
getFoldQueuedPromptsIntoHistory: () => foldHistory,
setFoldQueuedPromptsIntoHistory: (fold) => {
foldHistory = fold;
events.push(`fold:${fold}`);
},
createTurn: async (_messages, historyTurns) => {
events.push(`history:${historyTurns.length}`);
return newPrompt;
},
updateStatus: () => {},
dispatchNextQueuedTelegramTurn: () => {},
});
assert.equal(foldHistory, false);
assert.deepEqual(
queuedItems.map((item) => item.statusSummary),
["old", "new"],
);
assert.deepEqual(events, [
"fold:false",
"fold:false",
"history:0",
"items:old,new",
]);
});
test("Control runtime runs the control item and always settles", async () => {
const events: string[] = [];
await executeTelegramControlItemRuntime(
{
kind: "control",
controlType: "status",
chatId: 1,
replyToMessageId: 2,
queueOrder: 1,
queueLane: "control",
laneOrder: 0,
statusSummary: "status",
execute: async () => {
events.push("execute");
},
},
{
ctx: {},
sendTextReply: async () => {
events.push("reply");
return undefined;
},
onSettled: () => {
events.push("settled");
},
},
);
assert.deepEqual(events, ["execute", "settled"]);
});
test("Control runtime reports failures before settling", async () => {
const events: string[] = [];
await executeTelegramControlItemRuntime(
{
kind: "control",
controlType: "model",
chatId: 3,
replyToMessageId: 4,
queueOrder: 2,
queueLane: "control",
laneOrder: 1,
statusSummary: "model",
execute: async () => {
throw new Error("boom");
},
},
{
ctx: {},
sendTextReply: async (_chatId, _replyToMessageId, text) => {
events.push(text);
return undefined;
},
recordRuntimeEvent: (category, error, details) => {
const message = error instanceof Error ? error.message : String(error);
events.push(`${category}:${message}:${details?.controlType}`);
},
onSettled: () => {
events.push("settled");
},
},
);
assert.deepEqual(events, [
"control:boom:model",
"Telegram control action failed: boom",
"settled",
]);
});
test("Deferred queue dispatch uses only the bound session context", () => {
const events: string[] = [];
const callbacks: Array<() => void> = [];
const clearedTimers: number[] = [];
const runtime = createTelegramDeferredQueueDispatchRuntime<{ id: string }>({
setTimer: (callback) => {
callbacks.push(callback);
return callbacks.length as unknown as ReturnType<typeof setTimeout>;
},
clearTimer: (timer) => {
clearedTimers.push(timer as unknown as number);
},
});
runtime.request((ctx) => {
events.push(`dispatch:${ctx.id}`);
});
assert.equal(callbacks.length, 0);
runtime.bind({ id: "old" });
runtime.request((ctx) => {
events.push(`dispatch:${ctx.id}`);
});
runtime.unbind();
callbacks[0]?.();
runtime.bind({ id: "new" });
runtime.request((ctx) => {
events.push(`dispatch:${ctx.id}`);
});
callbacks[1]?.();
assert.deepEqual(clearedTimers, [1]);
assert.deepEqual(events, ["dispatch:new"]);
});
test("Queue dispatch watchdog retries dispatch for queued work", () => {
const events: string[] = [];
let hasQueuedItems = false;
let intervalCallback: (() => void) | undefined;
const watchdog = createTelegramQueueDispatchWatchdogRuntime<{ id: string }>({
hasQueuedItems: () => hasQueuedItems,
dispatchNextQueuedTelegramTurn: (ctx) => {
events.push(`dispatch:${ctx.id}`);
},
setInterval: (callback) => {
intervalCallback = callback;
return 1 as unknown as ReturnType<typeof setInterval>;
},
clearInterval: () => {
events.push("clear");
},
});
watchdog.start({ id: "ctx" });
assert.deepEqual(events, []);
hasQueuedItems = true;
watchdog.poke();
intervalCallback?.();
assert.deepEqual(events, ["dispatch:ctx", "dispatch:ctx"]);
watchdog.stop();
assert.deepEqual(events, ["dispatch:ctx", "dispatch:ctx", "clear"]);
});
test("Dispatch controller skips inactive stale contexts before readiness checks", () => {
const events: string[] = [];
const controller = createTelegramQueueDispatchController<string>({
getQueuedItems: () => {
events.push("unexpected:items");
return [];
},
setQueuedItems: () => {
events.push("unexpected:set");
},
canDispatch: () => {
events.push("unexpected:can-dispatch");
return true;
},
hasDispatchContext: () => false,
updateStatus: () => {
events.push("unexpected:status");
},
sendTextReply: async () => undefined,
onPromptDispatchStart: () => {},
sendUserMessage: () => {},
onPromptDispatchFailure: () => {},
});
controller.dispatchNext("stale");
assert.deepEqual(events, []);
});
test("Dispatch controller drops stale transport work before sending the next prompt", () => {
const stale = createQueueTestPromptTurn({ chatId: 1, replyToMessageId: 11 });
stale.transportStamp = { profile: "a", generation: "epoch-a" };
const current = createQueueTestPromptTurn({ chatId: 2, replyToMessageId: 22 });
current.transportStamp = { profile: "b", generation: "epoch-b" };
let items: TelegramQueueItem<string>[] = [stale, current];
const sent: number[] = [];
const controller = createTelegramQueueDispatchController<string>({
getQueuedItems: () => items,
setQueuedItems: (next) => {
items = next;
},
canDispatch: () => true,
isQueueItemTransportActive: (item) =>
item.transportStamp?.profile === "b" &&
item.transportStamp.generation === "epoch-b",
updateStatus: () => {},
sendTextReply: async () => undefined,
onPromptDispatchStart: (_ctx, chatId) => sent.push(chatId),
sendUserMessage: () => {},
onPromptDispatchFailure: () => {},
});
controller.dispatchNext("ctx");
assert.deepEqual(sent, [2]);
assert.deepEqual(items, [current]);
});
test("Dispatch runtime idles on none and executes control items directly", () => {
const events: string[] = [];
executeTelegramQueueDispatchPlan(
{ kind: "none", remainingItems: [] },
{
executeControlItem: () => {
events.push("control");
},
onPromptDispatchStart: () => {
events.push("prompt-start");
},
sendUserMessage: () => {
events.push("prompt");
},
onPromptDispatchFailure: (message) => {
events.push(`error:${message}`);
},
onIdle: () => {
events.push("idle");
},
},
);
executeTelegramQueueDispatchPlan(
{
kind: "control",
item: {
kind: "control",
controlType: "status",
chatId: 1,
replyToMessageId: 1,
queueOrder: 1,
queueLane: "control",
laneOrder: 0,
statusSummary: "status",
execute: async () => {},
},
remainingItems: [],
},
{
executeControlItem: () => {
events.push("control");
},
onPromptDispatchStart: () => {
events.push("prompt-start");
},
sendUserMessage: () => {
events.push("prompt");
},
onPromptDispatchFailure: (message) => {
events.push(`error:${message}`);
},
onIdle: () => {
events.push("idle");
},
},
);
assert.deepEqual(events, ["idle", "control"]);
});
test("Dispatch runtime sends prompt turns as normal user messages", () => {
const events: string[] = [];
executeTelegramQueueDispatchPlan(
{
kind: "prompt",
item: {
kind: "prompt",
chatId: 2,
replyToMessageId: 3,
sourceMessageIds: [3],
queueOrder: 2,
queueLane: "default",
laneOrder: 2,
queuedAttachments: [],
content: [{ type: "text", text: "prompt" }],
historyText: "prompt",
statusSummary: "prompt",
},
remainingItems: [],
},
{
executeControlItem: () => {
events.push("control");
},
onPromptDispatchStart: (chatId) => {
events.push(`start:${chatId}`);
},
sendUserMessage: (_content, options) => {
events.push(`send:${options?.deliverAs ?? "default"}`);
},
onPromptDispatchFailure: (message) => {
events.push(`error:${message}`);
},
onIdle: () => {
events.push("idle");
},
},
);
assert.deepEqual(events, ["start:2", "send:default"]);
});
test("Dispatch runtime reports prompt dispatch failures after starting", () => {
const events: string[] = [];
executeTelegramQueueDispatchPlan(
{
kind: "prompt",
item: {
kind: "prompt",
chatId: 2,
replyToMessageId: 3,
sourceMessageIds: [3],
queueOrder: 2,
queueLane: "default",
laneOrder: 2,
queuedAttachments: [],
content: [{ type: "text", text: "prompt" }],
historyText: "prompt",
statusSummary: "prompt",
},
remainingItems: [],
},
{
executeControlItem: () => {
events.push("control");
},
onPromptDispatchStart: (chatId) => {
events.push(`start:${chatId}`);
},
sendUserMessage: () => {
throw new Error("boom");
},
onPromptDispatchFailure: (message) => {
events.push(`error:${message}`);
},
onIdle: () => {
events.push("idle");
},
},
);
assert.deepEqual(events, ["start:2", "error:boom"]);
});
test("Queue dispatch controller plans prompts and reports dispatch failures", () => {
const events: string[] = [];
let queuedItems: TelegramQueueItem<string>[] = [
{
kind: "prompt",
chatId: 2,
replyToMessageId: 3,
sourceMessageIds: [3],
queueOrder: 2,
queueLane: "default",
laneOrder: 2,
queuedAttachments: [],
content: [{ type: "text", text: "prompt" }],
historyText: "prompt",
statusSummary: "prompt",
},
];
const controller = createTelegramQueueDispatchController<string>({
getQueuedItems: () => queuedItems,
setQueuedItems: (items) => {
queuedItems = items;
events.push(`items:${items.length}`);
},
canDispatch: () => true,
updateStatus: (_ctx, error) => {
events.push(`status:${error ?? "ok"}`);
},
sendTextReply: async () => undefined,
onPromptDispatchStart: (_ctx, chatId) => {
events.push(`start:${chatId}`);
},
sendUserMessage: () => {
throw new Error("boom");
},
onPromptDispatchFailure: (_ctx, message) => {
events.push(`failure:${message}`);
},
});
controller.dispatchNext("ctx");
assert.deepEqual(events, ["items:1", "start:2", "failure:boom"]);
assert.equal(queuedItems.length, 1);
});
test("Queue dispatch runtime binds readiness guards to dispatch controller", () => {
const events: string[] = [];
let active = true;
let queuedItems: TelegramQueueItem<string>[] = [
{
kind: "prompt",
chatId: 2,
replyToMessageId: 3,
sourceMessageIds: [3],
queueOrder: 2,
queueLane: "default",
laneOrder: 2,
queuedAttachments: [],
content: [{ type: "text", text: "prompt" }],
historyText: "prompt",
statusSummary: "prompt",
},
];
const controller = createTelegramQueueDispatchRuntime<string>({
getQueuedItems: () => queuedItems,
setQueuedItems: (items) => {
queuedItems = items;
events.push(`items:${items.length}`);
},
isCompactionInProgress: () => false,
hasActiveTurn: () => active,
hasDispatchPending: () => false,
isIdle: () => true,
hasPendingMessages: () => false,
updateStatus: (_ctx, error) => {
events.push(`status:${error ?? "ok"}`);
},
sendTextReply: async () => undefined,
onPromptDispatchStart: (_ctx, chatId) => {
events.push(`start:${chatId}`);
},
sendUserMessage: () => {
events.push("send");
},
onPromptDispatchFailure: () => {
events.push("unexpected:failure");
},
});
controller.dispatchNext("ctx");
active = false;
controller.dispatchNext("ctx");
assert.deepEqual(events, ["status:ok", "items:1", "start:2", "send"]);
});
test("Queue dispatch controller executes control items and continues", async () => {
const events: string[] = [];
let queuedItems: TelegramQueueItem<string>[] = [
{
kind: "control",
controlType: "status",
chatId: 1,
replyToMessageId: 2,
queueOrder: 1,
queueLane: "control",
laneOrder: 1,
statusSummary: "control",
execute: async (ctx) => {
events.push(`control:${ctx}`);
},
},
{
kind: "prompt",
chatId: 3,
replyToMessageId: 4,
sourceMessageIds: [4],
queueOrder: 2,
queueLane: "default",
laneOrder: 2,
queuedAttachments: [],
content: [{ type: "text", text: "prompt" }],
historyText: "prompt",
statusSummary: "prompt",
},
];
const controller = createTelegramQueueDispatchController<string>({
getQueuedItems: () => queuedItems,
setQueuedItems: (items) => {
queuedItems = items;
events.push(`items:${items.length}`);
},
canDispatch: () => true,
updateStatus: (_ctx, error) => {
events.push(`status:${error ?? "ok"}`);
},
sendTextReply: async () => undefined,
onPromptDispatchStart: (_ctx, chatId) => {
events.push(`start:${chatId}`);
},
sendUserMessage: () => {
events.push("send");
},
onPromptDispatchFailure: () => {
events.push("unexpected:failure");
},
});
controller.dispatchNext("ctx");
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(events, [
"items:1",
"status:ok",
"control:ctx",
"status:ok",
"items:1",
"start:3",
"send",
]);
});
test("Queue dispatch controller blocks reentrant prompt dispatch while control is pending", async () => {
const events: string[] = [];
let releaseControl: () => void = () => {};
const controlSettled = new Promise<void>((resolve) => {
releaseControl = resolve;
});
let queuedItems: TelegramQueueItem<string>[] = [
createQueueTestControlItem<string>({
queueOrder: 1,
laneOrder: 1,
execute: async (ctx) => {
events.push(`control:start:${ctx}`);
await controlSettled;
events.push("control:end");
},
}),
];
const prompt = createQueueTestPromptTurn({
chatId: 3,
replyToMessageId: 4,
sourceMessageIds: [4],
queueOrder: 2,
laneOrder: 2,
});
const controller = createTelegramQueueDispatchController<string>({
getQueuedItems: () => queuedItems,
setQueuedItems: (items) => {
queuedItems = items;
events.push(`items:${items.length}`);
},
canDispatch: () => true,
updateStatus: (_ctx, error) => {
events.push(`status:${error ?? "ok"}`);
},
sendTextReply: async () => undefined,
onPromptDispatchStart: (_ctx, chatId) => {
events.push(`start:${chatId}`);
},
sendUserMessage: () => {
events.push("send");
},
onPromptDispatchFailure: () => {
events.push("unexpected:failure");
},
});
controller.dispatchNext("ctx");
queuedItems = [prompt];
controller.dispatchNext("ctx");
assert.equal(events.includes("send"), false);
releaseControl();
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(events, [
"items:0",
"status:ok",
"control:start:ctx",
"status:ok",
"control:end",
"status:ok",
"items:1",
"start:3",
"send",
]);
});
test("Queue dispatch controller does not resume prompts after shutdown clears context during control", async () => {
const events: string[] = [];
let dispatchContextActive = true;
let releaseControl: () => void = () => {};
const controlSettled = new Promise<void>((resolve) => {
releaseControl = resolve;
});
let queuedItems: TelegramQueueItem<string>[] = [
createQueueTestControlItem<string>({
queueOrder: 1,
laneOrder: 1,
execute: async () => {
events.push("control:start");
await controlSettled;
events.push("control:end");
},
}),
createQueueTestPromptTurn({
chatId: 3,
replyToMessageId: 4,
sourceMessageIds: [4],
queueOrder: 2,
laneOrder: 2,
}),
];
const controller = createTelegramQueueDispatchController<string>({
hasDispatchContext: () => dispatchContextActive,
getQueuedItems: () => queuedItems,
setQueuedItems: (items) => {
queuedItems = items;
events.push(`items:${items.length}`);
},
canDispatch: () => true,
updateStatus: (_ctx, error) => {
events.push(`status:${error ?? "ok"}`);
},
sendTextReply: async () => undefined,
onPromptDispatchStart: (_ctx, chatId) => {
events.push(`start:${chatId}`);
},
sendUserMessage: () => {
events.push("send");
},
onPromptDispatchFailure: () => {
events.push("unexpected:failure");
},
});
controller.dispatchNext("ctx");
dispatchContextActive = false;
queuedItems = [];
releaseControl();
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(events, [
"items:1",
"status:ok",
"control:start",
"control:end",
]);
});
test("Session runtime helper resets session start state", () => {
const currentModel = createQueueTestModel();
const state = buildTelegramSessionStartState(currentModel);
assert.equal(state.currentTelegramModel, currentModel);
assert.equal(state.activeTelegramToolExecutions, 0);
assert.equal(state.nextQueuedTelegramItemOrder, 0);
assert.equal(state.nextQueuedTelegramControlOrder, 0);
assert.equal(state.telegramTurnDispatchPending, false);
assert.equal(state.compactionInProgress, false);
});
test("Session runtime helper runs start side effects in order", async () => {
const events: string[] = [];
const currentModel = createQueueTestModel();
await startTelegramSessionRuntime({
ctx: "ctx",
currentModel,
loadConfig: async () => {
events.push("load");
},
applyState: (state) => {
events.push(`state:${state.currentTelegramModel?.id}`);
},
bindDeferredDispatchContext: (ctx) => {
events.push(`bind:${ctx}`);
},
prepareTempDir: async () => {
events.push("temp");
},
updateStatus: () => {
events.push("status");
},
});
assert.deepEqual(events, [
"load",
"state:gpt-5",
"temp",
"bind:ctx",
"status",
]);
});
test("Session runtime start swallows only stale deferred-dispatch context binding", async () => {
const currentModel = createQueueTestModel();
const baseDeps = {
ctx: "ctx",
currentModel,
loadConfig: async () => {},
applyState: () => {},
prepareTempDir: async () => {},
updateStatus: () => {},
};
await assert.doesNotReject(() =>
startTelegramSessionRuntime({
...baseDeps,
bindDeferredDispatchContext: () => {
throw new Error("ctx is stale after session replacement");
},
}),
);
await assert.rejects(
() =>
startTelegramSessionRuntime({
...baseDeps,
bindDeferredDispatchContext: () => {
throw new Error("bind broke");
},
}),
/bind broke/,
);
});
test("Session runtime helper clears shutdown state", () => {
const state = buildTelegramSessionShutdownState<string>();
assert.deepEqual(state.queuedTelegramItems, []);
assert.equal(state.nextQueuedTelegramItemOrder, 0);
assert.equal(state.nextQueuedTelegramControlOrder, 0);
assert.equal(state.nextPriorityReactionOrder, 0);
assert.equal(state.currentTelegramModel, undefined);
assert.equal(state.activeTelegramToolExecutions, 0);
assert.equal(state.telegramTurnDispatchPending, false);
assert.equal(state.compactionInProgress, false);
assert.equal(state.foldQueuedPromptsIntoHistory, false);
});
test("Session lifecycle runtime binds state applier into lifecycle hooks", async () => {
const events: string[] = [];
const hooks = createTelegramSessionLifecycleRuntime<
string,
TelegramQueueItem<string>,
{ provider: string; id: string }
>({
getCurrentModel: () => createQueueTestModel(),
loadConfig: async () => {
events.push("load");
},
setQueuedItems: (items) => {
events.push(`queued:${items.length}`);
},
setCurrentModel: (model) => {
events.push(`model:${model?.id ?? "none"}`);
},
setPendingModelSwitch: () => {
events.push("pending:clear");
},
syncCounters: () => {
events.push("counters");
},
syncFlags: () => {
events.push("flags");
},
bindDeferredDispatchContext: (ctx) => {
events.push(`bind:${ctx}`);
},
prepareTempDir: async () => {
events.push("temp");
},
updateStatus: (ctx) => {
events.push(`status:${ctx}`);
},
unbindDeferredDispatchContext: () => {
events.push("unbind");
},
clearPendingMediaGroups: () => {
events.push("media:clear");
},
clearModelMenuState: () => {
events.push("menu:clear");
},
getActiveTurnChatId: () => undefined,
clearPreview: async () => {
events.push("preview:clear");
},
clearActiveTurn: () => {
events.push("turn:clear");
},
clearAbort: () => {
events.push("abort:clear");
},
stopPolling: async () => {
events.push("polling:stop");
},
});
await hooks.onSessionStart(undefined, "ctx");
await hooks.onSessionShutdown();
assert.deepEqual(events, [
"load",
"model:gpt-5",
"pending:clear",
"counters",
"flags",
"temp",
"bind:ctx",
"status:ctx",
"unbind",
"polling:stop",
"queued:0",
"counters",
"flags",
"model:none",
"pending:clear",
"media:clear",
"menu:clear",
"turn:clear",
"abort:clear",
]);
});
test("Session lifecycle hooks bind start and shutdown runtime ports", async () => {
const events: string[] = [];
const hooks = createTelegramSessionLifecycleHooks<
{ model?: { id: string } },
string,
{ id: string }
>({
getCurrentModel: (ctx) => ctx.model,
loadConfig: async () => {
events.push("load");
},
applySessionStartState: (state) => {
events.push(`start:${state.currentTelegramModel?.id}`);
},
bindDeferredDispatchContext: (ctx) => {
events.push(`bind:${ctx.model?.id ?? "none"}`);
},
prepareTempDir: async () => {
events.push("temp");
},
updateStatus: (ctx) => {
events.push(`status:${ctx.model?.id ?? "none"}`);
},
applySessionShutdownState: (state) => {
events.push(`shutdown:${state.queuedTelegramItems.length}`);
},
unbindDeferredDispatchContext: () => {
events.push("unbind");
},
clearPendingMediaGroups: () => {
events.push("media");
},
clearModelMenuState: () => {
events.push("menu");
},
getActiveTurnChatId: () => 7,
getActiveTurnTarget: () => ({ chatId: 7, threadId: 77 }),
clearPreview: async (chatId, options) => {
events.push(`preview:${chatId}:${options?.target?.threadId ?? "none"}`);
},
clearActiveTurn: () => {
events.push("turn");
},
clearAbort: () => {
events.push("abort");
},
stopPolling: async () => {
events.push("poll");
},
});
await hooks.onSessionStart({}, { model: { id: "gpt-5" } });
await hooks.onSessionShutdown();
assert.deepEqual(events, [
"load",
"start:gpt-5",
"temp",
"bind:gpt-5",
"status:gpt-5",
"unbind",
"poll",
"shutdown:0",
"media",
"menu",
"preview:7:77",
"turn",
"abort",
]);
});
// --- Dispatch ready-item contract ---
// When executeTelegramQueueDispatchPlan dispatches a prompt, it must call
// sendUserMessage as a normal turn (no followUp delivery option) because
// the caller already confirmed the agent is ready via canDispatch.
// Regression: followUp can queue idle prompts on some Pi-compatible runtimes.
await test("executeTelegramQueueDispatchPlan sends ready prompts as normal user turns", async (t) => {
await t.test(
"sendUserMessage is called without followUp delivery option for prompt plan",
() => {
let sendUserMessageArgs: { content: unknown; options: unknown } | undefined;
let onPromptDispatchChatId: number | undefined;
const deps: TelegramDispatchRuntimeDeps = {
executeControlItem: () => {},
onPromptDispatchStart: (chatId) => {
onPromptDispatchChatId = chatId;
},
sendUserMessage: (content, options) => {
sendUserMessageArgs = { content, options };
},
onPromptDispatchFailure: () => {},
onIdle: () => {},
};
const item: PendingTelegramTurn = {
kind: "prompt",
chatId: 123456,
sourceMessageIds: [1],
replyToMessageId: 0,
queueOrder: 0,
queueLane: "default",
laneOrder: 0,
statusSummary: "test",
content: [{ type: "text", text: "hello" }],
historyText: "hello",
queuedAttachments: [],
};
const plan = {
kind: "prompt" as const,
item,
remainingItems: [] as TelegramQueueItem[],
};
executeTelegramQueueDispatchPlan(plan, deps);
assert.equal(onPromptDispatchChatId, 123456);
assert.ok(sendUserMessageArgs, "sendUserMessage was called");
assert.deepStrictEqual(sendUserMessageArgs.content, item.content);
assert.equal(
sendUserMessageArgs.options,
undefined,
"sendUserMessage must be called WITHOUT followUp delivery option",
);
},
);
await t.test(
"sendUserMessage receives no second argument (no followUp)",
() => {
let sendUserMessageArgCount = 0;
const deps: TelegramDispatchRuntimeDeps = {
executeControlItem: () => {},
onPromptDispatchStart: () => {},
sendUserMessage: (...args: unknown[]) => {
sendUserMessageArgCount = args.length;
},
onPromptDispatchFailure: () => {},
onIdle: () => {},
};
const item: PendingTelegramTurn = {
kind: "prompt",
chatId: 789,
sourceMessageIds: [2],
replyToMessageId: 0,
queueOrder: 1,
queueLane: "default",
laneOrder: 1,
statusSummary: "test",
content: [{ type: "text", text: "arg count test" }],
historyText: "arg count test",
queuedAttachments: [],
};
executeTelegramQueueDispatchPlan(
{ kind: "prompt", item, remainingItems: [] },
deps,
);
assert.equal(
sendUserMessageArgCount,
1,
"sendUserMessage must receive exactly 1 argument (content only)",
);
},
);
});