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)
3982 lines
113 KiB
TypeScript
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)",
|
|
);
|
|
},
|
|
);
|
|
});
|