fix: keep the paired user when the poll loop commits its offset

The poll loop persisted the config object it captured when polling started.
The store swaps in a fresh object on every persist, so after the initial sync
that captured copy lacked later changes: the next offset commit erased the
paired user and the bridge re-paired on every message.
This commit is contained in:
evandrodevbr committed 2026-10-01 11:14:02 -03:00
1 parent 033a92f3bb
commit 60254b288b
2 files changed
+47 -1

No files matched your search

+9 -1
View File
@@ -971,7 +971,15 @@ export function createTelegramPollLoopRunner<
config: deps.getConfig(),
deleteWebhook: deps.deleteWebhook,
getUpdates: deps.getUpdates,
persistConfig: deps.persistConfig,
// The loop keeps the config object captured when polling started, but the
// store swaps in a fresh object on every persist. Persisting that stale
// copy erased later changes such as the paired user, so only the poll
// offset is carried onto the store's current config.
persistConfig: (polled) => {
const current = deps.getConfig();
current.lastUpdateId = polled.lastUpdateId;
return deps.persistConfig(current);
},
handleUpdate: deps.handleUpdate,
onErrorStatus: (message) => {
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
+38
View File
@@ -4,6 +4,9 @@
*/
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import {
@@ -29,6 +32,7 @@ import {
stopTelegramPollingRuntime,
TELEGRAM_ALLOWED_UPDATES,
} from "../lib/polling.ts";
import { createTelegramConfigStore } from "../lib/config.ts";
const TEST_CONTEXT = "ctx";
@@ -601,6 +605,40 @@ test("Poll loop runner binds config, status, and transport ports", async () => {
assert.deepEqual(events, ["deleteWebhook", "handle:ctx:6", "persist:6"]);
});
test("Poll loop keeps the paired user when the offset is committed after the initial sync replaced the stored config", async () => {
const dir = mkdtempSync(join(tmpdir(), "telegram-pairing-"));
const configPath = join(dir, "telegram.json");
const store = createTelegramConfigStore({ agentDir: dir, configPath });
await store.load();
store.update((config) => {
config.botToken = "123:abc";
});
await store.persist();
let calls = 0;
const runPollLoop = createTelegramPollLoopRunner({
getConfig: store.get,
deleteWebhook: async () => {},
getUpdates: async () => {
calls += 1;
if (calls === 1) return [{ update_id: 1 }]; // initial sync
if (calls === 2) return [{ update_id: 2 }]; // the owner's /start
throw new DOMException("stop", "AbortError");
},
persistConfig: store.persist,
handleUpdate: async () => {
store.setAllowedUserId(777);
await store.persist();
},
updateStatus: () => {},
sleep: async () => {},
});
await runPollLoop("ctx", new AbortController().signal);
assert.equal(store.getAllowedUserId(), 777);
const saved = JSON.parse(readFileSync(configPath, "utf8"));
assert.equal(saved.allowedUserId, 777);
assert.equal(saved.lastUpdateId, 2);
});
test("Poll loop runner ignores stale-context status failures while retrying", async () => {
const config = { botToken: "123:abc", lastUpdateId: 1 };
const events: string[] = [];