/** * 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 { 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 = []; 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((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((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((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 | 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" }, }, ]); });