- classify transport and API failures into a short state with the next step - count undelivered messages and show them next to the sent counter - toast on owner paired, connection recovered and reply not delivered (throttled) - second status line previews the next queued turns - host spinner names the Telegram prompt while its turn runs - sanitize every Telegram-controlled string that reaches the widget
107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
/**
|
|
* Regression tests for the Telegram operator notices
|
|
* Covers the toast wording, severity, throttling and failure isolation.
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
createTelegramNoticeRuntime,
|
|
TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS,
|
|
} from "../lib/notices.ts";
|
|
|
|
function createNoticeHarness(options: { notifyThrows?: boolean } = {}) {
|
|
const notices: string[] = [];
|
|
const recorded: string[] = [];
|
|
let nowMs = 1_000_000;
|
|
let context: string | undefined = "session";
|
|
const runtime = createTelegramNoticeRuntime({
|
|
getContext: () => context,
|
|
notify: (ctx: string, message, level) => {
|
|
if (options.notifyThrows) throw new Error("stale ctx");
|
|
notices.push(`${level}|${ctx}|${message}`);
|
|
},
|
|
recordRuntimeEvent: (category, error, details) => {
|
|
recorded.push(`${category}:${(error as Error).message}:${details?.phase}`);
|
|
},
|
|
now: () => nowMs,
|
|
});
|
|
return {
|
|
runtime,
|
|
notices,
|
|
recorded,
|
|
advance: (ms: number) => {
|
|
nowMs += ms;
|
|
},
|
|
dropContext: () => {
|
|
context = undefined;
|
|
},
|
|
};
|
|
}
|
|
|
|
test("Notices announce pairing and recovery as info toasts", () => {
|
|
const { runtime, notices } = createNoticeHarness();
|
|
runtime.paired(777, "ctx-a");
|
|
runtime.recovered("ctx-b");
|
|
assert.deepEqual(notices, [
|
|
"info|ctx-a|Telegram paired with user 777",
|
|
"info|ctx-b|Telegram reconnected",
|
|
]);
|
|
});
|
|
|
|
test("Delivery failures explain the reason and pick a severity from it", () => {
|
|
const harness = createNoticeHarness();
|
|
harness.runtime.deliveryFailed(
|
|
new Error("Telegram API sendMessage failed: HTTP 429: Too Many Requests: retry after 12"),
|
|
);
|
|
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
|
|
harness.runtime.deliveryFailed(new Error("Unable to connect. Is the computer able to access the url?"));
|
|
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
|
|
harness.runtime.deliveryFailed(new Error("Telegram API sendMessage failed: HTTP 401: Unauthorized"));
|
|
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
|
|
harness.runtime.deliveryFailed(new Error("message is too long"));
|
|
assert.deepEqual(harness.notices, [
|
|
"warning|session|Telegram: reply not delivered (rate limited 12s)",
|
|
"warning|session|Telegram: reply not delivered (offline)",
|
|
"error|session|Telegram: reply not delivered (invalid token)",
|
|
"error|session|Telegram: reply not delivered (message is too long)",
|
|
]);
|
|
});
|
|
|
|
test("Delivery failure notices are throttled and need a live context", () => {
|
|
const harness = createNoticeHarness();
|
|
harness.runtime.deliveryFailed(new Error("fetch failed"));
|
|
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS - 1);
|
|
harness.runtime.deliveryFailed(new Error("fetch failed"));
|
|
assert.equal(harness.notices.length, 1);
|
|
harness.advance(1);
|
|
harness.runtime.deliveryFailed(new Error("fetch failed"));
|
|
assert.equal(harness.notices.length, 2);
|
|
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
|
|
harness.dropContext();
|
|
harness.runtime.deliveryFailed(new Error("fetch failed"));
|
|
assert.equal(harness.notices.length, 2);
|
|
});
|
|
|
|
test("A failing toast is recorded and never reaches the caller", () => {
|
|
const harness = createNoticeHarness({ notifyThrows: true });
|
|
assert.doesNotThrow(() => {
|
|
harness.runtime.paired(777, "ctx");
|
|
harness.runtime.recovered("ctx");
|
|
harness.runtime.deliveryFailed(new Error("fetch failed"));
|
|
});
|
|
assert.deepEqual(harness.recorded, [
|
|
"status:stale ctx:notice",
|
|
"status:stale ctx:notice",
|
|
"status:stale ctx:notice",
|
|
]);
|
|
});
|
|
|
|
test("Delivery failure notices carry no control sequences from raw error text", () => {
|
|
const harness = createNoticeHarness();
|
|
harness.runtime.deliveryFailed(new Error("bad\u001b]0;pwned\u0007\u001b[31m text"));
|
|
assert.equal(harness.notices.length, 1);
|
|
assert.doesNotMatch(harness.notices[0], /[\u0000-\u001f\u007f-\u009f]/);
|
|
});
|