wip: snapshot das alterações não commitadas (2026-10-20)
Validate / validate (push) Failing after 4m58s

Origem: D:/Documents/Github/oh-my-pi-telegram
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 0
This commit is contained in:
evandro committed 2026-10-20 00:49:42 -03:00
1 parent a56408711e
commit 97f2b723bb
5 files changed
+916

No files matched your search

@@ -0,0 +1,404 @@
# Telegram OMP 17 Status Indicator Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Show the Telegram bridge's connection state in OMP 17's upper status bar immediately after the cost segment, while documenting the correct OMP plugin workflow.
**Architecture:** OMP gains one narrow public extension UI method for a sanitized status segment fixed after `cost`. The Telegram plugin calls that method from its existing status update path, keeping connection-state derivation local and network-free. A checkout of OMP is modified and built; the Bun-global package is never patched in place.
**Tech Stack:** TypeScript, Bun, OMP 17 status-line component, OMP extension API, Node test runner, Telegram bridge extension.
---
## File structure
### OMP source checkout: `D:/Documents/Github/oh-my-pi`
- `packages/coding-agent/src/extensibility/extensions/types.ts` — public extension UI type.
- `packages/coding-agent/src/modes/controllers/extension-ui-controller.ts` — forwards the public API to the status-line component.
- `packages/coding-agent/src/modes/components/status-line/component.ts` — owns segment storage, sanitization, insertion after `cost`, and width eviction.
- `packages/coding-agent/src/modes/components/status-line/component.test.ts` — verifies public-status segment rendering.
### Telegram plugin: `D:/Documents/Github/oh-my-pi-telegram`
- `lib/pi.ts` — narrow structural adapter for OMP's optional public top-status API.
- `lib/status.ts` — derives the two Telegram status labels and publishes through injected ports.
- `index.ts` — wires the OMP status-segment port into the existing status runtime.
- `tests/status.test.ts` — verifies connection-state labels and publication calls.
- `tests/public-api.test.ts` — raises the declared OMP compatibility floor to 17.0.0.
- `package.json` — raises `omp.compatibleWith["@oh-my-pi/pi-coding-agent"]` to `>=17.0.0`.
- `README.md` — correct OMP plugin install/link/setup instructions.
## Task 1: Create a durable OMP 17 source checkout
**Files:**
- Create: `D:/Documents/Github/oh-my-pi`
- [ ] **Step 1: Clone and install the OMP 17 source tree**
```powershell
git clone https://github.com/can1357/oh-my-pi.git D:/Documents/Github/oh-my-pi
cd D:/Documents/Github/oh-my-pi
git switch --create feature/telegram-status-segment
bun install
```
- [ ] **Step 2: Confirm the checkout matches the installed OMP major version**
Run:
```powershell
bun --cwd packages/coding-agent -e "console.log(require('./package.json').version)"
```
Expected: a `17.x.x` version. If the upstream checkout is newer, switch to the `v17.0.0` tag before editing so the source API matches the installed OMP.
- [ ] **Step 3: Commit no code in this task**
The checkout is preparation only. The first code commit is made after the API and its test pass together.
## Task 2: Add OMP's public after-cost extension segment API
**Files:**
- Modify: `D:/Documents/Github/oh-my-pi/packages/coding-agent/src/extensibility/extensions/types.ts`
- Modify: `D:/Documents/Github/oh-my-pi/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts`
- Modify: `D:/Documents/Github/oh-my-pi/packages/coding-agent/src/modes/components/status-line/component.ts`
- Test: `D:/Documents/Github/oh-my-pi/packages/coding-agent/src/modes/components/status-line/component.test.ts`
- [ ] **Step 1: Write failing status-line component tests**
Add tests that construct `StatusLineComponent`, register two extension segments, and inspect `getTopBorder(200).content` after stripping SGR codes. The assertions must prove:
```ts
expect(stripped.indexOf("$2.50")).toBeLessThan(stripped.indexOf("Telegram: conectado"));
expect(stripped.indexOf("Telegram: conectado")).toBeLessThan(stripped.indexOf("session-name"));
expect(stripped).not.toContain("\\x1b[31m");
```
Set the test session's usage statistics to `cost: 2.5` and its session name to `session-name`. Register a segment with `"Telegram: conectado"`; register another containing `"\x1b[31munsafe\x1b[0m"`; then clear the first segment and assert that only the cleared value disappears.
- [ ] **Step 2: Run the component test and observe the missing API failure**
Run:
```powershell
bun test packages/coding-agent/src/modes/components/status-line/component.test.ts
```
Expected: FAIL because `StatusLineComponent` has no extension-status-segment method.
- [ ] **Step 3: Define the public UI type**
In `ExtensionUIContext`, add the narrow method and option type:
```ts
export type ExtensionStatusSegmentPlacement = "after-cost";
export interface ExtensionStatusSegmentOptions {
placement: ExtensionStatusSegmentPlacement;
}
setStatusSegment(
key: string,
text: string | undefined,
options: ExtensionStatusSegmentOptions,
): void;
```
Only `"after-cost"` is accepted. Do not expose component references, arbitrary segment IDs, callbacks, or styling controls.
- [ ] **Step 4: Forward the API through the interactive extension UI controller**
Add to `uiContext` in `ExtensionUiController.initHooksAndCustomTools()`:
```ts
setStatusSegment: (key, text, options) => {
if (options.placement !== "after-cost") return;
this.ctx.statusLine.setExtensionStatusSegment(key, text);
this.ctx.ui.requestRender();
},
```
Add no-op implementations for non-interactive mock UI contexts in `extensibility/utils.ts`, `extensibility/extensions/runner.ts`, `modes/acp/acp-agent.ts`, and `session/agent-session.ts`, matching the existing `setStatus` convention.
- [ ] **Step 5: Store and render sanitized extension segments in the status-line component**
Add a private `Map<string, string>` and method:
```ts
setExtensionStatusSegment(key: string, text: string | undefined): void {
if (text === undefined) this.#extensionStatusSegments.delete(key);
else this.#extensionStatusSegments.set(key, sanitizeStatusText(text));
}
```
In `#buildStatusLine`, retain IDs for both rendered groups. After collecting built-in segments, insert the map values sorted by key immediately after the rendered `cost` entry in whichever group contains it. If `cost` is not visible, omit extension segments; their contract is explicitly after visible cost. Include synthetic IDs in the width bookkeeping so the normal right-to-left overflow policy can evict them without breaking `leftSegIds` alignment.
- [ ] **Step 6: Run the focused test to verify behavior**
Run:
```powershell
bun test packages/coding-agent/src/modes/components/status-line/component.test.ts
```
Expected: PASS. It proves deterministic ordering, sanitization, and clearing.
- [ ] **Step 7: Type-check and commit the OMP API**
Run:
```powershell
bun --cwd packages/coding-agent run check:types
git diff --check
git add packages/coding-agent/src/extensibility/extensions/types.ts packages/coding-agent/src/extensibility/utils.ts packages/coding-agent/src/extensibility/extensions/runner.ts packages/coding-agent/src/modes/acp/acp-agent.ts packages/coding-agent/src/modes/components/status-line/component.ts packages/coding-agent/src/modes/components/status-line/component.test.ts packages/coding-agent/src/modes/controllers/extension-ui-controller.ts packages/coding-agent/src/session/agent-session.ts
git commit -m "feat: expose after-cost extension status segments"
```
Expected: type-check exits 0 and the commit contains only the API, wiring, component, and test.
## Task 3: Publish Telegram bridge state through the new API
**Files:**
- Modify: `D:/Documents/Github/oh-my-pi-telegram/lib/pi.ts`
- Modify: `D:/Documents/Github/oh-my-pi-telegram/lib/status.ts`
- Modify: `D:/Documents/Github/oh-my-pi-telegram/index.ts`
- Test: `D:/Documents/Github/oh-my-pi-telegram/tests/status.test.ts`
- [ ] **Step 1: Write failing plugin status tests**
Add pure tests for a new helper with these observable contracts:
```ts
assert.equal(getTelegramTopStatusSegment({ pollingActive: true }), "Telegram: conectado");
assert.equal(getTelegramTopStatusSegment({ pollingActive: false }), "Telegram: desconectado");
```
Add an injected `setStatusSegment` spy and call `runtime.updateStatus(ctx)` with a context whose `ui.setStatus` is a no-op. Assert that the captured publication is:
```ts
["telegram", "Telegram: conectado", { placement: "after-cost" }]
```
when polling becomes active, and the disconnected text when it becomes inactive.
- [ ] **Step 2: Run the focused status test and observe the missing helper failure**
Run:
```powershell
node --experimental-strip-types --test tests/status.test.ts
```
Expected: FAIL because `getTelegramTopStatusSegment` is not exported.
- [ ] **Step 3: Add a narrow OMP context capability adapter**
In `lib/pi.ts`, define structural ports without importing OMP internal modules:
```ts
export interface PiTopStatusSegmentPorts {
setStatusSegment?: (
key: string,
text: string | undefined,
options: { placement: "after-cost" },
) => void;
}
export function setExtensionContextTopStatusSegment(
ctx: ExtensionContext,
key: string,
text: string,
): void {
const ui = ctx.ui as ExtensionContext["ui"] & PiTopStatusSegmentPorts;
ui.setStatusSegment?.(key, text, { placement: "after-cost" });
}
```
The optional capability keeps passive modes and stale local SDK declarations from throwing. The package compatibility floor still makes OMP 17 the supported runtime.
- [ ] **Step 4: Publish the two-state bridge label in `lib/status.ts`**
Add:
```ts
export function getTelegramTopStatusSegment(state: Pick<TelegramStatusBarState, "pollingActive">): string {
return state.pollingActive ? "Telegram: conectado" : "Telegram: desconectado";
}
```
Extend `TelegramStatusRuntimeDeps` with optional:
```ts
setStatusSegment?: (ctx: TContext, key: string, text: string) => void;
```
In `updateStatus`, compute the bridge state once, retain the existing footer `setStatus` call, and then call:
```ts
deps.setStatusSegment?.(ctx, "telegram", getTelegramTopStatusSegment(state));
```
The status runtime is already called from connect, disconnect, polling, lock, and session updates, so no additional event listeners are added.
- [ ] **Step 5: Wire the context adapter from `index.ts`**
Pass `Pi.setExtensionContextTopStatusSegment` as `setStatusSegment` into `createTelegramBridgeStatusRuntime`. Do not call the OMP API directly outside the `lib/pi.ts` adapter boundary.
- [ ] **Step 6: Run focused tests and type-check**
Run:
```powershell
node --experimental-strip-types --test tests/status.test.ts
npm run typecheck
```
Expected: all focused status tests pass and TypeScript exits 0.
- [ ] **Step 7: Commit the plugin integration**
```powershell
git diff --check
git add lib/pi.ts lib/status.ts index.ts tests/status.test.ts
git commit -m "feat: publish Telegram bridge status in OMP"
```
## Task 4: Correct OMP installation and secure setup documentation
**Files:**
- Modify: `D:/Documents/Github/oh-my-pi-telegram/README.md`
- Modify: `D:/Documents/Github/oh-my-pi-telegram/package.json`
- Modify: `D:/Documents/Github/oh-my-pi-telegram/tests/public-api.test.ts`
- [ ] **Step 1: Write a failing manifest test for the OMP 17 floor**
Change the existing test expectation to:
```ts
assert.equal(
packageJson.omp?.compatibleWith?.["@oh-my-pi/pi-coding-agent"],
">=17.0.0",
);
```
- [ ] **Step 2: Run the manifest test and observe the floor mismatch**
Run:
```powershell
node --experimental-strip-types --test tests/public-api.test.ts
```
Expected: FAIL because the manifest still declares `>=16.5.2`.
- [ ] **Step 3: Update the manifest and README**
Set the OMP compatibility floor in `package.json` to `>=17.0.0`.
Replace the incorrect OMP install block with:
```bash
omp plugin install npm:@evandrodevbr/oh-my-pi-telegram
omp plugin install git:github.com/evandrodevbr/oh-my-pi-telegram
omp plugin link /path/to/oh-my-pi-telegram
```
Document restarting OMP after installation/linking. Keep bot-token setup limited to the interactive commands:
```text
/telegram-setup
/telegram-connect
/telegram-disconnect
/telegram-status
```
Explicitly state that bot tokens must not be committed or pasted into chat.
- [ ] **Step 4: Run documentation-adjacent verification**
Run:
```powershell
node --experimental-strip-types --test tests/public-api.test.ts
npm run typecheck
npm run audit
npm run pack:check
```
Expected: all commands exit 0.
- [ ] **Step 5: Commit documentation and compatibility metadata**
```powershell
git diff --check
git add README.md package.json tests/public-api.test.ts
git commit -m "docs: document OMP 17 Telegram setup"
```
## Task 5: Build, install, configure, and smoke-test the integrated runtime
**Files:**
- Modify: no source files expected.
- [ ] **Step 1: Build and link the versioned OMP checkout**
From `D:/Documents/Github/oh-my-pi`:
```powershell
bun --cwd packages/coding-agent run check:types
bun --cwd packages/coding-agent run build
bun --cwd packages/coding-agent link
omp --version
```
Expected: the linked `omp` reports the built 17.x version. Do not edit or link files under the Bun global package directory directly.
- [ ] **Step 2: Link the updated Telegram plugin**
From `D:/Documents/Github/oh-my-pi-telegram`:
```powershell
omp plugin link .
omp plugin doctor
```
Expected: the doctor lists `@evandrodevbr/oh-my-pi-telegram` with zero errors.
- [ ] **Step 3: Configure the replacement token locally**
Start `omp` in the user's own terminal and run:
```text
/telegram-setup
```
Paste the new bot token only into OMP's local prompt. Then run:
```text
/telegram-connect
```
Expected: polling starts and the upper status bar contains `Telegram: conectado` after `$… (sub)`.
- [ ] **Step 4: Exercise disconnect and command behavior**
In the same OMP session, run:
```text
/telegram-status
/telegram-disconnect
```
Expected: `/telegram-status` returns bridge diagnostics and the upper status bar changes to `Telegram: desconectado` after cost.
- [ ] **Step 5: Commit and push both repositories**
From each repository after its focused tests pass:
```powershell
git status --short
git push origin HEAD
```
Expected: both commits are on their respective remote branches. Do not push a bot token, generated build output, Bun-global installation files, or `.env` files.
+104
View File
@@ -0,0 +1,104 @@
/**
* Telegram operator notices
* Zones: telegram ui, tui
* Owns the one-off host toasts (owner paired, connection recovered, reply not delivered) and their throttling
*/
import {
sanitizeTelegramStatusText,
summarizeTelegramStatusError,
} from "./status.ts";
export type TelegramNoticeLevel = "info" | "warning" | "error";
/** A failed delivery raises at most one notice per this window. */
export const TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS = 30_000;
export interface TelegramNoticeRuntimeDeps<TContext> {
/** Current session context, when the bridge has one. */
getContext: () => TContext | undefined;
notify: (ctx: TContext, message: string, level: TelegramNoticeLevel) => void;
recordRuntimeEvent: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
now?: () => number;
}
export interface TelegramNoticeRuntime<TContext> {
/** The first Telegram user was accepted as the bridge owner. */
paired: (userId: number, ctx: TContext) => void;
/** Polling works again after at least one failed attempt. */
recovered: (ctx: TContext) => void;
/** A message could not be delivered; throttled, uses the current context. */
deliveryFailed: (error: unknown) => void;
}
function describeTelegramDeliveryFailure(error: unknown): {
message: string;
level: TelegramNoticeLevel;
} {
const message = error instanceof Error ? error.message : String(error);
const summary = summarizeTelegramStatusError(message);
if (summary?.deliveryUnknown) {
return {
message: "Telegram: reply delivery unconfirmed (check the chat)",
level: "warning",
};
}
if (summary) {
return {
message: `Telegram: reply not delivered (${summary.label})`,
level: summary.kind === "fatal" ? "error" : "warning",
};
}
const raw = sanitizeTelegramStatusText(message);
const reason = raw.length > 80 ? `${raw.slice(0, 79)}…` : raw;
return { message: `Telegram: reply not delivered (${reason})`, level: "error" };
}
/**
* Toasts never propagate failures into the path that raised them: a stale
* session context only produces a recorded runtime event.
*/
export function createTelegramNoticeRuntime<TContext>(
deps: TelegramNoticeRuntimeDeps<TContext>,
): TelegramNoticeRuntime<TContext> {
const now = deps.now ?? Date.now;
let lastDeliveryNoticeAtMs: number | undefined;
const show = (
ctx: TContext,
message: string,
level: TelegramNoticeLevel,
): void => {
try {
deps.notify(ctx, message, level);
} catch (error) {
deps.recordRuntimeEvent("status", error, { phase: "notice" });
}
};
return {
paired(userId, ctx) {
show(ctx, `Telegram paired with user ${userId}`, "info");
},
recovered(ctx) {
show(ctx, "Telegram reconnected", "info");
},
deliveryFailed(error) {
const ctx = deps.getContext();
if (ctx === undefined) return;
const nowMs = now();
if (
lastDeliveryNoticeAtMs !== undefined &&
nowMs - lastDeliveryNoticeAtMs <
TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS
) {
return;
}
lastDeliveryNoticeAtMs = nowMs;
const failure = describeTelegramDeliveryFailure(error);
show(ctx, failure.message, failure.level);
},
};
}
+134
View File
@@ -0,0 +1,134 @@
/**
* Telegram bridge traffic counters
* Zones: telegram transport, status
* Owns in-memory received/sent message counts and last-activity time for the status bar
*
* This domain is pure: it counts, it does not read config, call Telegram, or import other domains.
*/
export interface TelegramTrafficSnapshot {
/** Authorized messages received from the paired owner since the bridge loaded. */
received: number;
/** Messages the bridge delivered to Telegram since the bridge loaded. */
sent: number;
/** Messages that could not be delivered after the transport retries. */
failed: number;
/** Time of the latest received or sent message, when there was one. */
lastAtMs?: number;
}
export interface TelegramTrafficCounters {
recordReceived: () => void;
recordSent: () => void;
recordFailed: () => void;
snapshot: () => TelegramTrafficSnapshot;
}
/**
* Bot API methods that deliver a message the owner can read. Typing actions,
* drafts, edits, reactions and callback answers are not messages.
*/
const TELEGRAM_OUTBOUND_MESSAGE_METHODS: Readonly<Record<string, true>> = {
sendMessage: true,
sendRichMessage: true,
sendPhoto: true,
sendDocument: true,
sendAudio: true,
sendVideo: true,
sendVoice: true,
sendAnimation: true,
sendVideoNote: true,
sendMediaGroup: true,
sendSticker: true,
};
export function isTelegramOutboundMessageMethod(method: string): boolean {
return Object.hasOwn(TELEGRAM_OUTBOUND_MESSAGE_METHODS, method);
}
export function createTelegramTrafficCounters(
now: () => number = Date.now,
): TelegramTrafficCounters {
let received = 0;
let sent = 0;
let failed = 0;
let lastAtMs: number | undefined;
return {
recordReceived() {
received += 1;
lastAtMs = now();
},
recordSent() {
sent += 1;
lastAtMs = now();
},
recordFailed() {
failed += 1;
},
snapshot: () => ({
received,
sent,
failed,
...(lastAtMs === undefined ? {} : { lastAtMs }),
}),
};
}
export interface TelegramTrafficStatusBridgeDeps<TContext> {
counters: TelegramTrafficCounters;
/** Current session context, when the bridge has one. */
getContext: () => TContext | undefined;
updateStatus: (ctx: TContext) => void;
recordRuntimeEvent: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
/** Told about every message delivery that failed, e.g. to raise a notice. */
onDeliveryFailed?: (error: unknown) => void;
}
export interface TelegramTrafficStatusBridge<TContext> {
/** Observer for successful Bot API calls; counts delivered messages. */
onCallSucceeded: (method: string) => void;
/** Observer for failed Bot API calls; counts undelivered messages. */
onCallFailed: (method: string, error: unknown) => void;
/** Observer for messages accepted from the paired owner. */
onAuthorizedMessage: (ctx: TContext) => void;
}
/**
* Counts traffic and refreshes the status bar so the counters stay current. A
* status refresh failure (for example a stale session context) is recorded and
* never propagates into the send or receive path that triggered it.
*/
export function createTelegramTrafficStatusBridge<TContext>(
deps: TelegramTrafficStatusBridgeDeps<TContext>,
): TelegramTrafficStatusBridge<TContext> {
const refreshStatus = (ctx: TContext): void => {
try {
deps.updateStatus(ctx);
} catch (error) {
deps.recordRuntimeEvent("status", error, { phase: "traffic-refresh" });
}
};
return {
onCallSucceeded(method) {
if (!isTelegramOutboundMessageMethod(method)) return;
deps.counters.recordSent();
const ctx = deps.getContext();
if (ctx !== undefined) refreshStatus(ctx);
},
onCallFailed(method, error) {
if (!isTelegramOutboundMessageMethod(method)) return;
deps.counters.recordFailed();
deps.onDeliveryFailed?.(error);
const ctx = deps.getContext();
if (ctx !== undefined) refreshStatus(ctx);
},
onAuthorizedMessage(ctx) {
deps.counters.recordReceived();
refreshStatus(ctx);
},
};
}
+111
View File
@@ -0,0 +1,111 @@
/**
* 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"));
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
harness.runtime.deliveryFailed(
new Error("Telegram API sendRichMessage may have committed before transport failed."),
);
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)",
"warning|session|Telegram: reply delivery unconfirmed (check the chat)",
]);
});
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]/);
});
+163
View File
@@ -0,0 +1,163 @@
/**
* Regression tests for the Telegram traffic counters
* Covers what counts as a message, last-activity tracking, failed deliveries, and the /telegram-status line.
*/
import assert from "node:assert/strict";
import test from "node:test";
import { buildTelegramBridgeStatusLines } from "../lib/status.ts";
import {
createTelegramTrafficCounters,
createTelegramTrafficStatusBridge,
isTelegramOutboundMessageMethod,
} from "../lib/traffic.ts";
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, 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,
});
});
test("Only calls that deliver a readable message count as sent", () => {
for (const method of ["sendMessage", "sendRichMessage", "sendPhoto", "sendDocument", "sendVoice"]) {
assert.equal(isTelegramOutboundMessageMethod(method), true, method);
}
for (const method of [
"sendChatAction",
"sendMessageDraft",
"editMessageText",
"setMessageReaction",
"answerCallbackQuery",
"getUpdates",
"toString",
]) {
assert.equal(isTelegramOutboundMessageMethod(method), false, method);
}
});
test("Status lines report the message counters and the time of the last one", () => {
const baseState = {
botUsername: "demo_bot",
allowedUserId: 7,
pollingActive: true,
pendingDispatch: false,
compactionInProgress: false,
activeToolExecutions: 0,
pendingModelSwitch: false,
queuedItems: [],
recentRuntimeEvents: [],
};
const lastAtMs = new Date(2026, 9, 1, 14, 32).getTime();
const clean = buildTelegramBridgeStatusLines({
...baseState,
traffic: { received: 12, sent: 11, failed: 0, lastAtMs },
});
assert.ok(
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);
});
test("Traffic bridge counts delivered messages and refreshes the status with the current context", () => {
const counters = createTelegramTrafficCounters();
const refreshed: string[] = [];
let context: string | undefined = "session-a";
const bridge = createTelegramTrafficStatusBridge({
counters,
getContext: () => context,
updateStatus: (ctx: string) => {
refreshed.push(ctx);
},
recordRuntimeEvent: () => {},
});
bridge.onCallSucceeded("sendChatAction");
bridge.onCallSucceeded("editMessageText");
assert.deepEqual(counters.snapshot(), { received: 0, sent: 0, failed: 0 });
assert.deepEqual(refreshed, []);
bridge.onCallSucceeded("sendRichMessage");
assert.equal(counters.snapshot().sent, 1);
assert.deepEqual(refreshed, ["session-a"]);
context = undefined;
bridge.onCallSucceeded("sendMessage");
assert.equal(counters.snapshot().sent, 2);
assert.deepEqual(refreshed, ["session-a"]);
bridge.onAuthorizedMessage("session-b");
assert.equal(counters.snapshot().received, 1);
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[] = [];
const bridge = createTelegramTrafficStatusBridge({
counters,
getContext: () => "session",
updateStatus: () => {
throw new Error("stale ctx");
},
recordRuntimeEvent: (category, error, details) => {
recorded.push(`${category}:${(error as Error).message}:${details?.phase}`);
},
});
bridge.onCallSucceeded("sendMessage");
bridge.onAuthorizedMessage("session");
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",
]);
});