Validate / validate (push) Successful in 9m52s
- the poll loop now clears its error status after the next successful getUpdates, not before the retry, so a slow retry never shows as healthy - recovery is reported only when getUpdates itself failed - a send that may have reached Telegram is reported as unconfirmed instead of failed - README and CHANGELOG describe the states, toasts, queue preview and spinner text
1294 lines
41 KiB
TypeScript
1294 lines
41 KiB
TypeScript
/**
|
|
* Regression tests for Telegram status helpers
|
|
* Covers runtime diagnostics lines and recent-event redaction/ring-buffer behavior
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
buildTelegramBridgeStatusLines,
|
|
buildTelegramRuntimeEventLines,
|
|
buildTelegramStatusBarText,
|
|
buildTelegramStatusBarLines,
|
|
clearTelegramStatusLineProviders,
|
|
createTelegramBridgeStatusRuntime,
|
|
createTelegramRuntimeDiagnosticsSnapshotScheduler,
|
|
createTelegramRuntimeEventRecorder,
|
|
createTelegramRuntimeLogScope,
|
|
createTelegramStatusHtmlBuilder,
|
|
createTelegramStatusSnapshot,
|
|
createTelegramStatusRuntime,
|
|
getTelegramStatusBarProcessingStatus,
|
|
formatTelegramWorkingMessage,
|
|
sanitizeTelegramStatusText,
|
|
summarizeTelegramStatusError,
|
|
recordStructuredTelegramRuntimeEvent,
|
|
recordTelegramRuntimeEvent,
|
|
registerTelegramStatusLineProvider,
|
|
type TelegramRuntimeEvent,
|
|
} from "../lib/status.ts";
|
|
|
|
test("Status helpers build runtime log scope and persisted snapshot projections", () => {
|
|
const state = {
|
|
busRole: "leader" as const,
|
|
botThreadMode: "enabled" as const,
|
|
botThreadModeUpdatedAtMs: 10,
|
|
botThreadModeAction: "probe",
|
|
instanceSlot: "A",
|
|
instanceThreadName: "Axial",
|
|
pollingActive: true,
|
|
lockState: "active-here",
|
|
pendingDispatch: true,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 1,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
busFollowers: [{ instanceId: "follower", lastHeartbeatMs: 5 }],
|
|
topicTargets: [{ instanceId: "leader", status: "active" }],
|
|
threadReservations: [{ slot: "B", reason: "startup" }],
|
|
topicSyncObservations: [{ syncStatus: "open", observedAtMs: 9 }],
|
|
syncState: { pairing: { status: "fresh" } },
|
|
recentRuntimeEvents: [],
|
|
};
|
|
|
|
assert.deepEqual(
|
|
createTelegramRuntimeLogScope({ state, instanceId: "instance-1" }),
|
|
{
|
|
instanceId: "instance-1",
|
|
role: "leader",
|
|
slot: "A",
|
|
threadName: "Axial",
|
|
lockState: "active-here",
|
|
},
|
|
);
|
|
assert.deepEqual(createTelegramStatusSnapshot(state), {
|
|
runtime: {
|
|
busRole: "leader",
|
|
botThreadMode: "enabled",
|
|
botThreadModeUpdatedAtMs: 10,
|
|
botThreadModeAction: "probe",
|
|
instanceSlot: "A",
|
|
instanceThreadName: "Axial",
|
|
pollingActive: true,
|
|
lockState: "active-here",
|
|
},
|
|
liveRoster: {
|
|
busFollowers: [{ instanceId: "follower", lastHeartbeatMs: 5 }],
|
|
topicTargets: [{ instanceId: "leader", status: "active" }],
|
|
reservations: [{ slot: "B", reason: "startup" }],
|
|
syncObservations: [{ syncStatus: "open", observedAtMs: 9 }],
|
|
},
|
|
diagnostics: {
|
|
pendingDispatch: true,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 1,
|
|
pendingModelSwitch: false,
|
|
syncState: { pairing: { status: "fresh" } },
|
|
threadReconciliation: undefined,
|
|
recentRuntimeEvents: [],
|
|
},
|
|
});
|
|
});
|
|
|
|
test("Status runtime diagnostics scheduler coalesces snapshot persists", async () => {
|
|
let scheduled: (() => void) | undefined;
|
|
let persistCount = 0;
|
|
const errors: unknown[] = [];
|
|
const schedule = createTelegramRuntimeDiagnosticsSnapshotScheduler({
|
|
persistSnapshot: async () => {
|
|
persistCount += 1;
|
|
},
|
|
recordError: (error) => errors.push(error),
|
|
setTimer(callback) {
|
|
scheduled = callback as () => void;
|
|
return { unref() {} } as ReturnType<typeof setTimeout>;
|
|
},
|
|
});
|
|
|
|
schedule();
|
|
schedule();
|
|
scheduled?.();
|
|
await Promise.resolve();
|
|
|
|
assert.equal(persistCount, 1);
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
const statusBarTheme = {
|
|
fg: (token: string, text: string) => `<${token}>${text}</${token}>`,
|
|
};
|
|
|
|
function plainStatusText(text: string): string {
|
|
return text.replace(/<\/?[a-z]+>/g, "");
|
|
}
|
|
|
|
function escapeRegExp(text: string): string {
|
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
test("Status bar text keeps the label, state color and queue for every connection state", () => {
|
|
const base = {
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: false,
|
|
queuedStatus: "",
|
|
};
|
|
const cases: Array<{
|
|
state: Partial<Parameters<typeof buildTelegramStatusBarText>[1]>;
|
|
label: string;
|
|
token: string;
|
|
word: string;
|
|
queued?: string;
|
|
}> = [
|
|
{ state: { hasBotToken: false, pollingActive: false, paired: false }, label: "telegram", token: "muted", word: "not configured" },
|
|
{ state: { processing: true, queuedStatus: " +1" }, label: "telegram", token: "warning", word: "active", queued: " +1" },
|
|
{ state: { processing: true, processingStatus: "dispatching", queuedStatus: " +1" }, label: "telegram", token: "warning", word: "active", queued: " +1" },
|
|
{ state: { processing: true, processingStatus: "active" }, label: "telegram", token: "warning", word: "active" },
|
|
{ state: { compactionInProgress: true }, label: "telegram", token: "success", word: "connected" },
|
|
{ state: { compactionInProgress: true, processing: true, processingStatus: "active" }, label: "telegram", token: "warning", word: "active" },
|
|
{ state: { pollingActive: false, instanceThreadName: "Aurora", processing: true, processingStatus: "queued", queuedStatus: " +2" }, label: "telegram", token: "muted", word: "disconnected", queued: " +2" },
|
|
{ state: { pollingActive: false, busRole: "follower" }, label: "telegram", token: "success", word: "follower" },
|
|
{ state: { pollingActive: false, busRole: "follower", instanceThreadName: "Amber", processing: true, processingStatus: "active" }, label: "Amber", token: "warning", word: "active" },
|
|
{ state: { pollingActive: false, busRole: "follower", busLifecyclePhase: "electing" }, label: "telegram", token: "warning", word: "electing" },
|
|
{ state: { pollingActive: false, busLifecyclePhase: "electing" }, label: "telegram", token: "warning", word: "electing" },
|
|
{ state: { pollingActive: false, busRole: "follower", instanceThreadName: "Follower" }, label: "telegram", token: "success", word: "follower" },
|
|
{ state: { pollingActive: false, busRole: "follower", instanceThreadName: "Lname" }, label: "Lname", token: "success", word: "follower" },
|
|
{ state: { pollingActive: false, busRole: "follower", instanceSlot: "O", instanceThreadName: "extensions Follower" }, label: "extensions Follower", token: "success", word: "follower" },
|
|
{ state: { pollingActive: false, busRole: "follower", instanceSlot: "O", instanceThreadName: "Oname" }, label: "Oname", token: "success", word: "follower" },
|
|
{ state: { busRole: "leader" }, label: "telegram", token: "success", word: "leader" },
|
|
{ state: { busRole: "leader", instanceThreadName: "🌙 A-identity" }, label: "🌙 A-identity", token: "success", word: "leader" },
|
|
{ state: { paired: false, processing: true, processingStatus: "queued", queuedStatus: " +1" }, label: "telegram", token: "warning", word: "awaiting pairing", queued: " +1" },
|
|
];
|
|
for (const { state, label, token, word, queued } of cases) {
|
|
const text = buildTelegramStatusBarText(statusBarTheme, { ...base, ...state });
|
|
const expected = new RegExp(
|
|
`^<accent>${escapeRegExp(label)}</accent> <${token}>\\S+ ${word}</${token}>${queued ? `<success>${escapeRegExp(queued)}</success>` : ""}`,
|
|
);
|
|
assert.match(text, expected, JSON.stringify(state));
|
|
}
|
|
assert.match(
|
|
buildTelegramStatusBarText(statusBarTheme, {
|
|
...base,
|
|
error: "typing failed",
|
|
}),
|
|
/^<accent>telegram<\/accent> <error>\S+ error<\/error> <muted>typing failed<\/muted>$/,
|
|
);
|
|
});
|
|
|
|
test("Status bar shows bot, message counters and last activity only while the bridge is up", () => {
|
|
const lastAtMs = new Date(2026, 9, 1, 14, 32).getTime();
|
|
const connected = {
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: false,
|
|
queuedStatus: "",
|
|
botUsername: "demo_bot",
|
|
traffic: { received: 12, sent: 11, lastAtMs },
|
|
};
|
|
const up = plainStatusText(buildTelegramStatusBarText(statusBarTheme, connected));
|
|
assert.match(up, /connected/);
|
|
assert.match(up, /@demo_bot/);
|
|
assert.match(up, /12 \S*11/);
|
|
assert.match(up, /14:32/);
|
|
|
|
const withoutActivity = plainStatusText(
|
|
buildTelegramStatusBarText(statusBarTheme, {
|
|
...connected,
|
|
traffic: { received: 0, sent: 0 },
|
|
}),
|
|
);
|
|
assert.match(withoutActivity, /0 \S*0/);
|
|
assert.doesNotMatch(withoutActivity, /\d\d:\d\d/);
|
|
|
|
const working = plainStatusText(
|
|
buildTelegramStatusBarText(statusBarTheme, {
|
|
...connected,
|
|
processing: true,
|
|
processingStatus: "active",
|
|
}),
|
|
);
|
|
assert.match(working, /active/);
|
|
assert.match(working, /@demo_bot/);
|
|
assert.match(working, /12 \S*11/);
|
|
|
|
const offline = plainStatusText(
|
|
buildTelegramStatusBarText(statusBarTheme, { ...connected, pollingActive: false }),
|
|
);
|
|
assert.doesNotMatch(offline, /@demo_bot|14:32/);
|
|
assert.match(offline, /disconnected/);
|
|
});
|
|
|
|
test("Status bar tells the operator how to leave a non-working state", () => {
|
|
const base = {
|
|
hasBotToken: true,
|
|
pollingActive: false,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: false,
|
|
queuedStatus: "",
|
|
};
|
|
assert.match(
|
|
plainStatusText(buildTelegramStatusBarText(statusBarTheme, { ...base, hasBotToken: false, paired: false })),
|
|
/\/telegram-setup/,
|
|
);
|
|
const pairing = plainStatusText(
|
|
buildTelegramStatusBarText(statusBarTheme, { ...base, paired: false, botUsername: "demo_bot" }),
|
|
);
|
|
assert.match(pairing, /\/start/);
|
|
assert.match(pairing, /@demo_bot/);
|
|
assert.match(
|
|
plainStatusText(buildTelegramStatusBarText(statusBarTheme, base)),
|
|
/\/telegram-connect/,
|
|
);
|
|
});
|
|
|
|
test("Status bar follows the host symbol preset and falls back without one", () => {
|
|
const state = {
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: false,
|
|
queuedStatus: "",
|
|
botUsername: "demo_bot",
|
|
traffic: { received: 3, sent: 2, lastAtMs: new Date(2026, 9, 1, 9, 5).getTime() },
|
|
};
|
|
const ascii = plainStatusText(
|
|
buildTelegramStatusBarText(
|
|
{
|
|
...statusBarTheme,
|
|
status: { enabled: "[x]" },
|
|
sep: { dot: " - " },
|
|
icon: { input: "in:", output: "out:", time: "t:" },
|
|
},
|
|
state,
|
|
),
|
|
);
|
|
assert.equal(ascii, "telegram [x] connected - @demo_bot - in:3 out:2 - t: 09:05");
|
|
const fallback = plainStatusText(buildTelegramStatusBarText(statusBarTheme, state));
|
|
assert.match(fallback, /^telegram \S+ connected · @demo_bot · \S*3 \S*2 · \S+ 09:05$/u);
|
|
});
|
|
|
|
test("Status runtime updates the status bar and exposes bridge lines", () => {
|
|
const events: string[] = [];
|
|
const ctx = {
|
|
ui: {
|
|
theme: {
|
|
fg: (token: string, text: string) => `<${token}>${text}</${token}>`,
|
|
},
|
|
setStatus: (key: string, text: string) => {
|
|
events.push(`${key}:${text}`);
|
|
},
|
|
},
|
|
};
|
|
const runtime = createTelegramStatusRuntime({
|
|
getStatusBarState: (_ctx, error) => ({
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: false,
|
|
queuedStatus: "",
|
|
error,
|
|
}),
|
|
getBridgeStatusLineState: () => ({
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 7,
|
|
lockState: "active here",
|
|
pollingActive: true,
|
|
lastUpdateId: 10,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
recentRuntimeEvents: [],
|
|
}),
|
|
});
|
|
runtime.updateStatus(ctx, "demo error");
|
|
assert.match(
|
|
events[0],
|
|
/^telegram:<accent>telegram<\/accent> <error>\S+ error<\/error> <muted>demo error<\/muted>$/,
|
|
);
|
|
assert.deepEqual(runtime.getStatusLines().slice(0, 3), [
|
|
"connection:",
|
|
"- bot: @demo_bot",
|
|
"- user: 7",
|
|
]);
|
|
});
|
|
|
|
test("Status runtime keeps theme colors in the TUI through a widget and stays plain elsewhere", () => {
|
|
const createContext = (mode: string | undefined) => {
|
|
const events: string[] = [];
|
|
return {
|
|
events,
|
|
ctx: {
|
|
mode,
|
|
ui: {
|
|
theme: statusBarTheme,
|
|
setStatus: (key: string, text: string | undefined) => {
|
|
events.push(`status:${key}:${text}`);
|
|
},
|
|
setWidget: (
|
|
key: string,
|
|
content: string[] | undefined,
|
|
options?: { placement?: string },
|
|
) => {
|
|
events.push(`widget:${key}:${options?.placement}:${content?.join("|")}`);
|
|
},
|
|
},
|
|
},
|
|
};
|
|
};
|
|
const runtime = createTelegramStatusRuntime({
|
|
getStatusBarState: () => ({
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: false,
|
|
queuedStatus: "",
|
|
}),
|
|
getBridgeStatusLineState: () => {
|
|
throw new Error("not used");
|
|
},
|
|
});
|
|
|
|
const tui = createContext("tui");
|
|
runtime.updateStatus(tui.ctx);
|
|
assert.equal(tui.events.length, 2);
|
|
assert.match(tui.events[0], /^widget:telegram:belowEditor:<accent>telegram<\/accent> <success>\S+ connected<\/success>$/);
|
|
assert.equal(tui.events[1], "status:telegram:undefined");
|
|
|
|
for (const mode of ["rpc", "json", "print", undefined]) {
|
|
const other = createContext(mode);
|
|
runtime.updateStatus(other.ctx);
|
|
assert.equal(other.events.length, 1, String(mode));
|
|
assert.match(other.events[0], /^status:telegram:<accent>telegram<\/accent> <success>\S+ connected<\/success>$/);
|
|
}
|
|
|
|
const noWidgetApi = createContext("tui");
|
|
delete (noWidgetApi.ctx.ui as { setWidget?: unknown }).setWidget;
|
|
runtime.updateStatus(noWidgetApi.ctx);
|
|
assert.equal(noWidgetApi.events.length, 1);
|
|
assert.match(noWidgetApi.events[0], /^status:telegram:/);
|
|
});
|
|
|
|
test("Error summaries turn transport failures into a short state and next step", () => {
|
|
const cases: Array<[string, string | undefined, string | undefined, string | undefined]> = [
|
|
["Telegram API getUpdates failed: HTTP 401: Unauthorized", "fatal", "invalid token", "/telegram-setup"],
|
|
["Telegram API sendMessage failed: HTTP 429: Too Many Requests: retry after 12", "retrying", "rate limited 12s", "retrying"],
|
|
["Telegram API sendMessage failed: HTTP 429", "retrying", "rate limited", "retrying"],
|
|
["Conflict: terminated by other getUpdates request; make sure that only one bot instance is running", "attention", "another poller is active", "/telegram-status"],
|
|
["Telegram API getUpdates failed: HTTP 502: Bad Gateway", "retrying", "Telegram unavailable", "retrying"],
|
|
["Unable to connect. Is the computer able to access the url?", "retrying", "offline", "retrying"],
|
|
["fetch failed", "retrying", "offline", "retrying"],
|
|
["Telegram API sendRichMessage may have committed before transport failed.", "attention", "delivery unconfirmed", undefined],
|
|
["typing failed", undefined, undefined, undefined],
|
|
["settingsManager.reload is not a function", undefined, undefined, undefined],
|
|
];
|
|
for (const [message, kind, label, hint] of cases) {
|
|
const summary = summarizeTelegramStatusError(message);
|
|
assert.equal(summary?.kind, kind, message);
|
|
assert.equal(summary?.label, label, message);
|
|
assert.equal(summary?.hint, hint, message);
|
|
}
|
|
});
|
|
|
|
test("Status bar shows classified errors with their next step and keeps unknown ones verbatim", () => {
|
|
const base = {
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: false,
|
|
queuedStatus: "",
|
|
};
|
|
const fatal = buildTelegramStatusBarText(statusBarTheme, {
|
|
...base,
|
|
error: "Telegram API getUpdates failed: HTTP 401: Unauthorized",
|
|
});
|
|
assert.match(fatal, /<error>\S+ invalid token<\/error>/);
|
|
assert.match(plainStatusText(fatal), /\/telegram-setup/);
|
|
const retrying = buildTelegramStatusBarText(statusBarTheme, {
|
|
...base,
|
|
error: "Unable to connect. Is the computer able to access the url?",
|
|
});
|
|
assert.match(retrying, /<warning>\S+ offline<\/warning>/);
|
|
assert.match(plainStatusText(retrying), /retrying/);
|
|
const attention = buildTelegramStatusBarText(statusBarTheme, {
|
|
...base,
|
|
error: "HTTP 409 Conflict",
|
|
});
|
|
assert.match(attention, /<warning>\S+ another poller is active<\/warning>/);
|
|
assert.match(plainStatusText(attention), /\/telegram-status/);
|
|
assert.match(
|
|
buildTelegramStatusBarText(statusBarTheme, { ...base, error: "typing failed" }),
|
|
/<error>\S+ error<\/error> <muted>typing failed<\/muted>/,
|
|
);
|
|
const long = plainStatusText(
|
|
buildTelegramStatusBarText(statusBarTheme, { ...base, error: "x".repeat(200) }),
|
|
);
|
|
assert.ok(long.includes("…"));
|
|
assert.equal(long.includes("x".repeat(101)), false);
|
|
});
|
|
|
|
test("Status bar lines carry no terminal control sequences from Telegram-controlled text", () => {
|
|
const hostile = "\u001b]0;pwned\u0007\u001b[31mred\u001b[0m\u202e";
|
|
const base = {
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: true,
|
|
queuedStatus: " +1",
|
|
instanceThreadName: `thread${hostile}`,
|
|
botUsername: `bot${hostile}`,
|
|
queuedPreview: [`queued${hostile}`],
|
|
};
|
|
const unsafe = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e]/;
|
|
for (const state of [
|
|
base,
|
|
{ ...base, error: `boom${hostile}` },
|
|
{ ...base, paired: false },
|
|
]) {
|
|
for (const line of buildTelegramStatusBarLines(statusBarTheme, state)) {
|
|
assert.doesNotMatch(line, unsafe, JSON.stringify(line));
|
|
}
|
|
}
|
|
assert.equal(sanitizeTelegramStatusText(" a\u001b[0m b\u0000 "), "a [0m b");
|
|
});
|
|
|
|
test("Status bar previews the next queued turns only while the bridge is up", () => {
|
|
const base = {
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: true,
|
|
queuedStatus: " +3",
|
|
};
|
|
const three = buildTelegramStatusBarLines(statusBarTheme, {
|
|
...base,
|
|
queuedPreview: ["one", "two", "three"],
|
|
});
|
|
assert.equal(three.length, 2);
|
|
assert.match(plainStatusText(three[1]), /next: "one" · "two" · \+1 more/);
|
|
const two = buildTelegramStatusBarLines(statusBarTheme, {
|
|
...base,
|
|
queuedPreview: ["one", "two"],
|
|
});
|
|
assert.match(plainStatusText(two[1]), /next: "one" · "two"$/);
|
|
for (const state of [
|
|
{ ...base, queuedPreview: [] },
|
|
{ ...base, queuedPreview: ["one"], pollingActive: false },
|
|
{ ...base, queuedPreview: ["one"], paired: false },
|
|
{ ...base, queuedPreview: ["one"], error: "typing failed" },
|
|
]) {
|
|
assert.equal(buildTelegramStatusBarLines(statusBarTheme, state).length, 1, JSON.stringify(state));
|
|
}
|
|
});
|
|
|
|
test("Status bar surfaces failed deliveries only when there are some", () => {
|
|
const state = {
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: false,
|
|
queuedStatus: "",
|
|
traffic: { received: 12, sent: 11, failed: 2 },
|
|
};
|
|
assert.match(plainStatusText(buildTelegramStatusBarText(statusBarTheme, state)), /12 \S*11 \S*2/);
|
|
const none = plainStatusText(
|
|
buildTelegramStatusBarText(statusBarTheme, {
|
|
...state,
|
|
traffic: { received: 12, sent: 11, failed: 0 },
|
|
}),
|
|
);
|
|
assert.equal(none.includes("✘"), false);
|
|
});
|
|
|
|
test("Status runtime puts the queue preview in the widget and keeps the plain status to one line", () => {
|
|
const runtime = createTelegramStatusRuntime({
|
|
getStatusBarState: () => ({
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: true,
|
|
queuedStatus: " +1",
|
|
queuedPreview: ["one"],
|
|
}),
|
|
getBridgeStatusLineState: () => {
|
|
throw new Error("not used");
|
|
},
|
|
});
|
|
const widgets: string[][] = [];
|
|
const statuses: Array<string | undefined> = [];
|
|
const ui = {
|
|
theme: statusBarTheme,
|
|
setStatus: (_key: string, text: string | undefined) => {
|
|
statuses.push(text);
|
|
},
|
|
setWidget: (_key: string, content: string[] | undefined) => {
|
|
widgets.push(content ?? []);
|
|
},
|
|
};
|
|
runtime.updateStatus({ mode: "tui", ui });
|
|
assert.equal(widgets[0].length, 2);
|
|
assert.match(plainStatusText(widgets[0][1]), /next: "one"/);
|
|
runtime.updateStatus({ mode: "rpc", ui });
|
|
assert.equal(statuses.at(-1)?.includes("next:"), false);
|
|
});
|
|
|
|
test("Working message names the Telegram prompt and ignores control characters", () => {
|
|
assert.equal(formatTelegramWorkingMessage("say pong"), 'Answering Telegram · "say pong"');
|
|
assert.equal(formatTelegramWorkingMessage(" "), "Answering Telegram");
|
|
assert.equal(formatTelegramWorkingMessage("a\u001b[0mb"), 'Answering Telegram · "a [0mb"');
|
|
});
|
|
|
|
test("Status lines expose thread reconciliation state", () => {
|
|
const lines = buildTelegramBridgeStatusLines({
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 7,
|
|
pollingActive: true,
|
|
lastUpdateId: 10,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
threadReconciliation: {
|
|
phase: "cleanup-required",
|
|
event: "cleanup-required",
|
|
atMs: 1000,
|
|
leaderEpoch: 3,
|
|
pendingProvisionCount: 1,
|
|
syncActionCount: 2,
|
|
cleanupActionCount: 1,
|
|
},
|
|
recentRuntimeEvents: [],
|
|
});
|
|
|
|
assert.ok(lines.includes("reconciliation:"));
|
|
assert.ok(
|
|
lines.includes("- phase: cleanup-required event=cleanup-required epoch=3"),
|
|
);
|
|
assert.ok(lines.includes("- counts: pending=1, sync=2, cleanup=1"));
|
|
});
|
|
|
|
test("Status runtime propagates status update failures to safety wrappers", () => {
|
|
const runtime = createTelegramStatusRuntime({
|
|
getStatusBarState: () => ({
|
|
hasBotToken: true,
|
|
pollingActive: true,
|
|
paired: true,
|
|
compactionInProgress: false,
|
|
processing: false,
|
|
queuedStatus: "",
|
|
}),
|
|
getBridgeStatusLineState: () => ({
|
|
botUsername: undefined,
|
|
allowedUserId: undefined,
|
|
pollingActive: false,
|
|
lastUpdateId: undefined,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
recentRuntimeEvents: [],
|
|
}),
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
runtime.updateStatus({
|
|
ui: {
|
|
theme: { fg: (_token: string, text: string) => text },
|
|
setStatus: () => {
|
|
throw new Error("ctx is stale after session reload");
|
|
},
|
|
},
|
|
}),
|
|
/stale after session/,
|
|
);
|
|
});
|
|
|
|
test("Status bar processing labels prefer the most specific live state", () => {
|
|
assert.equal(
|
|
getTelegramStatusBarProcessingStatus({
|
|
hasActiveTurn: true,
|
|
hasPendingDispatch: true,
|
|
hasPendingModelSwitch: true,
|
|
activeToolExecutions: 1,
|
|
queuedItems: 1,
|
|
}),
|
|
"model",
|
|
);
|
|
assert.equal(
|
|
getTelegramStatusBarProcessingStatus({
|
|
hasActiveTurn: true,
|
|
hasPendingDispatch: false,
|
|
hasPendingModelSwitch: false,
|
|
activeToolExecutions: 1,
|
|
queuedItems: 1,
|
|
}),
|
|
"active",
|
|
);
|
|
assert.equal(
|
|
getTelegramStatusBarProcessingStatus({
|
|
hasActiveTurn: false,
|
|
hasPendingDispatch: false,
|
|
hasPendingModelSwitch: false,
|
|
activeToolExecutions: 1,
|
|
queuedItems: 1,
|
|
}),
|
|
"active",
|
|
);
|
|
assert.equal(
|
|
getTelegramStatusBarProcessingStatus({
|
|
hasActiveTurn: false,
|
|
hasPendingDispatch: true,
|
|
hasPendingModelSwitch: false,
|
|
activeToolExecutions: 0,
|
|
queuedItems: 1,
|
|
}),
|
|
"dispatching",
|
|
);
|
|
assert.equal(
|
|
getTelegramStatusBarProcessingStatus({
|
|
hasActiveTurn: false,
|
|
hasPendingDispatch: false,
|
|
hasPendingModelSwitch: false,
|
|
activeToolExecutions: 0,
|
|
queuedItems: 1,
|
|
}),
|
|
"queued",
|
|
);
|
|
});
|
|
|
|
test("Bridge status runtime stays active while tools run after queue changes", () => {
|
|
const events: string[] = [];
|
|
const runtime = createTelegramBridgeStatusRuntime({
|
|
getConfig: () => ({
|
|
botToken: "token",
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 7,
|
|
}),
|
|
isPollingActive: () => true,
|
|
getActiveSourceMessageIds: () => undefined,
|
|
hasActiveTurn: () => false,
|
|
hasDispatchPending: () => false,
|
|
isCompactionInProgress: () => false,
|
|
getActiveToolExecutions: () => 1,
|
|
hasPendingModelSwitch: () => false,
|
|
getQueuedItems: () => [],
|
|
formatQueuedStatus: () => "",
|
|
getRecentRuntimeEvents: () => [],
|
|
});
|
|
runtime.updateStatus({
|
|
ui: {
|
|
theme: {
|
|
fg: (token: string, text: string) => `<${token}>${text}</${token}>`,
|
|
},
|
|
setStatus: (key: string, text: string) => {
|
|
events.push(`${key}:${text}`);
|
|
},
|
|
},
|
|
});
|
|
assert.match(
|
|
events[0],
|
|
/^telegram:<accent>telegram<\/accent> <warning>\S+ active<\/warning>/,
|
|
);
|
|
});
|
|
|
|
test("Bridge status runtime builds status state from live ports", () => {
|
|
const events: string[] = [];
|
|
const runtime = createTelegramBridgeStatusRuntime({
|
|
getConfig: () => ({
|
|
botToken: "token",
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 7,
|
|
lastUpdateId: 99,
|
|
}),
|
|
isPollingActive: () => true,
|
|
getActiveSourceMessageIds: () => [1, 2],
|
|
hasActiveTurn: () => false,
|
|
hasDispatchPending: () => true,
|
|
isCompactionInProgress: () => false,
|
|
getActiveToolExecutions: () => 3,
|
|
hasPendingModelSwitch: () => true,
|
|
getQueuedItems: () => [{ queueLane: "control" as const }],
|
|
formatQueuedStatus: () => " +1",
|
|
getRecentRuntimeEvents: () => [
|
|
{ at: 1000, category: "api", message: "ok" },
|
|
],
|
|
getRuntimeLockState: () => "active here",
|
|
});
|
|
runtime.updateStatus({
|
|
ui: {
|
|
theme: {
|
|
fg: (token: string, text: string) => `<${token}>${text}</${token}>`,
|
|
},
|
|
setStatus: (key: string, text: string) => {
|
|
events.push(`${key}:${text}`);
|
|
},
|
|
},
|
|
});
|
|
assert.match(
|
|
events[0],
|
|
/^telegram:<accent>telegram<\/accent> <warning>\S+ active<\/warning><success> \+1<\/success>/,
|
|
);
|
|
assert.deepEqual(runtime.getStatusLines(), [
|
|
"connection:",
|
|
"- bot: @demo_bot",
|
|
"- user: 7",
|
|
"- owner: active here",
|
|
"",
|
|
"health:",
|
|
"- polling: running",
|
|
"- state: pending dispatch",
|
|
"- queued turns: 1 (control=1, priority=0, default=0)",
|
|
"- active tools: 3",
|
|
"- pending model switch: yes",
|
|
"",
|
|
"diagnostics:",
|
|
"- state: ~/.pi/agent/tmp/telegram/state.json",
|
|
"- logs: ~/.pi/agent/tmp/telegram/logs.jsonl",
|
|
"- full dump: /telegram-status --debug",
|
|
]);
|
|
});
|
|
|
|
test("Bridge status lines render named-profile diagnostic paths", () => {
|
|
const lines = buildTelegramBridgeStatusLines({
|
|
activeProfileName: "work",
|
|
botUsername: "work_bot",
|
|
pollingActive: false,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
recentRuntimeEvents: [],
|
|
});
|
|
assert.ok(
|
|
lines.includes("- state: ~/.pi/agent/tmp/telegram/state.work.json"),
|
|
);
|
|
assert.ok(
|
|
lines.includes("- logs: ~/.pi/agent/tmp/telegram/logs.work.jsonl"),
|
|
);
|
|
});
|
|
|
|
test("Bridge status lines distinguish unknown bot identity from missing config", () => {
|
|
const base = {
|
|
allowedUserId: 42,
|
|
pollingActive: true,
|
|
lastUpdateId: 100,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
recentRuntimeEvents: [],
|
|
};
|
|
assert.equal(
|
|
buildTelegramBridgeStatusLines({ ...base, hasBotToken: true })[1],
|
|
"- bot: unknown",
|
|
);
|
|
assert.equal(
|
|
buildTelegramBridgeStatusLines({ ...base, hasBotToken: false })[1],
|
|
"- bot: not configured",
|
|
);
|
|
});
|
|
|
|
test("Bridge status lines include role and instance thread name", () => {
|
|
const lines = buildTelegramBridgeStatusLines({
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 42,
|
|
busRole: "leader",
|
|
instanceSlot: "A",
|
|
instanceThreadName: "A-identity",
|
|
pollingActive: true,
|
|
lastUpdateId: 100,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
recentRuntimeEvents: [],
|
|
});
|
|
|
|
assert.deepEqual(lines.slice(0, 5), [
|
|
"connection:",
|
|
"- bot: @demo_bot",
|
|
"- user: 42",
|
|
"- role: leader",
|
|
"- instance: A-identity",
|
|
]);
|
|
});
|
|
|
|
test("Bridge status lines include sync slice diagnostics", () => {
|
|
const lines = buildTelegramBridgeStatusLines(
|
|
{
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 42,
|
|
pollingActive: true,
|
|
lastUpdateId: 100,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
syncState: {
|
|
"topic-state": {
|
|
status: "fresh",
|
|
updatedAtMs: 2000,
|
|
lastReconcileAction: "topic-lifecycle",
|
|
},
|
|
"transport-health": {
|
|
status: "suspect",
|
|
suspectAtMs: 3000,
|
|
reason: "rate limited",
|
|
},
|
|
},
|
|
recentRuntimeEvents: [],
|
|
},
|
|
{ verbose: true },
|
|
);
|
|
assert.ok(lines.includes("sync:"));
|
|
assert.ok(lines.includes("- topic-state: fresh reconcile=topic-lifecycle"));
|
|
assert.ok(lines.includes("- transport-health: suspect reason=rate limited"));
|
|
});
|
|
|
|
test("Bridge status lines include bot thread capability diagnostics", () => {
|
|
const lines = buildTelegramBridgeStatusLines(
|
|
{
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 42,
|
|
botThreadMode: "disabled",
|
|
botThreadModeAction: "thread-mode-unavailable",
|
|
pollingActive: true,
|
|
lastUpdateId: 100,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
recentRuntimeEvents: [],
|
|
},
|
|
{ verbose: true },
|
|
);
|
|
assert.ok(
|
|
lines.includes("- thread mode: disabled reconcile=thread-mode-unavailable"),
|
|
);
|
|
});
|
|
|
|
test("Bridge status lines include topic binding diagnostics", () => {
|
|
const lines = buildTelegramBridgeStatusLines(
|
|
{
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 42,
|
|
pollingActive: true,
|
|
lastUpdateId: 100,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
topicTargets: [
|
|
{
|
|
instanceId: "inst-a",
|
|
status: "active",
|
|
target: { chatId: 42, threadId: 10 },
|
|
slot: "B",
|
|
threadName: "Beacon",
|
|
syncStatus: "open",
|
|
lastSyncObservedAtMs: 1000,
|
|
lastSyncProbeAtMs: 2000,
|
|
lastReconcileAction: "leader-startup-probe",
|
|
},
|
|
{
|
|
instanceId: "inst-a",
|
|
status: "starting",
|
|
target: { chatId: 42, threadId: 11 },
|
|
slot: "C",
|
|
threadName: "Cedar",
|
|
},
|
|
{ instanceId: "inst-b", status: "offline", slot: "D" },
|
|
],
|
|
threadReservations: [
|
|
{
|
|
instanceId: "old-leader",
|
|
target: { chatId: 42, threadId: 9 },
|
|
slot: "A",
|
|
reason: "previous-process-still-probes-alive",
|
|
lastReconcileAction: "leader-topic-previous-instance-still-live",
|
|
},
|
|
],
|
|
topicSyncObservations: [
|
|
{
|
|
instanceId: "closed-inst",
|
|
target: { chatId: 42, threadId: 8 },
|
|
slot: "D",
|
|
syncStatus: "closed",
|
|
observedAtMs: 3000,
|
|
lastReconcileAction: "mark-stale",
|
|
},
|
|
],
|
|
recentRuntimeEvents: [],
|
|
},
|
|
{ verbose: true },
|
|
);
|
|
assert.deepEqual(lines.slice(18, 21), [
|
|
"topics:",
|
|
"- active bindings: instances=1, targets=2",
|
|
"- duplicate inst-a: 2 active threads Beacon target 42:10, Cedar target 42:11",
|
|
]);
|
|
assert.ok(
|
|
lines.includes(
|
|
"- Beacon target 42:10 sync=open observed=1970-01-01T00:00:01.000Z probed=1970-01-01T00:00:02.000Z reconcile=leader-startup-probe",
|
|
),
|
|
);
|
|
assert.ok(lines.includes("- Cedar target 42:11"));
|
|
assert.ok(
|
|
lines.includes(
|
|
"- reservation [A] target 42:9 reason=previous-process-still-probes-alive instance=old-leader reconcile=leader-topic-previous-instance-still-live",
|
|
),
|
|
);
|
|
assert.ok(
|
|
lines.includes(
|
|
"- sync [D] target 42:8 sync=closed observed=1970-01-01T00:00:03.000Z instance=closed-inst reconcile=mark-stale",
|
|
),
|
|
);
|
|
});
|
|
|
|
test("Bridge status lines include bus follower diagnostics when present", () => {
|
|
const lines = buildTelegramBridgeStatusLines(
|
|
{
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 42,
|
|
pollingActive: true,
|
|
lastUpdateId: 100,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
busNowMs: 20_000,
|
|
busFollowers: [
|
|
{
|
|
instanceId: "inst-a",
|
|
cwd: "/repo/a",
|
|
lastHeartbeatMs: 18_600,
|
|
target: { chatId: -1007, threadId: 42 },
|
|
threadName: "Ember",
|
|
},
|
|
{ instanceId: "inst-b", lastHeartbeatMs: 12_000 },
|
|
],
|
|
recentRuntimeEvents: [],
|
|
},
|
|
{ verbose: true },
|
|
);
|
|
assert.deepEqual(lines.slice(19, 24), [
|
|
"bus:",
|
|
"- followers: 2",
|
|
"- inst-a: Ember heartbeat 1s ago target -1007:42 /repo/a",
|
|
"- inst-b: heartbeat 8s ago",
|
|
"",
|
|
]);
|
|
});
|
|
|
|
test("Bridge status lines include local bus diagnostics", () => {
|
|
const lines = buildTelegramBridgeStatusLines(
|
|
{
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 42,
|
|
pollingActive: false,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
localBus: {
|
|
leaderSocketPath: "\\\\.\\pipe\\pi-telegram-demo-bus",
|
|
leaderTransport: "pipe",
|
|
followerSocketPath: "\\\\.\\pipe\\pi-telegram-demo-follower",
|
|
followerTransport: "pipe",
|
|
followerRegistered: true,
|
|
followerTarget: { chatId: 42, threadId: 9 },
|
|
followerThreadName: "Boreal",
|
|
},
|
|
recentRuntimeEvents: [],
|
|
},
|
|
{ verbose: true },
|
|
);
|
|
assert.ok(lines.includes("local bus:"));
|
|
assert.ok(lines.includes("- follower registered: yes Boreal target 42:9"));
|
|
assert.ok(
|
|
lines.includes(
|
|
"- leader endpoint [pipe]: \\\\.\\pipe\\pi-telegram-demo-bus",
|
|
),
|
|
);
|
|
assert.ok(
|
|
lines.includes(
|
|
"- follower endpoint [pipe]: \\\\.\\pipe\\pi-telegram-demo-follower",
|
|
),
|
|
);
|
|
});
|
|
|
|
test("Bridge status lines include queue lanes and recent runtime events", () => {
|
|
const lines = buildTelegramBridgeStatusLines(
|
|
{
|
|
botUsername: "demo_bot",
|
|
allowedUserId: 42,
|
|
pollingActive: true,
|
|
lastUpdateId: 100,
|
|
activeSourceMessageIds: [7, 8],
|
|
pendingDispatch: true,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 2,
|
|
pendingModelSwitch: true,
|
|
queuedItems: [
|
|
{ queueLane: "control" },
|
|
{ queueLane: "priority" },
|
|
{ queueLane: "default" },
|
|
{ queueLane: "default" },
|
|
],
|
|
recentRuntimeEvents: [
|
|
{ at: 1, category: "api:sendMessage", message: "rate limited" },
|
|
],
|
|
},
|
|
{ verbose: true },
|
|
);
|
|
assert.deepEqual(lines, [
|
|
"connection:",
|
|
"- bot: @demo_bot",
|
|
"- allowed user: 42",
|
|
"",
|
|
"polling:",
|
|
"- state: running",
|
|
"- last update id: 100",
|
|
"",
|
|
"execution:",
|
|
"- active turn: 7,8",
|
|
"- pending dispatch: yes",
|
|
"- compaction: idle",
|
|
"- active tools: 2",
|
|
"- pending model switch: yes",
|
|
"",
|
|
"queue:",
|
|
"- queued turns: 4",
|
|
"- lanes: control=1, priority=1, default=2",
|
|
"",
|
|
"recent runtime events:",
|
|
"- summary: api:sendMessage=1",
|
|
"- 1970-01-01T00:00:00.001Z api:sendMessage: rate limited",
|
|
]);
|
|
});
|
|
|
|
test("Status HTML builder binds active model lookup", () => {
|
|
const model = { provider: "openai", id: "gpt-5", contextWindow: 1000 };
|
|
const buildStatusHtml = createTelegramStatusHtmlBuilder({
|
|
getActiveModel: () => model,
|
|
});
|
|
const html = buildStatusHtml({
|
|
sessionManager: { getEntries: () => [] },
|
|
getContextUsage: () => ({ percent: 0, contextWindow: undefined }),
|
|
isIdle: () => true,
|
|
modelRegistry: { isUsingOAuth: () => false },
|
|
});
|
|
assert.match(html, /Status.*idle/s);
|
|
assert.match(html, /Context.*0\.0%\/1\.0k/s);
|
|
});
|
|
|
|
test("Status HTML builder appends Threaded Mode bus role to status row", () => {
|
|
const buildStatusHtml = createTelegramStatusHtmlBuilder({
|
|
getActiveModel: () => undefined,
|
|
getBridgeStatusLineState: () => ({
|
|
hasBotToken: true,
|
|
botThreadMode: "enabled",
|
|
busRole: "leader",
|
|
instanceThreadName: "Dune",
|
|
pollingActive: true,
|
|
pendingDispatch: false,
|
|
compactionInProgress: false,
|
|
activeToolExecutions: 0,
|
|
pendingModelSwitch: false,
|
|
queuedItems: [],
|
|
recentRuntimeEvents: [],
|
|
}),
|
|
});
|
|
const html = buildStatusHtml({
|
|
sessionManager: { getEntries: () => [] },
|
|
getContextUsage: () => ({ percent: 0, contextWindow: 1000 }),
|
|
isIdle: () => true,
|
|
modelRegistry: { isUsingOAuth: () => false },
|
|
});
|
|
assert.match(html, /Status.*idle @leader/s);
|
|
assert.doesNotMatch(html, /Telegram/s);
|
|
});
|
|
|
|
test("Status HTML builder includes extension-provided status lines", () => {
|
|
clearTelegramStatusLineProviders();
|
|
const unregisterCodex = registerTelegramStatusLineProvider(
|
|
({ activeModel }) =>
|
|
activeModel?.contextWindow === 1000
|
|
? { label: "codex", value: "████ 23.7h" }
|
|
: undefined,
|
|
{ id: "@scope/codex" },
|
|
);
|
|
const unregisterBroken = registerTelegramStatusLineProvider(
|
|
() => {
|
|
throw new Error("optional provider failed");
|
|
},
|
|
{ id: "@scope/broken" },
|
|
);
|
|
try {
|
|
const buildStatusHtml = createTelegramStatusHtmlBuilder({
|
|
getActiveModel: () => ({ contextWindow: 1000 }),
|
|
});
|
|
const html = buildStatusHtml({
|
|
sessionManager: { getEntries: () => [] },
|
|
getContextUsage: () => ({ percent: 0, contextWindow: undefined }),
|
|
isIdle: () => true,
|
|
modelRegistry: { isUsingOAuth: () => false },
|
|
});
|
|
assert.match(html, /Context.*0\.0%\/1\.0k/s);
|
|
assert.match(html, /Codex.*████ 23\.7h/s);
|
|
} finally {
|
|
unregisterCodex();
|
|
unregisterBroken();
|
|
clearTelegramStatusLineProviders();
|
|
}
|
|
});
|
|
|
|
test("Status HTML builder leaves compaction lifecycle to Pi", () => {
|
|
const buildStatusHtml = createTelegramStatusHtmlBuilder({
|
|
getActiveModel: () => undefined,
|
|
isCompactionInProgress: () => true,
|
|
});
|
|
const html = buildStatusHtml({
|
|
sessionManager: { getEntries: () => [] },
|
|
getContextUsage: () => ({ percent: 0, contextWindow: 1000 }),
|
|
isIdle: () => true,
|
|
modelRegistry: { isUsingOAuth: () => false },
|
|
});
|
|
assert.match(html, /Status.*idle/s);
|
|
assert.doesNotMatch(html, /compacting/);
|
|
});
|
|
|
|
test("Runtime event lines render the recent-event ring newest first", () => {
|
|
assert.deepEqual(buildTelegramRuntimeEventLines([]), [
|
|
"recent runtime events: none",
|
|
]);
|
|
assert.deepEqual(
|
|
buildTelegramRuntimeEventLines([
|
|
{ at: 0, category: "poll", message: "started" },
|
|
{ at: 1000, category: "api:sendMessage", message: "rate limited" },
|
|
]),
|
|
[
|
|
"recent runtime events:",
|
|
"- summary: api:sendMessage=1, poll=1",
|
|
"- 1970-01-01T00:00:01.000Z api:sendMessage: rate limited",
|
|
"- 1970-01-01T00:00:00.000Z poll: started",
|
|
],
|
|
);
|
|
});
|
|
|
|
test("Structured runtime event recording redacts messages and details", () => {
|
|
const events: TelegramRuntimeEvent[] = [];
|
|
recordStructuredTelegramRuntimeEvent(
|
|
events,
|
|
{
|
|
category: "api",
|
|
error: new Error("token 123:abc failed"),
|
|
details: { method: "sendMessage", token: "123:abc", retryable: true },
|
|
},
|
|
{ botToken: "123:abc", maxEvents: 3, now: 1000 },
|
|
);
|
|
assert.deepEqual(events, [
|
|
{
|
|
at: 1000,
|
|
category: "api",
|
|
message: "token <redacted-token> failed",
|
|
details: {
|
|
method: "sendMessage",
|
|
token: "<redacted-token>",
|
|
retryable: true,
|
|
},
|
|
},
|
|
]);
|
|
assert.deepEqual(buildTelegramRuntimeEventLines(events), [
|
|
"recent runtime events:",
|
|
"- summary: api:sendMessage=1",
|
|
'- 1970-01-01T00:00:01.000Z api:sendMessage: token <redacted-token> failed (token="<redacted-token>", retryable=true)',
|
|
]);
|
|
});
|
|
|
|
test("Runtime event recording bounds messages and string details", () => {
|
|
const events: TelegramRuntimeEvent[] = [];
|
|
recordStructuredTelegramRuntimeEvent(
|
|
events,
|
|
{
|
|
category: "handler",
|
|
error: new Error("x".repeat(1200)),
|
|
details: { output: "y".repeat(1200) },
|
|
},
|
|
{ maxEvents: 3, now: 1000 },
|
|
);
|
|
|
|
assert.equal(events[0]?.message.length, 1023);
|
|
assert.match(events[0]?.message ?? "", /truncated 200 chars/);
|
|
assert.equal(String(events[0]?.details?.output).length, 1023);
|
|
assert.match(String(events[0]?.details?.output), /truncated 200 chars/);
|
|
});
|
|
|
|
test("Runtime event recorder owns redacted bounded event state", () => {
|
|
const recorder = createTelegramRuntimeEventRecorder({
|
|
getBotToken: () => "123:abc",
|
|
maxEvents: 1,
|
|
now: () => 1000,
|
|
});
|
|
recorder.record("api", new Error("token 123:abc failed"), {
|
|
method: "sendMessage",
|
|
});
|
|
recorder.record("poll", "ok");
|
|
assert.deepEqual(recorder.getEvents(), [
|
|
{ at: 1000, category: "poll", message: "ok" },
|
|
]);
|
|
recorder.clear();
|
|
assert.deepEqual(recorder.getEvents(), []);
|
|
});
|
|
|
|
test("Runtime event recording redacts bot tokens and keeps a bounded ring", () => {
|
|
const events: TelegramRuntimeEvent[] = [];
|
|
recordTelegramRuntimeEvent(events, "one", new Error("token 123:abc failed"), {
|
|
botToken: "123:abc",
|
|
maxEvents: 3,
|
|
now: 1,
|
|
});
|
|
assert.deepEqual(events, [
|
|
{ at: 1, category: "one", message: "token <redacted-token> failed" },
|
|
]);
|
|
recordTelegramRuntimeEvent(events, "two", "plain", {
|
|
botToken: "123:abc",
|
|
maxEvents: 3,
|
|
now: 2,
|
|
});
|
|
recordTelegramRuntimeEvent(events, "three", "last", {
|
|
botToken: "123:abc",
|
|
maxEvents: 2,
|
|
now: 3,
|
|
});
|
|
assert.deepEqual(events, [
|
|
{ at: 2, category: "two", message: "plain" },
|
|
{ at: 3, category: "three", message: "last" },
|
|
]);
|
|
});
|