feat: human error states, operator toasts and a queue/origin view in the omp UI

- 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
This commit is contained in:
evandrodevbr committed 2026-10-01 17:33:37 -03:00
1 parent 9435ff709b
commit 057281a3bb
19 files changed
+862 -22

No files matched your search

+52 -10
View File
@@ -1,6 +1,6 @@
/**
* Regression tests for the Telegram traffic counters
* Covers what counts as a message, last-activity tracking, and the /telegram-status line.
* Covers what counts as a message, last-activity tracking, failed deliveries, and the /telegram-status line.
*/
import assert from "node:assert/strict";
@@ -13,17 +13,20 @@ import {
isTelegramOutboundMessageMethod,
} from "../lib/traffic.ts";
test("Traffic counters count both directions and remember the latest activity", () => {
test("Traffic counters count every direction and remember the latest message", () => {
let nowMs = 1_000;
const counters = createTelegramTrafficCounters(() => nowMs);
assert.deepEqual(counters.snapshot(), { received: 0, sent: 0 });
assert.deepEqual(counters.snapshot(), { received: 0, sent: 0, failed: 0 });
counters.recordReceived();
nowMs = 2_000;
counters.recordSent();
counters.recordSent();
nowMs = 3_000;
counters.recordFailed();
assert.deepEqual(counters.snapshot(), {
received: 1,
sent: 2,
failed: 1,
lastAtMs: 2_000,
});
});
@@ -57,13 +60,22 @@ test("Status lines report the message counters and the time of the last one", ()
queuedItems: [],
recentRuntimeEvents: [],
};
const withTraffic = buildTelegramBridgeStatusLines({
const lastAtMs = new Date(2026, 9, 1, 14, 32).getTime();
const clean = buildTelegramBridgeStatusLines({
...baseState,
traffic: { received: 12, sent: 11, lastAtMs: new Date(2026, 9, 1, 14, 32).getTime() },
traffic: { received: 12, sent: 11, failed: 0, lastAtMs },
});
assert.ok(
withTraffic.includes("- messages: 12 received, 11 sent, last 14:32"),
withTraffic.join("\n"),
clean.includes("- messages: 12 received, 11 sent, last 14:32"),
clean.join("\n"),
);
const withFailures = buildTelegramBridgeStatusLines({
...baseState,
traffic: { received: 12, sent: 11, failed: 2, lastAtMs },
});
assert.ok(
withFailures.includes("- messages: 12 received, 11 sent, 2 failed, last 14:32"),
withFailures.join("\n"),
);
const withoutTraffic = buildTelegramBridgeStatusLines(baseState);
assert.equal(withoutTraffic.some((line) => line.startsWith("- messages:")), false);
@@ -83,7 +95,7 @@ test("Traffic bridge counts delivered messages and refreshes the status with the
});
bridge.onCallSucceeded("sendChatAction");
bridge.onCallSucceeded("editMessageText");
assert.deepEqual(counters.snapshot(), { received: 0, sent: 0 });
assert.deepEqual(counters.snapshot(), { received: 0, sent: 0, failed: 0 });
assert.deepEqual(refreshed, []);
bridge.onCallSucceeded("sendRichMessage");
assert.equal(counters.snapshot().sent, 1);
@@ -97,6 +109,33 @@ test("Traffic bridge counts delivered messages and refreshes the status with the
assert.deepEqual(refreshed, ["session-a", "session-b"]);
});
test("Traffic bridge counts undelivered messages, reports them and refreshes the status", () => {
const counters = createTelegramTrafficCounters();
const refreshed: string[] = [];
const failures: string[] = [];
const bridge = createTelegramTrafficStatusBridge({
counters,
getContext: () => "session",
updateStatus: (ctx: string) => {
refreshed.push(ctx);
},
recordRuntimeEvent: () => {},
onDeliveryFailed: (error) => {
failures.push((error as Error).message);
},
});
bridge.onCallFailed("getUpdates", new Error("poll failed"));
bridge.onCallFailed("sendChatAction", new Error("typing failed"));
assert.equal(counters.snapshot().failed, 0);
assert.deepEqual(failures, []);
assert.deepEqual(refreshed, []);
bridge.onCallFailed("sendMessage", new Error("HTTP 500"));
assert.equal(counters.snapshot().failed, 1);
assert.equal(counters.snapshot().sent, 0);
assert.deepEqual(failures, ["HTTP 500"]);
assert.deepEqual(refreshed, ["session"]);
});
test("Traffic bridge records a failed status refresh instead of breaking the message path", () => {
const counters = createTelegramTrafficCounters();
const recorded: string[] = [];
@@ -112,10 +151,13 @@ test("Traffic bridge records a failed status refresh instead of breaking the mes
});
bridge.onCallSucceeded("sendMessage");
bridge.onAuthorizedMessage("session");
assert.deepEqual(counters.snapshot().received, 1);
assert.deepEqual(counters.snapshot().sent, 1);
bridge.onCallFailed("sendMessage", new Error("HTTP 500"));
assert.equal(counters.snapshot().received, 1);
assert.equal(counters.snapshot().sent, 1);
assert.equal(counters.snapshot().failed, 1);
assert.deepEqual(recorded, [
"status:stale ctx:traffic-refresh",
"status:stale ctx:traffic-refresh",
"status:stale ctx:traffic-refresh",
]);
});