Files
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

517 lines
18 KiB
TypeScript

/**
* Regression tests for the runtime domain
* Covers lib/runtime.ts state helpers, controllers, lifecycle hooks, typing, and progress primitives
*/
import assert from "node:assert/strict";
import testRoot, { type TestContext } from "node:test";
import * as Runtime from "../lib/runtime.ts";
import { createTelegramThreadTarget } from "../lib/target.ts";
type RuntimeTestHandler = (context: TestContext) => void | Promise<void>;
function test(name: string, fn: RuntimeTestHandler): void {
void testRoot(name, { concurrency: false, timeout: 5000 }, fn);
}
async function flushMicrotasks(iterations = 10): Promise<void> {
for (let i = 0; i < iterations; i++) {
await Promise.resolve();
}
}
test("Runtime facade binds grouped operations to one bridge state", () => {
const state = Runtime.createTelegramBridgeRuntimeState();
const runtime = Runtime.createTelegramBridgeRuntime(state);
assert.equal(runtime.state, state);
assert.equal(runtime.queue.allocateItemOrder(), 0);
assert.equal(runtime.queue.allocateControlOrder(), 0);
runtime.queue.syncCounters({ nextPriorityReactionOrder: 5 });
assert.equal(runtime.queue.getNextPriorityReactionOrder(), 5);
runtime.queue.incrementNextPriorityReactionOrder();
assert.equal(runtime.queue.getNextPriorityReactionOrder(), 6);
runtime.lifecycle.setDispatchPending(true);
runtime.lifecycle.setCompactionInProgress(true);
runtime.lifecycle.setActiveToolExecutions(3);
runtime.lifecycle.setFoldQueuedPromptsIntoHistory(true);
assert.equal(runtime.lifecycle.hasDispatchPending(), true);
assert.equal(runtime.lifecycle.isCompactionInProgress(), true);
assert.equal(runtime.lifecycle.getActiveToolExecutions(), 3);
runtime.lifecycle.clearDispatchPending();
runtime.lifecycle.resetActiveToolExecutions();
assert.equal(runtime.lifecycle.hasDispatchPending(), false);
assert.equal(runtime.lifecycle.getActiveToolExecutions(), 0);
assert.equal(runtime.lifecycle.shouldFoldQueuedPromptsIntoHistory(), true);
assert.equal(runtime.setup.start(), true);
assert.equal(runtime.setup.isInProgress(), true);
runtime.setup.finish();
assert.equal(runtime.setup.isInProgress(), false);
let abortCount = 0;
runtime.abort.setHandler(() => {
abortCount += 1;
});
assert.equal(runtime.abort.hasHandler(), true);
assert.equal(runtime.abort.abortTurn(), true);
assert.equal(abortCount, 1);
runtime.abort.clearHandler();
assert.equal(runtime.abort.hasHandler(), false);
});
test("Runtime state helpers allocate queue order and manage typing loops", async () => {
const state = Runtime.createTelegramBridgeRuntimeState();
assert.equal(Runtime.allocateTelegramQueueItemOrder(state), 0);
assert.equal(Runtime.allocateTelegramQueueItemOrder(state), 1);
assert.equal(Runtime.allocateTelegramQueueControlOrder(state), 0);
assert.equal(Runtime.getNextTelegramPriorityReactionOrder(state), 0);
Runtime.incrementNextTelegramPriorityReactionOrder(state);
assert.equal(Runtime.getNextTelegramPriorityReactionOrder(state), 1);
Runtime.syncTelegramQueueRuntimeCounters(state, {
nextQueuedTelegramItemOrder: 10,
nextQueuedTelegramControlOrder: 20,
nextPriorityReactionOrder: 30,
});
assert.equal(Runtime.allocateTelegramQueueItemOrder(state), 10);
assert.equal(Runtime.allocateTelegramQueueControlOrder(state), 20);
assert.equal(Runtime.getNextTelegramPriorityReactionOrder(state), 30);
assert.equal(Runtime.hasTelegramDispatchPending(state), false);
assert.equal(Runtime.isTelegramCompactionInProgress(state), false);
assert.equal(Runtime.getActiveTelegramToolExecutions(state), 0);
assert.equal(Runtime.shouldFoldQueuedPromptsIntoHistory(state), false);
Runtime.syncTelegramLifecycleRuntimeFlags(state, {
activeTelegramToolExecutions: 2,
telegramTurnDispatchPending: true,
compactionInProgress: true,
foldQueuedPromptsIntoHistory: true,
});
assert.equal(Runtime.hasTelegramDispatchPending(state), true);
assert.equal(Runtime.isTelegramCompactionInProgress(state), true);
assert.equal(Runtime.getActiveTelegramToolExecutions(state), 2);
assert.equal(Runtime.shouldFoldQueuedPromptsIntoHistory(state), true);
Runtime.clearTelegramDispatchPending(state);
Runtime.setTelegramCompactionInProgress(state, false);
Runtime.resetActiveTelegramToolExecutions(state);
assert.equal(Runtime.hasTelegramDispatchPending(state), false);
assert.equal(Runtime.getActiveTelegramToolExecutions(state), 0);
Runtime.setActiveTelegramToolExecutions(state, 1);
Runtime.setFoldQueuedPromptsIntoHistory(state, false);
assert.equal(Runtime.startTelegramSetup(state), true);
assert.equal(Runtime.startTelegramSetup(state), false);
assert.equal(Runtime.isTelegramSetupInProgress(state), true);
Runtime.finishTelegramSetup(state);
assert.equal(Runtime.isTelegramSetupInProgress(state), false);
let abortCount = 0;
assert.equal(Runtime.hasTelegramAbortHandler(state), false);
assert.equal(Runtime.abortTelegramTurn(state), false);
Runtime.setTelegramAbortHandler(state, () => {
abortCount += 1;
});
assert.equal(Runtime.hasTelegramAbortHandler(state), true);
assert.equal(Runtime.abortTelegramTurn(state), true);
assert.equal(abortCount, 1);
assert.equal(typeof Runtime.getTelegramAbortHandler(state), "function");
Runtime.clearTelegramAbortHandler(state);
assert.equal(Runtime.hasTelegramAbortHandler(state), false);
assert.equal(Runtime.hasTelegramDispatchPending(state), false);
assert.equal(Runtime.isTelegramCompactionInProgress(state), false);
assert.equal(Runtime.getActiveTelegramToolExecutions(state), 1);
assert.equal(Runtime.shouldFoldQueuedPromptsIntoHistory(state), false);
const typingActions: number[] = [];
assert.equal(
Runtime.startTelegramTypingLoop(state, {
chatId: undefined,
intervalMs: 1000,
sendTypingAction: async (chatId) => {
typingActions.push(chatId);
},
}),
false,
);
assert.equal(
Runtime.startTelegramTypingLoop(state, {
chatId: 42,
intervalMs: 1000,
sendTypingAction: async (chatId) => {
typingActions.push(chatId);
},
}),
true,
);
await flushMicrotasks();
assert.deepEqual(typingActions, [42]);
assert.equal(
Runtime.startTelegramTypingLoop(state, {
chatId: 43,
intervalMs: 1000,
sendTypingAction: async (chatId) => {
typingActions.push(chatId);
},
}),
true,
);
await flushMicrotasks();
assert.deepEqual(typingActions, [42, 43]);
assert.equal(Runtime.stopTelegramTypingLoop(state), true);
assert.equal(Runtime.stopTelegramTypingLoop(state), false);
});
test("Typing loop retargets chat-level activity into the active thread", async () => {
const state = Runtime.createTelegramBridgeRuntimeState();
const typingActions: Array<{
chatId: number;
threadId?: number;
aggregate?: boolean;
}> = [];
const recordTypingAction = (
chatId: number,
options?: { message_thread_id?: number },
): void => {
typingActions.push({
chatId,
...(typeof options?.message_thread_id === "number"
? { threadId: options.message_thread_id }
: {}),
});
};
assert.equal(
Runtime.startTelegramTypingLoop(state, {
chatId: 42,
intervalMs: 1000,
sendTypingAction: async (chatId, options) => {
recordTypingAction(chatId, options);
},
sendAggregateTypingAction: async (chatId) => {
typingActions.push({ chatId, aggregate: true });
},
}),
true,
);
await flushMicrotasks();
assert.deepEqual(typingActions, [{ chatId: 42 }]);
assert.equal(
Runtime.startTelegramTypingLoop(state, {
chatId: 42,
target: createTelegramThreadTarget(42, 99),
intervalMs: 1000,
sendTypingAction: async (chatId, options) => {
recordTypingAction(chatId, options);
},
sendAggregateTypingAction: async (chatId) => {
typingActions.push({ chatId, aggregate: true });
},
}),
true,
);
await flushMicrotasks();
assert.deepEqual(typingActions, [
{ chatId: 42 },
{ chatId: 42, threadId: 99 },
{ chatId: 42, aggregate: true },
]);
assert.equal(Runtime.stopTelegramTypingLoop(state), true);
});
test("Typing loop sends chat actions into thread target and aggregate surface", async () => {
const state = Runtime.createTelegramBridgeRuntimeState();
const typingActions: Array<{
chatId: number;
threadId?: number;
aggregate?: boolean;
}> = [];
assert.equal(
Runtime.startTelegramTypingLoop(state, {
chatId: 42,
target: createTelegramThreadTarget(42, 99),
intervalMs: 1000,
sendTypingAction: async (chatId, options) => {
typingActions.push({
chatId,
threadId: options?.message_thread_id,
});
},
sendAggregateTypingAction: async (chatId) => {
typingActions.push({ chatId, aggregate: true });
},
}),
true,
);
await flushMicrotasks();
assert.deepEqual(typingActions, [
{ chatId: 42, threadId: 99 },
{ chatId: 42, aggregate: true },
]);
assert.equal(Runtime.stopTelegramTypingLoop(state), true);
});
test("Typing loop idle wait is bounded for slow in-flight chat actions", async () => {
const state = Runtime.createTelegramBridgeRuntimeState();
state.typingInFlight = new Promise(() => {});
const startedAt = Date.now();
await Runtime.waitForTelegramTypingLoopIdle(state, 1);
assert.ok(Date.now() - startedAt < 100);
});
test("Abort handler setter and agent-end resetter bind runtime cleanup", () => {
const runtime = Runtime.createTelegramBridgeRuntime();
const events: string[] = [];
const setAbortHandler = Runtime.createTelegramContextAbortHandlerSetter(
runtime.abort,
);
setAbortHandler({
abort: () => {
events.push("abort");
},
});
assert.equal(runtime.abort.abortTurn(), true);
const reset = Runtime.createTelegramAgentEndResetter({
abort: runtime.abort,
typing: runtime.typing,
clearActiveTurn: () => {
events.push("active");
},
resetToolExecutions: () => {
events.push("tools");
},
clearPendingModelSwitch: () => {
events.push("switch");
},
clearDispatchPending: runtime.lifecycle.clearDispatchPending,
});
runtime.lifecycle.setDispatchPending(true);
reset();
assert.equal(runtime.abort.hasHandler(), false);
assert.equal(runtime.lifecycle.hasDispatchPending(), false);
assert.deepEqual(events, ["abort", "active", "tools", "switch"]);
});
test("Prompt dispatch lifecycle owns dispatch flags, typing, and status", () => {
const runtime = Runtime.createTelegramBridgeRuntime();
const events: string[] = [];
const lifecycle = Runtime.createTelegramPromptDispatchLifecycle<{
id: string;
}>({
lifecycle: runtime.lifecycle,
typing: runtime.typing,
startTypingLoop: (ctx, chatId) => {
events.push(`typing:${ctx.id}:${chatId ?? "default"}`);
},
updateStatus: (ctx, error) => {
events.push(`status:${ctx.id}:${error ?? "ok"}`);
},
recordRuntimeEvent: (category, error) => {
const message = error instanceof Error ? error.message : String(error);
events.push(`event:${category}:${message}`);
},
});
lifecycle.onPromptDispatchStart({ id: "ctx" }, 42);
assert.equal(runtime.lifecycle.hasDispatchPending(), true);
lifecycle.onPromptDispatchFailure({ id: "ctx" }, "boom");
assert.equal(runtime.lifecycle.hasDispatchPending(), false);
assert.deepEqual(events, [
"typing:ctx:42",
"status:ctx:ok",
"event:dispatch:boom",
"status:ctx:dispatch failed: boom",
]);
});
test("Prompt dispatch lifecycle records stale status failures", () => {
const runtime = Runtime.createTelegramBridgeRuntime();
const events: string[] = [];
const lifecycle = Runtime.createTelegramPromptDispatchLifecycle<{
id: string;
}>({
lifecycle: runtime.lifecycle,
typing: runtime.typing,
startTypingLoop: () => {
events.push("typing");
},
updateStatus: () => {
throw new Error("stale ctx");
},
recordRuntimeEvent: (category, error, details) => {
const message = error instanceof Error ? error.message : String(error);
events.push(`${category}:${message}:${details?.phase ?? "event"}`);
},
});
assert.doesNotThrow(() => lifecycle.onPromptDispatchStart({ id: "ctx" }));
assert.equal(runtime.lifecycle.hasDispatchPending(), true);
assert.doesNotThrow(() =>
lifecycle.onPromptDispatchFailure({ id: "ctx" }, "boom"),
);
assert.equal(runtime.lifecycle.hasDispatchPending(), false);
assert.deepEqual(events, [
"typing",
"dispatch:stale ctx:status-update",
"dispatch:boom:event",
"dispatch:stale ctx:status-update",
]);
});
test("Prompt dispatch runtime binds typing starter and dispatch lifecycle", async () => {
const runtime = Runtime.createTelegramBridgeRuntime();
const sentChatIds: number[] = [];
const statuses: string[] = [];
const promptRuntime = Runtime.createTelegramPromptDispatchRuntime<{
id: string;
}>({
lifecycle: runtime.lifecycle,
typing: runtime.typing,
getDefaultChatId: () => 7,
sendTypingAction: async (chatId) => {
sentChatIds.push(chatId);
},
updateStatus: (_ctx, error) => {
statuses.push(error ?? "ok");
},
intervalMs: 1000,
});
promptRuntime.onPromptDispatchStart({ id: "ctx" }, 9);
await flushMicrotasks();
assert.equal(runtime.lifecycle.hasDispatchPending(), true);
assert.deepEqual(sentChatIds, [9]);
promptRuntime.onPromptDispatchFailure({ id: "ctx" }, "boom");
assert.equal(runtime.lifecycle.hasDispatchPending(), false);
assert.deepEqual(statuses, ["ok", "dispatch failed: boom"]);
});
test("Typing loop starter uses a conservative native keepalive interval", () => {
let capturedIntervalMs = 0;
const startTypingLoop = Runtime.createTelegramTypingLoopStarter<{
id: string;
}>({
typing: {
start: (deps) => {
capturedIntervalMs = deps.intervalMs;
return true;
},
stop: () => true,
waitForIdle: async () => {},
},
getDefaultChatId: () => 7,
sendTypingAction: async () => {},
updateStatus: () => {},
});
startTypingLoop({ id: "ctx" });
assert.equal(capturedIntervalMs, 2500);
});
test("Typing loop starter sends one thread action and one aggregate action", async () => {
const state = Runtime.createTelegramBridgeRuntimeState();
const runtime = Runtime.createTelegramBridgeRuntime(state);
const actions: string[] = [];
const startTypingLoop = Runtime.createTelegramTypingLoopStarter<{
id: string;
}>({
typing: runtime.typing,
getDefaultChatId: () => undefined,
sendTypingAction: async (chatId, options) => {
actions.push(`thread:${chatId}:${options?.message_thread_id ?? "all"}`);
},
sendAggregateTypingAction: async (chatId) => {
actions.push(`aggregate:${chatId}`);
},
updateStatus: () => {},
intervalMs: 1000,
});
startTypingLoop({ id: "ctx" }, 8, {
target: createTelegramThreadTarget(8, 44),
});
await flushMicrotasks();
assert.deepEqual(actions, ["thread:8:44", "aggregate:8"]);
assert.equal(runtime.typing.stop(), true);
});
test("Typing loop starter binds default chat and reports failures", async () => {
const state = Runtime.createTelegramBridgeRuntimeState();
const runtime = Runtime.createTelegramBridgeRuntime(state);
const sentChatIds: number[] = [];
const statusErrors: string[] = [];
const runtimeEvents: string[] = [];
const startTypingLoop = Runtime.createTelegramTypingLoopStarter<{
id: string;
}>({
typing: runtime.typing,
getDefaultChatId: () => 7,
sendTypingAction: async (chatId) => {
sentChatIds.push(chatId);
},
updateStatus: (_ctx: { id: string }, error?: string) => {
if (error) statusErrors.push(error);
},
recordRuntimeEvent: (category, error, details) => {
const message = error instanceof Error ? error.message : String(error);
runtimeEvents.push(`${category}:${message}:${details?.chatId}`);
},
intervalMs: 1000,
});
startTypingLoop({ id: "ctx" });
await flushMicrotasks();
assert.deepEqual(sentChatIds, [7]);
assert.deepEqual(statusErrors, []);
assert.equal(runtime.typing.stop(), true);
const failingStatusErrors: string[] = [];
const startFailingTypingLoop = Runtime.createTelegramTypingLoopStarter<{
id: string;
}>({
typing: runtime.typing,
getDefaultChatId: () => undefined,
sendTypingAction: async () => {
throw new Error("boom");
},
updateStatus: (_ctx: { id: string }, error?: string) => {
if (error) failingStatusErrors.push(error);
},
recordRuntimeEvent: (category, error, details) => {
const message = error instanceof Error ? error.message : String(error);
runtimeEvents.push(`${category}:${message}:${details?.chatId}`);
},
intervalMs: 1000,
});
startFailingTypingLoop({ id: "ctx" }, 8);
await flushMicrotasks();
assert.deepEqual(failingStatusErrors, ["boom"]);
assert.deepEqual(runtimeEvents, ["typing:boom:8"]);
assert.equal(runtime.typing.stop(), true);
});
test("Typing loop starter records stale status failures", async () => {
const state = Runtime.createTelegramBridgeRuntimeState();
const runtime = Runtime.createTelegramBridgeRuntime(state);
const runtimeEvents: string[] = [];
const startTypingLoop = Runtime.createTelegramTypingLoopStarter<{
id: string;
}>({
typing: runtime.typing,
getDefaultChatId: () => undefined,
sendTypingAction: async () => {
throw new Error("typing failed");
},
updateStatus: () => {
throw new Error("stale ctx");
},
recordRuntimeEvent: (category, error, details) => {
const message = error instanceof Error ? error.message : String(error);
runtimeEvents.push(
`${category}:${message}:${details?.phase ?? details?.chatId}`,
);
},
intervalMs: 1000,
});
startTypingLoop({ id: "ctx" }, 8);
await flushMicrotasks();
assert.deepEqual(runtimeEvents, [
"typing:stale ctx:status-update",
"typing:typing failed:8",
]);
assert.equal(runtime.typing.stop(), true);
});