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

1606 lines
48 KiB
TypeScript

/**
* Regression tests for Telegram multi-instance bus follower helpers
* Covers follower registration, forwarded update receiving, and follower-routed API calls
*/
import assert from "node:assert/strict";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import {
createTelegramBusFollowerApiCaller,
createTelegramBusFollowerHeartbeatRecoveryHandler,
createTelegramBusFollowerRegistrationRuntime,
createTelegramBusFollowerPromotionHandler,
createTelegramBusFollowerRegistrationState,
createTelegramBusFollowerRuntimeAssembly,
createTelegramBusFollowerSessionRefreshHook,
createTelegramBusFollowerSessionReplacementSuspender,
createTelegramBusFollowerTargetReplacementHandler,
createTelegramBusForwardedRouteHandlers,
createTelegramBusForwardedUpdateReceiverRuntime,
createTelegramManualFollowerProfileKeyResolver,
getTelegramFollowerSessionHandoff,
setTelegramFollowerSessionHandoff,
} from "../lib/bus-follower.ts";
import {
createTelegramBusFollowerRegistry,
createTelegramBusFollowerTargetController,
createTelegramBusLocalServer,
sendTelegramBusLocalEnvelope,
} from "../lib/bus.ts";
import { createTelegramBusLeaderEnvelopeHandler } from "../lib/bus-leader.ts";
import { createTelegramTopicTargetStore } from "../lib/threads.ts";
import { isTelegramApiCommitUnknownError } from "../lib/telegram-api.ts";
async function waitForCondition(
predicate: () => boolean,
timeoutMs = 250,
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) return;
await new Promise((resolve) => setTimeout(resolve, 5));
}
assert.fail("Timed out waiting for condition");
}
test("Bus follower route handlers adapt forwarded envelopes", async () => {
const events: string[] = [];
const handlers = createTelegramBusForwardedRouteHandlers<
string,
{ emoji: string },
{ id: string },
{ text: string }
>({
handleUpdate(update, ctx) {
events.push(
`${ctx}:${update.callback_query?.id ?? update.message?.text ?? update.edited_message?.text}`,
);
},
handleAuthorizedReactionUpdate(reaction, ctx) {
events.push(`${ctx}:${reaction.emoji}`);
},
});
await handlers.handleForwardedCallback({ id: "callback" }, "ctx");
await handlers.handleForwardedReaction({ emoji: "👍" }, "ctx");
await handlers.handleForwardedMessage?.({ text: "message" }, "ctx");
await handlers.handleForwardedEditedMessage?.({ text: "edited" }, "ctx");
assert.deepEqual(events, [
"ctx:callback",
"ctx:👍",
"ctx:message",
"ctx:edited",
]);
});
test("Bus follower profile key resolver follows the active profile", () => {
let profileName: string | undefined;
const resolveProfileKey = createTelegramManualFollowerProfileKeyResolver({
getActiveProfileName: () => profileName,
manualFollowerOwnerId: "7",
});
assert.equal(resolveProfileKey(), "manual:7");
profileName = "work";
assert.equal(resolveProfileKey(), "profile:work:manual:7");
});
test("Bus follower promotion handler transfers binding only after leadership acquisition", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-follower-promotion-"));
const store = createTelegramTopicTargetStore({ path: join(dir, "state.json") });
const events: unknown[] = [];
const promote = createTelegramBusFollowerPromotionHandler({
topicTargetStore: store,
instanceId: "inst-a",
getActiveProfileName: () => "work",
startLeader: async (ctx: { cwd: string }, _election, onAcquired) => {
events.push(`acquired:${ctx.cwd}`);
await onAcquired();
return true;
},
recordRuntimeEvent: (category, message, details) => {
events.push({ category, message, details });
},
});
try {
await promote(
{ cwd: "/repo" },
{
target: { chatId: 42, threadId: 11 },
slot: "E",
threadName: "Ember",
},
{},
);
assert.equal(store.list()[0]?.profileKey, "profile:work:cwd:/repo");
assert.equal(store.list()[0]?.owner?.kind, "leader");
assert.equal(events[0], "acquired:/repo");
assert.deepEqual(events[1], {
category: "bus",
message: "Follower thread binding promoted to leader",
details: {
phase: "follower-promoted-binding",
chatId: 42,
threadId: 11,
slot: "E",
threadName: "Ember",
},
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower promotion leaves binding unchanged when election is lost", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-follower-election-lost-"));
const store = createTelegramTopicTargetStore({ path: join(dir, "state.json") });
const promote = createTelegramBusFollowerPromotionHandler({
topicTargetStore: store,
instanceId: "inst-a",
getActiveProfileName: () => "work",
startLeader: async () => false,
recordRuntimeEvent: () => undefined,
});
try {
assert.equal(
await promote(
{ cwd: "/repo" },
{
target: { chatId: 42, threadId: 11 },
slot: "E",
threadName: "Ember",
},
{ expectedOwner: { pid: 99 } },
),
false,
);
assert.deepEqual(store.list(), []);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower receiver handles leader-forwarded updates and target replacement", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-forward-"));
const leaderSocketPath = join(dir, "leader.sock");
const followerSocketPath = join(dir, "follower.sock");
const registry = createTelegramBusFollowerRegistry();
const received: unknown[] = [];
let nowMs = 2000;
const receiver = createTelegramBusForwardedUpdateReceiverRuntime({
socketPath: followerSocketPath,
instanceId: "inst-b",
getContext() {
return "ctx";
},
handleForwardedCallback(query, ctx) {
received.push({ kind: "callback", query, ctx });
},
handleForwardedReaction(reactionUpdate, ctx) {
received.push({ kind: "reaction", reactionUpdate, ctx });
},
handleForwardedMessage(message, ctx) {
received.push({ kind: "message", message, ctx });
},
handleForwardedEditedMessage(message, ctx) {
received.push({ kind: "edited-message", message, ctx });
},
handleReplaceTarget(input, ctx) {
received.push({ kind: "replace-target", input, ctx });
},
});
const leader = createTelegramBusLocalServer({
socketPath: leaderSocketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: registry,
getNowMs: () => nowMs,
}),
});
try {
await receiver.start();
await leader.start();
registry.register({
instanceId: "inst-b",
busSocketPath: followerSocketPath,
connectedAtMs: 1000,
});
const callbackResponse = await sendTelegramBusLocalEnvelope({
socketPath: leaderSocketPath,
envelope: {
kind: "leader.forwardCallback",
requestId: "leader:1",
recipientInstanceId: "inst-b",
query: { id: "cb-1", data: "queue:pause" },
sentAtMs: 2000,
},
});
assert.equal(registry.get("inst-b")?.lastHeartbeatMs, 2000);
nowMs = 3000;
const reactionResponse = await sendTelegramBusLocalEnvelope({
socketPath: leaderSocketPath,
envelope: {
kind: "leader.forwardReaction",
requestId: "leader:2",
recipientInstanceId: "inst-b",
reactionUpdate: { message_id: 9, new_reaction: [] },
sentAtMs: 3000,
},
});
assert.equal(registry.get("inst-b")?.lastHeartbeatMs, 3000);
nowMs = 4000;
const messageResponse = await sendTelegramBusLocalEnvelope({
socketPath: leaderSocketPath,
envelope: {
kind: "leader.forwardMessage",
requestId: "leader:3",
recipientInstanceId: "inst-b",
message: { message_id: 10, text: "hi" },
sentAtMs: 4000,
},
});
assert.equal(registry.get("inst-b")?.lastHeartbeatMs, 4000);
nowMs = 5000;
const editedMessageResponse = await sendTelegramBusLocalEnvelope({
socketPath: leaderSocketPath,
envelope: {
kind: "leader.forwardEditedMessage",
requestId: "leader:4",
recipientInstanceId: "inst-b",
message: { message_id: 10, text: "edited" },
sentAtMs: 5000,
},
});
const targetController = createTelegramBusFollowerTargetController({
socketPath: followerSocketPath,
createRequestId: () => "leader:5",
getNowMs: () => 6000,
});
const replaceTargetResponse = await targetController.replaceTarget({
follower: registry.get("inst-b")!,
target: { chatId: 7, threadId: 42 },
oldTarget: { chatId: 7, threadId: 10 },
reason: "thread-restore",
});
assert.deepEqual(callbackResponse, {
kind: "bus.ack",
requestId: "leader:1",
ok: true,
message: undefined,
});
assert.deepEqual(reactionResponse, {
kind: "bus.ack",
requestId: "leader:2",
ok: true,
message: undefined,
});
assert.deepEqual(messageResponse, {
kind: "bus.ack",
requestId: "leader:3",
ok: true,
message: undefined,
});
assert.deepEqual(editedMessageResponse, {
kind: "bus.ack",
requestId: "leader:4",
ok: true,
message: undefined,
});
assert.equal(replaceTargetResponse, true);
assert.equal(registry.get("inst-b")?.lastHeartbeatMs, 5000);
assert.deepEqual(received, [
{
kind: "callback",
query: { id: "cb-1", data: "queue:pause" },
ctx: "ctx",
},
{
kind: "reaction",
reactionUpdate: { message_id: 9, new_reaction: [] },
ctx: "ctx",
},
{ kind: "message", message: { message_id: 10, text: "hi" }, ctx: "ctx" },
{
kind: "edited-message",
message: { message_id: 10, text: "edited" },
ctx: "ctx",
},
{
kind: "replace-target",
input: {
target: { chatId: 7, threadId: 42 },
oldTarget: { chatId: 7, threadId: 10 },
reason: "thread-restore",
},
ctx: "ctx",
},
]);
} finally {
await leader.stop();
await receiver.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower receiver rejects delayed work from a replaced registration generation", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-forward-generation-"));
const socketPath = join(dir, "follower.sock");
let handled = 0;
const receiver = createTelegramBusForwardedUpdateReceiverRuntime({
socketPath,
instanceId: "inst-b",
getRegistrationGeneration: () => "generation-new",
getContext: () => "ctx",
handleForwardedCallback() {
handled += 1;
},
handleForwardedReaction() {},
});
try {
await receiver.start();
const response = await sendTelegramBusLocalEnvelope({
socketPath,
envelope: {
kind: "leader.forwardCallback",
requestId: "leader:old:1",
recipientInstanceId: "inst-b",
recipientRegistrationGeneration: "generation-old",
query: { id: "old" },
sentAtMs: 2000,
},
});
assert.equal(handled, 0);
assert.deepEqual(response, {
kind: "bus.ack",
requestId: "leader:old:1",
ok: false,
message: "Stale Telegram bus follower registration generation.",
});
} finally {
await receiver.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower heartbeat recovery passes current binding into promotion", async () => {
const promoted: unknown[] = [];
let leaderStateCalls = 0;
const registrationState = createTelegramBusFollowerRegistrationState();
registrationState.setRegistered(
true,
{ chatId: 42, threadId: 10 },
{
slot: "F",
threadName: "Fjord",
},
);
const handler = createTelegramBusFollowerHeartbeatRecoveryHandler({
registrationState,
getRegistrationRuntime: () => ({
registerWithLeader: async () => false,
setContext: () => undefined,
stop: () => undefined,
}),
getLeaderState: () => {
leaderStateCalls += 1;
return leaderStateCalls === 1
? { kind: "active-elsewhere", lock: { pid: 99 } }
: { kind: "inactive" };
},
setLifecyclePhase: () => undefined,
updateStatus: () => undefined,
promoteToLeader: async (_ctx, binding) => {
promoted.push(binding);
return true;
},
sleep: async () => undefined,
promotionGraceMs: 0,
recordRuntimeEvent: () => undefined,
});
await handler(new Error("heartbeat failed"), "ctx");
assert.deepEqual(promoted, [
{ target: { chatId: 42, threadId: 10 }, slot: "F", threadName: "Fjord" },
]);
});
test("Bus follower heartbeat recovery never promotes over a live leader lease", async () => {
const promoted: unknown[] = [];
const phases: Array<string | undefined> = [];
const events: Array<{ message: unknown; phase?: unknown }> = [];
const registrationState = createTelegramBusFollowerRegistrationState();
registrationState.setRegistered(true, { chatId: 42, threadId: 10 });
const liveLeader = {
kind: "active-elsewhere" as const,
lock: {
pid: 99,
instanceId: "leader-a",
leaderEpoch: "epoch-a",
},
};
const handler = createTelegramBusFollowerHeartbeatRecoveryHandler({
registrationState,
getRegistrationRuntime: () => ({
registerWithLeader: async () => false,
setContext: () => undefined,
stop: () => undefined,
}),
getLeaderState: () => liveLeader,
setLifecyclePhase: (phase) => {
phases.push(phase);
},
updateStatus: () => undefined,
promoteToLeader: async (_ctx, binding) => {
promoted.push(binding);
return true;
},
sleep: async () => undefined,
promotionGraceMs: 0,
recordRuntimeEvent: (_category, message, details) => {
events.push({ message, phase: details?.phase });
},
});
await handler(new Error("heartbeat failed"), "ctx");
assert.deepEqual(promoted, []);
assert.equal(phases.at(-1), undefined);
assert.equal(
events.some(
(event) => event.phase === "follower-promotion-live-owner",
),
true,
);
});
test("Bus follower heartbeat recovery retries until a live lease becomes stale", async () => {
let stateReadCount = 0;
let scheduledRetry: (() => void) | undefined;
let resolvePromoted: (() => void) | undefined;
const promoted = new Promise<void>((resolve) => {
resolvePromoted = resolve;
});
const registrationState = createTelegramBusFollowerRegistrationState();
registrationState.setRegistered(
true,
{ chatId: 42, threadId: 10 },
{ slot: "F", threadName: "Fjord" },
);
const liveLock = {
pid: 99,
instanceId: "leader-a",
leaderEpoch: "epoch-a",
};
const handler = createTelegramBusFollowerHeartbeatRecoveryHandler({
registrationState,
getRegistrationRuntime: () => ({
registerWithLeader: async () => false,
setContext: () => undefined,
stop: () => undefined,
}),
getLeaderState: () => {
stateReadCount += 1;
return stateReadCount <= 2
? { kind: "active-elsewhere", lock: liveLock }
: { kind: "stale", lock: liveLock };
},
setLifecyclePhase: () => undefined,
updateStatus: () => undefined,
promoteToLeader: async (_ctx, binding, election) => {
assert.deepEqual(binding, {
target: { chatId: 42, threadId: 10 },
slot: "F",
threadName: "Fjord",
});
assert.deepEqual(election, { expectedOwner: liveLock });
resolvePromoted?.();
return true;
},
sleep: async () => undefined,
scheduleRetry: (retry) => {
scheduledRetry = retry;
},
getActiveContext: () => "ctx",
promotionGraceMs: 0,
recordRuntimeEvent: () => undefined,
});
await handler(new Error("heartbeat failed"), "ctx");
assert.ok(scheduledRetry);
scheduledRetry();
await promoted;
});
test("Bus follower election loser schedules re-registration with the winner", async () => {
const scheduled: Array<() => void> = [];
let registrationCalls = 0;
let promotionCalls = 0;
let registrationTarget: unknown;
let resolveRegistered: (() => void) | undefined;
const registered = new Promise<void>((resolve) => {
resolveRegistered = resolve;
});
const registrationState = createTelegramBusFollowerRegistrationState();
registrationState.setRegistered(true, { chatId: 42, threadId: 10 });
const staleLock = { pid: 99, leaderEpoch: "old-epoch" };
const winnerLock = { pid: 100, leaderEpoch: "winner-epoch" };
let state: "stale" | "winner" = "stale";
const handler = createTelegramBusFollowerHeartbeatRecoveryHandler({
registrationState,
getRegistrationRuntime: () => ({
registerWithLeader: async (_ctx, _leader, options) => {
registrationCalls += 1;
registrationTarget = options?.target;
resolveRegistered?.();
return true;
},
setContext: () => undefined,
stop: () => {
registrationState.setRegistered(false);
},
}),
getLeaderState: () =>
state === "stale"
? { kind: "stale", lock: staleLock }
: { kind: "active-elsewhere", lock: winnerLock },
setLifecyclePhase: () => undefined,
updateStatus: () => undefined,
promoteToLeader: async () => {
promotionCalls += 1;
state = "winner";
return false;
},
sleep: async () => undefined,
scheduleRetry: (retry) => {
scheduled.push(retry);
},
getActiveContext: () => "ctx",
promotionGraceMs: 0,
recordRuntimeEvent: () => undefined,
});
await handler(new Error("heartbeat failed"), "ctx");
assert.equal(promotionCalls, 1);
assert.equal(scheduled.length, 1);
scheduled.shift()?.();
await registered;
assert.equal(registrationCalls, 1);
assert.deepEqual(registrationTarget, { chatId: 42, threadId: 10 });
});
test("Bus follower scheduled recovery transfers across session context replacement", async () => {
const scheduled: Array<() => void> = [];
let activeContext: string | undefined = "old-ctx";
let stateReads = 0;
let promotedContext: string | undefined;
let resolvePromoted: (() => void) | undefined;
const promoted = new Promise<void>((resolve) => {
resolvePromoted = resolve;
});
const registrationState = createTelegramBusFollowerRegistrationState();
registrationState.setRegistered(true, { chatId: 42, threadId: 10 });
const lock = { pid: 99, leaderEpoch: "epoch-a" };
const handler = createTelegramBusFollowerHeartbeatRecoveryHandler({
registrationState,
getRegistrationRuntime: () => ({
registerWithLeader: async () => false,
setContext: () => undefined,
stop: () => undefined,
}),
getLeaderState: () => {
stateReads += 1;
return stateReads <= 2
? { kind: "active-elsewhere", lock }
: { kind: "stale", lock };
},
setLifecyclePhase: () => undefined,
updateStatus: () => undefined,
promoteToLeader: async (ctx) => {
promotedContext = ctx;
resolvePromoted?.();
return true;
},
sleep: async () => undefined,
scheduleRetry: (retry) => {
scheduled.push(retry);
},
getActiveContext: () => activeContext,
promotionGraceMs: 0,
recordRuntimeEvent: () => undefined,
});
await handler(new Error("heartbeat failed"), "old-ctx");
activeContext = undefined;
scheduled.shift()?.();
assert.equal(scheduled.length, 1);
activeContext = "new-ctx";
scheduled.shift()?.();
await promoted;
assert.equal(promotedContext, "new-ctx");
});
test("Bus follower heartbeat recovery swallows stale-context status updates", async () => {
const events: unknown[] = [];
let leaderStateCalls = 0;
const registrationState = createTelegramBusFollowerRegistrationState();
registrationState.setRegistered(true, { chatId: 42, threadId: 10 });
const handler = createTelegramBusFollowerHeartbeatRecoveryHandler({
registrationState,
getRegistrationRuntime: () => ({
registerWithLeader: async () => false,
setContext: () => undefined,
stop: () => undefined,
}),
getLeaderState: () => {
leaderStateCalls += 1;
return leaderStateCalls === 1
? { kind: "active-elsewhere", lock: { pid: 99 } }
: { kind: "inactive" };
},
setLifecyclePhase: () => undefined,
updateStatus: () => {
throw new Error("This extension ctx is stale after session replacement");
},
promoteToLeader: async () => true,
sleep: async () => undefined,
promotionGraceMs: 0,
recordRuntimeEvent: (category, error, details) => {
events.push({ category, error, details });
},
});
await handler(new Error("heartbeat failed"), "stale-ctx");
assert.equal(registrationState.getTarget(), undefined);
assert.equal(
events.some(
(event) =>
typeof event === "object" &&
event !== null &&
(event as { details?: { phase?: string } }).details?.phase ===
"follower-stale-context-status",
),
true,
);
});
test("Bus follower target replacement handler persists restored target", async () => {
const staleTargets: unknown[] = [];
const upserts: unknown[] = [];
let persisted = false;
let updated = false;
let syncState = {};
const events: unknown[] = [];
const registrationState = createTelegramBusFollowerRegistrationState();
registrationState.setRegistered(true, { chatId: 42, threadId: 10 });
const handler = createTelegramBusFollowerTargetReplacementHandler({
topicTargetStore: {
load: async () => undefined,
list: () => [
{
profileKey: "manual:old",
owner: { kind: "manual-follower", instanceId: "old" },
instanceId: "inst-a",
target: { chatId: 42, threadId: 10 },
status: "active",
createdAtMs: 1000,
updatedAtMs: 1000,
slot: "E",
threadName: "Ember",
},
],
markStaleByTarget: (target) => {
staleTargets.push(target);
return true;
},
upsert: (record) => {
upserts.push(record);
return record;
},
persist: async () => {
persisted = true;
},
},
registrationState,
instanceId: "inst-a",
getManualFollowerProfileKey: () => "manual:new",
manualFollowerOwnerId: "new",
getSyncState: () => syncState,
setSyncState: (state) => {
syncState = state;
},
getNowMs: () => 2000,
updateStatus: () => {
updated = true;
},
recordRuntimeEvent: (_category, message, details) => {
events.push({ message, details });
},
});
await handler(
{
target: { chatId: 42, threadId: 11 },
oldTarget: { chatId: 42, threadId: 10 },
reason: "thread-restore",
},
"ctx",
);
assert.deepEqual(staleTargets, [{ chatId: 42, threadId: 10 }]);
assert.equal(registrationState.getTarget()?.threadId, 11);
assert.equal(persisted, true);
assert.equal(updated, true);
assert.deepEqual(syncState, {
"target-bindings": {
status: "fresh",
updatedAtMs: 2000,
lastReconcileAction: "follower-thread-restore",
},
});
assert.deepEqual(upserts, [
{
profileKey: "manual:old",
owner: { kind: "manual-follower", instanceId: "new" },
target: { chatId: 42, threadId: 11 },
status: "active",
syncStatus: "open",
createdAtMs: 1000,
updatedAtMs: 2000,
lastSyncObservedAtMs: 2000,
lastReconcileAction: "follower-thread-restore",
instanceId: "inst-a",
slot: "E",
threadName: "Ember",
rerouteConfirmedAtMs: 2000,
},
]);
assert.deepEqual(events, [
{
message: "Telegram follower thread target replaced",
details: {
phase: "follower-thread-restore",
chatId: 42,
threadId: 11,
oldThreadId: 10,
slot: "E",
},
},
]);
});
test("Bus follower target replacement resolves named-profile fallback at call time", async () => {
let activeProfileKey = "manual:default";
const upserts: Array<{ profileKey: string }> = [];
const registrationState = createTelegramBusFollowerRegistrationState();
const handler = createTelegramBusFollowerTargetReplacementHandler({
topicTargetStore: {
load: async () => undefined,
list: () => [],
markStaleByTarget: () => false,
upsert: (record) => {
upserts.push(record);
return record;
},
persist: async () => undefined,
},
registrationState,
instanceId: "inst-a",
getManualFollowerProfileKey: () => activeProfileKey,
manualFollowerOwnerId: "owner-a",
getSyncState: () => ({}),
setSyncState: () => undefined,
getNowMs: () => 2000,
updateStatus: () => undefined,
});
activeProfileKey = "profile:work:manual-follower:owner-a";
await handler(
{
target: { chatId: 42, threadId: 11 },
reason: "thread-restore",
},
"ctx",
);
assert.equal(upserts[0]?.profileKey, "profile:work:manual-follower:owner-a");
});
test("Bus follower assembly wires receiver, recovery, and registration", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-follower-assembly-"));
const leaderSocketPath = join(dir, "leader.sock");
const followerSocketPath = join(dir, "follower.sock");
const registrationState = createTelegramBusFollowerRegistrationState();
let requestSequence = 0;
const leader = createTelegramBusLocalServer({
socketPath: leaderSocketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: createTelegramBusFollowerRegistry(),
provisionFollowerTarget: () => ({ chatId: 7, threadId: 42 }),
}),
});
const assembly = createTelegramBusFollowerRuntimeAssembly<
{ cwd: string },
unknown,
unknown
>({
receiver: {
socketPath: followerSocketPath,
instanceId: "inst-a",
getContext: () => ({ cwd: "/repo" }),
handleForwardedCallback: () => undefined,
handleForwardedReaction: () => undefined,
},
targetReplacement: {
topicTargetStore: {
load: async () => undefined,
list: () => [],
markStaleByTarget: () => false,
upsert: (record) => record,
persist: async () => undefined,
},
registrationState,
instanceId: "inst-a",
getManualFollowerProfileKey: () => "manual:a",
manualFollowerOwnerId: "a",
getSyncState: () => ({}),
setSyncState: () => undefined,
updateStatus: () => undefined,
},
recovery: {
registrationState,
getLeaderState: () => ({ kind: "inactive" }),
setLifecyclePhase: () => undefined,
updateStatus: () => undefined,
promoteToLeader: async () => true,
sleep: async () => undefined,
promotionGraceMs: 1,
recordRuntimeEvent: () => undefined,
},
registration: {
instanceId: "inst-a",
getFollowerBusSocketPath: () => followerSocketPath,
getLeaderSocketPath: () => leaderSocketPath,
registrationState,
createRequestId: () => `inst-a:${++requestSequence}`,
},
});
try {
await leader.start();
assert.equal(
await assembly.registration.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: leaderSocketPath },
),
true,
);
assert.equal(existsSync(followerSocketPath), true);
assert.deepEqual(registrationState.getTarget(), {
chatId: 7,
threadId: 42,
});
} finally {
assembly.registration.stop();
await assembly.receiver.stop();
await leader.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration state tracks successful registration and stop", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-follower-state-"));
const socketPath = join(dir, "bus.sock");
const registry = createTelegramBusFollowerRegistry();
const state = createTelegramBusFollowerRegistrationState();
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: registry,
provisionFollowerTarget() {
return {
chatId: -1007,
threadId: 42,
slot: "E",
threadName: "Ember",
};
},
}),
});
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => "inst-a:1",
getNowMs: () => 1000,
registrationState: state,
});
try {
await server.start();
assert.equal(state.isRegistered(), false);
assert.equal(state.getTarget(), undefined);
assert.equal(
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
),
true,
);
assert.equal(state.isRegistered(), true);
assert.deepEqual(state.getTarget(), { chatId: -1007, threadId: 42 });
assert.equal(state.getSlot(), "E");
assert.equal(state.getThreadName(), "Ember");
follower.stop();
assert.equal(state.isRegistered(), false);
assert.equal(state.getTarget(), undefined);
assert.equal(state.getSlot(), undefined);
assert.equal(state.getThreadName(), undefined);
} finally {
follower.stop();
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower re-registration carries its last known target", async () => {
const dir = mkdtempSync(
join(tmpdir(), "pi-telegram-follower-reload-target-"),
);
const socketPath = join(dir, "bus.sock");
const state = createTelegramBusFollowerRegistrationState();
const registrations: Array<{
target?: unknown;
slot?: string;
threadName?: string;
}> = [];
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: createTelegramBusFollowerRegistry(),
provisionFollowerTarget(registration) {
registrations.push({
target: registration.target,
slot: registration.slot,
threadName: registration.threadName,
});
return {
chatId: 7,
threadId: 42,
slot: "E",
threadName: "Ember",
};
},
}),
});
let requestSequence = 0;
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => `inst-a:reload:${++requestSequence}`,
registrationState: state,
});
try {
await server.start();
assert.equal(
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
),
true,
);
state.setRegistered(false);
assert.equal(
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
),
true,
);
assert.deepEqual(registrations, [
{ target: undefined, slot: undefined, threadName: "repo" },
{
target: { chatId: 7, threadId: 42 },
slot: "E",
threadName: "Ember",
},
]);
} finally {
follower.stop();
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration runtime retries while leader endpoint is starting", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-follower-retry-"));
const socketPath = join(dir, "bus.sock");
const registry = createTelegramBusFollowerRegistry();
const state = createTelegramBusFollowerRegistrationState();
const events: Array<Record<string, unknown> | undefined> = [];
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: registry,
provisionFollowerTarget() {
return { chatId: -1007, threadId: 42 };
},
}),
});
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => "inst-a:1",
getNowMs: () => 1000,
registrationState: state,
registrationTimeoutMs: 50,
registrationRetryAttempts: 10,
registrationRetryDelayMs: 10,
recordRuntimeEvent(_category, _error, details) {
events.push(details);
},
});
try {
setTimeout(() => {
void server.start();
}, 25).unref?.();
assert.equal(
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
),
true,
);
assert.equal(state.isRegistered(), true);
assert.deepEqual(state.getTarget(), { chatId: -1007, threadId: 42 });
assert.equal(
events.some((event) => event?.phase === "follower-register-client-retry"),
true,
);
} finally {
follower.stop();
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration runtime waits for slow target provisioning", async () => {
const dir = mkdtempSync(
join(tmpdir(), "pi-telegram-bus-follower-slow-register-"),
);
const socketPath = join(dir, "bus.sock");
const registry = createTelegramBusFollowerRegistry();
const state = createTelegramBusFollowerRegistrationState();
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: registry,
async provisionFollowerTarget() {
await new Promise((resolve) => setTimeout(resolve, 50));
return { chatId: -1007, threadId: 42 };
},
}),
});
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => "inst-a:1",
getNowMs: () => 1000,
registrationState: state,
timeoutMs: 20,
registrationTimeoutMs: 250,
});
try {
await server.start();
assert.equal(
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
),
true,
);
assert.equal(state.isRegistered(), true);
assert.deepEqual(state.getTarget(), { chatId: -1007, threadId: 42 });
assert.deepEqual(registry.get("inst-a")?.target, {
chatId: -1007,
threadId: 42,
});
} finally {
follower.stop();
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration runtime registers with a live leader socket", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-follower-"));
const socketPath = join(dir, "bus.sock");
const registry = createTelegramBusFollowerRegistry();
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: registry,
getNowMs: () => 1000,
}),
});
let sequence = 0;
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => `inst-a:${++sequence}`,
getNowMs: () => 1000,
getPid: () => 123,
});
try {
await server.start();
assert.equal(
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
),
true,
);
assert.deepEqual(registry.get("inst-a"), {
instanceId: "inst-a",
profileKey: "cwd:/repo",
threadName: "repo",
cwd: "/repo",
pid: 123,
registrationGeneration: "inst-a:1",
connectedAtMs: 1000,
lastHeartbeatMs: 1000,
target: undefined,
});
} finally {
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration runtime accepts explicit manual profile keys", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-follower-profile-"));
const socketPath = join(dir, "bus.sock");
const registry = createTelegramBusFollowerRegistry();
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: registry,
}),
});
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => "inst-a:1",
getNowMs: () => 1000,
getProfileKey: () => "manual:inst-a",
});
try {
await server.start();
assert.equal(
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
),
true,
);
assert.equal(registry.get("inst-a")?.profileKey, "manual:inst-a");
} finally {
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration runtime reports heartbeat failure with active context", async () => {
const dir = mkdtempSync(
join(tmpdir(), "pi-telegram-bus-follower-heartbeat-fail-"),
);
const socketPath = join(dir, "bus.sock");
const failures: unknown[] = [];
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: (envelope) => ({
kind: "bus.ack",
requestId: envelope.requestId,
ok: true,
}),
});
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => "inst-a:1",
registrationState: createTelegramBusFollowerRegistrationState(),
heartbeatMs: 10,
timeoutMs: 50,
onHeartbeatFailure(error, ctx) {
failures.push({ error: String(error), ctx });
},
});
try {
await server.start();
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
);
await server.stop();
await waitForCondition(() => failures.length > 0, 200);
assert.deepEqual((failures[0] as { ctx: unknown }).ctx, { cwd: "/repo" });
} finally {
follower.stop();
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration runtime reports rejected heartbeat with active context", async () => {
const dir = mkdtempSync(
join(tmpdir(), "pi-telegram-bus-follower-heartbeat-reject-"),
);
const socketPath = join(dir, "bus.sock");
const failures: unknown[] = [];
let requestSequence = 0;
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: (envelope) => ({
kind: "bus.ack",
requestId: envelope.requestId,
ok: envelope.kind === "follower.register",
message:
envelope.kind === "follower.register"
? undefined
: "Unknown Telegram bus follower instance.",
}),
});
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => `inst-a:${++requestSequence}`,
registrationState: createTelegramBusFollowerRegistrationState(),
heartbeatMs: 10,
timeoutMs: 50,
onHeartbeatFailure(error, ctx) {
failures.push({ error: String(error), ctx });
},
});
try {
await server.start();
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
);
await waitForCondition(() => failures.length > 0, 200);
assert.deepEqual(failures[0], {
error: "Error: Unknown Telegram bus follower instance.",
ctx: { cwd: "/repo" },
});
} finally {
follower.stop();
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration runtime heartbeats until stopped", async () => {
const dir = mkdtempSync(
join(tmpdir(), "pi-telegram-bus-follower-heartbeat-"),
);
const socketPath = join(dir, "bus.sock");
const registry = createTelegramBusFollowerRegistry();
let nowMs = 1000;
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: registry,
getNowMs: () => nowMs,
}),
});
let requestSequence = 0;
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => `inst-a:${++requestSequence}`,
getNowMs: () => nowMs,
heartbeatMs: 50,
});
try {
await server.start();
assert.equal(
await follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
),
true,
);
nowMs = 2000;
await waitForCondition(
() => registry.get("inst-a")?.lastHeartbeatMs === 2000,
120,
);
follower.stop();
nowMs = 3000;
await new Promise((resolve) => setTimeout(resolve, 20));
assert.equal(registry.get("inst-a")?.lastHeartbeatMs, 2000);
} finally {
follower.stop();
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration runtime surfaces leader rejection reasons", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-follower-reject-"));
const socketPath = join(dir, "bus.sock");
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: () => ({
kind: "bus.ack",
requestId: "inst-a:1",
ok: false,
message: "Unauthorized Telegram bus envelope.",
}),
});
const stopped: string[] = [];
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => "inst-a:1",
stopReceiving: () => {
stopped.push("stop");
},
});
try {
await server.start();
await assert.rejects(
() =>
follower.registerWithLeader(
{ cwd: "/repo" },
{ busSocketPath: socketPath },
),
/Unauthorized Telegram bus envelope/,
);
assert.deepEqual(stopped, ["stop"]);
} finally {
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower registration runtime derives leader socket when lock omits it", async () => {
const dir = mkdtempSync(
join(tmpdir(), "pi-telegram-bus-follower-derived-socket-"),
);
const socketPath = join(dir, "bus.sock");
const registry = createTelegramBusFollowerRegistry();
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
followerRegistry: registry,
getNowMs: () => 1000,
}),
});
const follower = createTelegramBusFollowerRegistrationRuntime({
instanceId: "inst-a",
createRequestId: () => "inst-a:1",
getLeaderSocketPath: () => socketPath,
});
try {
await server.start();
assert.equal(await follower.registerWithLeader({ cwd: "/repo" }, {}), true);
assert.equal(registry.get("inst-a")?.instanceId, "inst-a");
} finally {
follower.stop();
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower API caller sends method calls and returns leader results", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-api-caller-"));
const socketPath = join(dir, "bus.sock");
const received: unknown[] = [];
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: (envelope) => {
received.push(envelope);
return {
kind: "bus.ack",
requestId: envelope.requestId,
ok: true,
result: { message_id: 55 },
};
},
});
const callApi = createTelegramBusFollowerApiCaller({
socketPath,
instanceId: "inst-a",
createRequestId: () => "inst-a:1",
getNowMs: () => 7000,
});
try {
await server.start();
assert.deepEqual(await callApi("sendRichMessage", [{ chat_id: 1 }]), {
message_id: 55,
});
assert.deepEqual(received, [
{
kind: "follower.callApi",
requestId: "inst-a:1",
instanceId: "inst-a",
method: "sendRichMessage",
args: [{ chat_id: 1 }],
sentAtMs: 7000,
},
]);
} finally {
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower API caller preserves structured commit-unknown errors", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-api-ambiguous-"));
const socketPath = join(dir, "bus.sock");
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: (envelope) => ({
kind: "bus.ack",
requestId: envelope.requestId,
ok: false,
message: "sendMessage response was lost",
error: { code: "commit-unknown", method: "sendMessage" },
}),
});
const callApi = createTelegramBusFollowerApiCaller({
socketPath,
instanceId: "inst-a",
createRequestId: () => "inst-a:ambiguous:1",
});
try {
await server.start();
await assert.rejects(
() => callApi("call", ["sendMessage", { chat_id: 1, text: "hello" }]),
isTelegramApiCommitUnknownError,
);
} finally {
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower API caller classifies non-idempotent acknowledgement loss as commit-unknown", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-telegram-bus-api-ack-loss-"));
const socketPath = join(dir, "bus.sock");
let executions = 0;
const server = createTelegramBusLocalServer({
socketPath,
handleEnvelope: (envelope) => {
executions += 1;
return {
kind: "bus.ack",
requestId: envelope.requestId,
ok: true,
result: { message_id: 77 },
};
},
shouldDropResponse: () => true,
});
const callApi = createTelegramBusFollowerApiCaller({
socketPath,
instanceId: "inst-a",
createRequestId: () => "inst-a:ack-loss:1",
timeoutMs: 10,
});
try {
await server.start();
await assert.rejects(
() => callApi("call", ["sendMessage", { chat_id: 1, text: "hello" }]),
isTelegramApiCommitUnknownError,
);
assert.equal(executions, 1);
} finally {
await server.stop();
rmSync(dir, { recursive: true, force: true });
}
});
test("Bus follower session replacement preserves a same-process handoff", async () => {
const registrationState = createTelegramBusFollowerRegistrationState();
registrationState.setRegistered(
true,
{ chatId: 1, threadId: 2 },
{ slot: "B", threadName: "Beryl" },
);
const events: unknown[] = [];
let suspended = false;
const suspend = createTelegramBusFollowerSessionReplacementSuspender({
registrationState,
instanceId: "old-inst",
async suspendPolling() {
suspended = true;
registrationState.setRegistered(false);
},
recordRuntimeEvent(category, message, details) {
events.push({ category, message, details });
},
getPid: () => 10,
getNowMs: () => 500,
});
await suspend();
assert.equal(suspended, true);
assert.equal(registrationState.isRegistered(), false);
assert.deepEqual(getTelegramFollowerSessionHandoff(), {
pid: 10,
instanceId: "old-inst",
createdAtMs: 500,
target: { chatId: 1, threadId: 2 },
slot: "B",
threadName: "Beryl",
});
assert.deepEqual(events, [
{
category: "bus",
message: "Telegram follower registration suspended for session replacement",
details: {
phase: "follower-session-handoff",
instanceId: "old-inst",
chatId: 1,
threadId: 2,
},
},
]);
setTelegramFollowerSessionHandoff(undefined);
});
test("Bus follower session refresh re-registers with the handed-off target", async () => {
const registrationState = createTelegramBusFollowerRegistrationState();
const registrations: unknown[] = [];
const events: unknown[] = [];
setTelegramFollowerSessionHandoff({
pid: process.pid,
instanceId: "old-inst",
createdAtMs: Date.now(),
target: { chatId: 1, threadId: 2 },
slot: "B",
threadName: "Beryl",
});
const refresh = createTelegramBusFollowerSessionRefreshHook({
registrationState,
registrationRuntime: {
async registerWithLeader(ctx, leader, options) {
registrations.push({ ctx, leader, options });
registrationState.setRegistered(
true,
options?.target,
{ slot: "B", threadName: "Beryl" },
);
return true;
},
setContext: () => undefined,
},
getLeaderState: () => ({
kind: "active-elsewhere",
lock: { pid: 20, busSocketPath: "/tmp/leader.sock" },
}),
updateStatus: () => undefined,
recordRuntimeEvent(category, message, details) {
events.push({ category, message, details });
},
});
await refresh({}, { cwd: "/repo" });
assert.deepEqual(registrations, [
{
ctx: { cwd: "/repo" },
leader: { pid: 20, busSocketPath: "/tmp/leader.sock" },
options: { target: { chatId: 1, threadId: 2 } },
},
]);
assert.equal(registrationState.isRegistered(), true);
assert.deepEqual(registrationState.getTarget(), { chatId: 1, threadId: 2 });
assert.equal(getTelegramFollowerSessionHandoff(), undefined);
assert.deepEqual(events, [
{
category: "bus",
message: "Telegram follower registration restored after session replacement",
details: {
phase: "follower-session-restore",
previousInstanceId: "old-inst",
},
},
{
category: "bus",
message: "Telegram follower session context refreshed",
details: { phase: "follower-session-refresh" },
},
]);
});