Files
oh-my-pi-telegram/tests/preview.test.ts
Evandro Fonseca Junior d1d04001d0
Release / release (push) Failing after 5s
Validate / validate (push) Failing after 5m21s
chore: fork @llblab/[email protected] as @evandrodevbr/oh-my-pi-telegram
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)
2026-07-15 14:18:16 -03:00

473 lines
14 KiB
TypeScript

/**
* Regression tests for the Telegram preview domain
* Covers native rich draft previews, safe-prefix selection, and finalization behavior
*/
import assert from "node:assert/strict";
import test from "node:test";
import {
allocateTelegramDraftId,
buildTelegramPreviewFinalText,
clearTelegramPreview,
createTelegramAssistantPreviewRuntime,
createTelegramNativeMarkdownPreviewFinalizer,
createTelegramPreviewControllerRuntime,
createTelegramPreviewRuntimeState,
finalizeTelegramPreview,
flushTelegramPreview,
getSafeTelegramRichMarkdownDraftPrefix,
shouldUseTelegramDraftPreview,
type TelegramPreviewRuntimeState,
} from "../lib/preview.ts";
import { createTelegramThreadTarget } from "../lib/target.ts";
function createPreviewRuntimeHarness(state?: TelegramPreviewRuntimeState) {
let previewState = state;
let draftSupport: "unknown" | "supported" = "unknown";
let nextDraftId = 10;
const events: string[] = [];
return {
events,
getState: () => previewState,
getDraftSupport: () => draftSupport,
deps: {
getState: () => previewState,
setState: (nextState: TelegramPreviewRuntimeState | undefined) => {
previewState = nextState;
},
maxMessageLength: 100,
getDraftSupport: () => draftSupport,
setDraftSupport: (support: "unknown" | "supported") => {
draftSupport = support;
},
allocateDraftId: () => nextDraftId++,
sendDraft: async (
chatId: number,
draftId: number,
text?: string,
_options?: { message_thread_id?: number },
) => {
events.push(`draft:${chatId}:${draftId}:${text}`);
},
canSend: undefined as undefined | (() => boolean),
recordRuntimeEvent: (
category: string,
_error: unknown,
details?: Record<string, unknown>,
) => {
events.push(`${category}:${details?.phase}`);
},
},
};
}
test("Preview helpers create draft-only state and allocate draft ids", () => {
assert.deepEqual(createTelegramPreviewRuntimeState(), {
mode: "draft",
pendingText: "",
lastSentText: "",
});
assert.equal(allocateTelegramDraftId(0, 2), 1);
assert.equal(allocateTelegramDraftId(1, 2), 2);
assert.equal(allocateTelegramDraftId(2, 2), 1);
});
test("Preview final text prefers pending text then last sent draft text", () => {
assert.equal(
buildTelegramPreviewFinalText({
mode: "draft",
pendingText: " final ",
lastSentText: "old",
}),
"final",
);
assert.equal(
buildTelegramPreviewFinalText({
mode: "draft",
pendingText: " ",
lastSentText: " old ",
}),
"old",
);
assert.equal(
buildTelegramPreviewFinalText({
mode: "draft",
pendingText: " ",
lastSentText: " ",
}),
undefined,
);
});
test("Preview helpers always use native rich drafts", () => {
assert.equal(
shouldUseTelegramDraftPreview({ draftSupport: "unknown" }),
true,
);
assert.equal(
shouldUseTelegramDraftPreview({
draftSupport: "supported",
snapshot: { text: "ok" },
}),
true,
);
});
test("Native Markdown draft prefix keeps only structurally closed Markdown", () => {
assert.equal(
getSafeTelegramRichMarkdownDraftPrefix("**Bold** and *italic*", 100),
"**Bold** and *italic*",
);
assert.equal(
getSafeTelegramRichMarkdownDraftPrefix("**Bold** and *ita", 100),
"**Bold** and",
);
assert.equal(
getSafeTelegramRichMarkdownDraftPrefix("**Heading still streaming", 100),
undefined,
);
assert.equal(
getSafeTelegramRichMarkdownDraftPrefix("Before\n\n```ts\nconst x = 1", 100),
"Before",
);
assert.equal(
getSafeTelegramRichMarkdownDraftPrefix(
"Before\n\n```ts\nconst x = 1\n```",
100,
),
"Before\n\n```ts\nconst x = 1\n```",
);
assert.equal(
getSafeTelegramRichMarkdownDraftPrefix(
"[OpenAI](https://openai.com) and [half",
100,
),
"[OpenAI](https://openai.com) and",
);
assert.equal(
getSafeTelegramRichMarkdownDraftPrefix("Block math:\n\n$$\nx^2", 100),
"Block math:",
);
assert.equal(
getSafeTelegramRichMarkdownDraftPrefix(
"<!-- telegram_button label=Ok",
100,
),
undefined,
);
});
test("Preview runtime sends only safe native markdown draft prefixes", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
draftId: 10,
pendingText: "**Bold** and *ita",
lastSentText: "",
});
await flushTelegramPreview(7, harness.deps);
assert.deepEqual(harness.events, ["draft:7:10:**Bold** and"]);
assert.equal(harness.getState()?.lastSentText, "**Bold** and");
assert.equal(harness.getDraftSupport(), "supported");
});
test("Preview runtime sends draft previews into thread target", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
draftId: 10,
pendingText: "thread draft",
lastSentText: "",
});
const sentOptions: unknown[] = [];
harness.deps.sendDraft = async (
_chatId: number,
_draftId: number,
_text?: string,
options?: { message_thread_id?: number },
) => {
sentOptions.push(options);
};
await flushTelegramPreview(7, harness.deps, {
target: createTelegramThreadTarget(7, 42),
});
assert.deepEqual(sentOptions, [{ message_thread_id: 42 }]);
});
test("Preview runtime clears thread drafts in thread target", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
draftId: 10,
pendingText: "new draft",
lastSentText: "",
});
const sentOptions: unknown[] = [];
harness.deps.sendDraft = async (
_chatId: number,
_draftId: number,
_text?: string,
options?: { message_thread_id?: number },
) => {
sentOptions.push(options);
};
await clearTelegramPreview(7, harness.deps, {
target: createTelegramThreadTarget(7, 42),
});
assert.deepEqual(sentOptions, [{ message_thread_id: 42 }]);
});
test("Preview runtime does not send thinking placeholder for unsafe draft tails", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
pendingText: "<!-- telegram_button label=Ok",
lastSentText: "",
});
await flushTelegramPreview(7, harness.deps);
assert.deepEqual(harness.events, []);
assert.equal(harness.getState()?.draftId, undefined);
assert.equal(harness.getState()?.lastSentText, "");
});
test("Preview runtime skips unchanged unsafe draft tails", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
pendingText: "**Bold** and *ita",
lastSentText: "**Bold** and",
});
await flushTelegramPreview(7, harness.deps);
assert.deepEqual(harness.events, []);
});
test("Preview runtime records draft failures without plain fallback", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
draftId: 10,
pendingText: "abcdef",
lastSentText: "",
});
harness.deps.sendDraft = async () => {
throw new Error("draft rejected partial markdown");
};
await flushTelegramPreview(7, harness.deps);
assert.deepEqual(harness.events, ["preview:draft"]);
assert.equal(harness.getState()?.mode, "draft");
assert.equal(harness.getDraftSupport(), "unknown");
});
test("Preview runtime serializes overlapping flush requests", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
draftId: 44,
pendingText: "first",
lastSentText: "",
});
let releaseDraft: (() => void) | undefined;
harness.deps.sendDraft = async (chatId, draftId, text) => {
harness.events.push(`draft:${chatId}:${draftId}:${text}`);
if (!releaseDraft) {
await new Promise<void>((resolve) => {
releaseDraft = resolve;
});
}
};
const firstFlush = flushTelegramPreview(7, harness.deps);
await Promise.resolve();
const state = harness.getState();
assert.ok(state);
state.pendingText = "second";
const secondFlush = flushTelegramPreview(7, harness.deps);
releaseDraft?.();
await Promise.all([firstFlush, secondFlush]);
assert.deepEqual(harness.events, ["draft:7:44:first", "draft:7:44:second"]);
assert.equal(harness.getState()?.lastSentText, "second");
});
test("Preview runtime clears active rich draft on explicit clear", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
draftId: 10,
pendingText: "new draft",
lastSentText: "",
});
await clearTelegramPreview(7, harness.deps);
assert.deepEqual(harness.events, ["draft:7:10:undefined"]);
assert.equal(harness.getState(), undefined);
});
test("Preview runtime optional send gate clears without sending new content", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
pendingText: "**hello**",
lastSentText: "",
});
harness.deps.canSend = () => false;
await flushTelegramPreview(7, harness.deps);
assert.deepEqual(harness.events, []);
assert.equal(harness.getState(), undefined);
});
test("Native Markdown finalizer waits for active draft flush before final reply", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
draftId: 10,
pendingText: "draft body",
lastSentText: "",
});
const releases: Array<() => void> = [];
harness.deps.sendDraft = async (chatId, draftId, text) => {
harness.events.push(`draft-start:${chatId}:${draftId}:${text}`);
await new Promise<void>((resolve) => {
releases.push(resolve);
});
harness.events.push(`draft-finish:${chatId}:${draftId}:${text}`);
};
const finalizeMarkdown = createTelegramNativeMarkdownPreviewFinalizer({
getState: harness.deps.getState,
clear: (chatId) => clearTelegramPreview(chatId, harness.deps),
discard: () => harness.deps.setState(undefined),
sendMarkdownReply: async (chatId, replyToMessageId, markdown) => {
harness.events.push(
`final:${chatId}:${replyToMessageId ?? "none"}:${markdown}`,
);
return 88;
},
});
const flush = flushTelegramPreview(7, harness.deps);
await Promise.resolve();
const finalize = finalizeMarkdown(7, "final body", 55);
await Promise.resolve();
assert.deepEqual(harness.events, ["draft-start:7:10:draft body"]);
releases.shift()?.();
await Promise.all([flush, finalize]);
assert.deepEqual(harness.events, [
"draft-start:7:10:draft body",
"draft-finish:7:10:draft body",
"final:7:55:final body",
]);
assert.equal(harness.getState(), undefined);
});
test("Native Markdown finalizer stops when preview generation changes during flush", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
draftId: 10,
pendingText: "old draft",
lastSentText: "",
});
let release: (() => void) | undefined;
harness.deps.sendDraft = async () =>
new Promise<void>((resolve) => {
release = resolve;
});
let finalSends = 0;
const finalizeMarkdown = createTelegramNativeMarkdownPreviewFinalizer({
getState: harness.deps.getState,
clear: (chatId) => clearTelegramPreview(chatId, harness.deps),
discard: () => harness.deps.setState(undefined),
sendMarkdownReply: async () => {
finalSends += 1;
return 88;
},
});
const flush = flushTelegramPreview(7, harness.deps);
await Promise.resolve();
const finalize = finalizeMarkdown(7, "old final", 55);
await Promise.resolve();
harness.deps.setState({
mode: "draft",
draftId: 11,
pendingText: "new draft",
lastSentText: "",
});
release?.();
assert.equal(await finalize, false);
await flush;
assert.equal(finalSends, 0);
assert.equal(harness.getState()?.draftId, 11);
});
test("Plain preview finalization does not send fallback messages", async () => {
const harness = createPreviewRuntimeHarness({
mode: "draft",
draftId: 10,
pendingText: "final body",
lastSentText: "final",
});
assert.equal(await finalizeTelegramPreview(7, harness.deps), false);
assert.deepEqual(harness.events, ["draft:7:10:final body"]);
assert.equal(harness.getState(), undefined);
});
test("Assistant preview runtime finalizes previous markdown through native reply sender", async () => {
const events: string[] = [];
const runtime = createTelegramAssistantPreviewRuntime<{
role: string;
text?: string;
}>({
getActiveTurn: () => ({
chatId: 7,
replyToMessageId: 24,
target: createTelegramThreadTarget(7, 42),
}),
isAssistantMessage: (message) => message.role === "assistant",
getMessageText: (message) => message.text ?? "",
maxMessageLength: 100,
sendDraft: async () => {},
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
events.push(
`native-final:${chatId}:${replyToMessageId ?? "none"}:${markdown}:${
options?.target?.threadId ?? "private"
}`,
);
return 99;
},
});
runtime.setState({
mode: "draft",
pendingText: "**previous**",
lastSentText: "**previous**",
});
await runtime.onMessageStart({ message: { role: "assistant" } });
assert.deepEqual(events, ["native-final:7:24:**previous**:42"]);
assert.equal(runtime.getState()?.pendingText, "");
});
test("Assistant preview runtime suppresses text preview for voice-tagged turns", async () => {
const events: string[] = [];
const activeTurn = { chatId: 7, voiceReplyPreferred: true };
const runtime = createTelegramAssistantPreviewRuntime<{
role: string;
text?: string;
}>({
getActiveTurn: () => activeTurn,
isAssistantMessage: (message) => message.role === "assistant",
getMessageText: (message) => message.text ?? "",
maxMessageLength: 100,
sendDraft: async () => {
events.push("draft");
},
sendMarkdownReply: async () => undefined,
});
await runtime.onMessageStart({ message: { role: "assistant" } });
assert.equal(runtime.getState(), undefined);
await runtime.onMessageUpdate({
message: { role: "assistant", text: "hello" },
});
assert.equal(runtime.getState(), undefined);
assert.deepEqual(events, []);
});
test("Preview controller runtime binds Bot API draft transport", async () => {
const events: string[] = [];
const runtime = createTelegramPreviewControllerRuntime({
sendDraft: async (chatId, draftId, text) => {
events.push(`draft:${chatId}:${draftId}:${text}`);
},
maxDraftId: 10,
});
runtime.resetState();
runtime.setPendingText("hello".repeat(60));
await runtime.flush(7);
assert.deepEqual(events, [`draft:7:1:${"hello".repeat(60)}`]);
});