mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 07:47:39 +00:00
fix(telegram): clean up update offset on channels remove --delete (#18233)
This commit is contained in:
committed by
Peter Steinberger
parent
b91e43714b
commit
6757a9fedc
@@ -11,6 +11,10 @@ const authMocks = vi.hoisted(() => ({
|
|||||||
loadAuthProfileStore: vi.fn(),
|
loadAuthProfileStore: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const offsetMocks = vi.hoisted(() => ({
|
||||||
|
deleteTelegramUpdateOffset: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../config/config.js", async (importOriginal) => {
|
vi.mock("../config/config.js", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||||
return {
|
return {
|
||||||
@@ -28,6 +32,14 @@ vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../telegram/update-offset-store.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../telegram/update-offset-store.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
deleteTelegramUpdateOffset: offsetMocks.deleteTelegramUpdateOffset,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import {
|
import {
|
||||||
channelsAddCommand,
|
channelsAddCommand,
|
||||||
channelsListCommand,
|
channelsListCommand,
|
||||||
@@ -42,6 +54,7 @@ describe("channels command", () => {
|
|||||||
configMocks.readConfigFileSnapshot.mockReset();
|
configMocks.readConfigFileSnapshot.mockReset();
|
||||||
configMocks.writeConfigFile.mockClear();
|
configMocks.writeConfigFile.mockClear();
|
||||||
authMocks.loadAuthProfileStore.mockReset();
|
authMocks.loadAuthProfileStore.mockReset();
|
||||||
|
offsetMocks.deleteTelegramUpdateOffset.mockClear();
|
||||||
runtime.log.mockClear();
|
runtime.log.mockClear();
|
||||||
runtime.error.mockClear();
|
runtime.error.mockClear();
|
||||||
runtime.exit.mockClear();
|
runtime.exit.mockClear();
|
||||||
@@ -456,4 +469,70 @@ describe("channels command", () => {
|
|||||||
});
|
});
|
||||||
expect(disconnected.join("\n")).toMatch(/disconnected/i);
|
expect(disconnected.join("\n")).toMatch(/disconnected/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("cleans up telegram update offset when deleting a telegram account", async () => {
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||||
|
...baseConfigSnapshot,
|
||||||
|
config: {
|
||||||
|
channels: {
|
||||||
|
telegram: { botToken: "123:abc", enabled: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await channelsRemoveCommand(
|
||||||
|
{ channel: "telegram", account: "default", delete: true },
|
||||||
|
runtime,
|
||||||
|
{
|
||||||
|
hasFlags: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(offsetMocks.deleteTelegramUpdateOffset).toHaveBeenCalledWith({ accountId: "default" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not clean up offset when deleting a non-telegram channel", async () => {
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||||
|
...baseConfigSnapshot,
|
||||||
|
config: {
|
||||||
|
channels: {
|
||||||
|
discord: {
|
||||||
|
accounts: {
|
||||||
|
default: { token: "d0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await channelsRemoveCommand({ channel: "discord", account: "default", delete: true }, runtime, {
|
||||||
|
hasFlags: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(offsetMocks.deleteTelegramUpdateOffset).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not clean up offset when disabling (not deleting) a telegram account", async () => {
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||||
|
...baseConfigSnapshot,
|
||||||
|
config: {
|
||||||
|
channels: {
|
||||||
|
telegram: { botToken: "123:abc", enabled: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = { confirm: vi.fn().mockResolvedValue(true) };
|
||||||
|
const prompterModule = await import("../wizard/clack-prompter.js");
|
||||||
|
const promptSpy = vi
|
||||||
|
.spyOn(prompterModule, "createClackPrompter")
|
||||||
|
.mockReturnValue(prompt as never);
|
||||||
|
|
||||||
|
await channelsRemoveCommand({ channel: "telegram", account: "default" }, runtime, {
|
||||||
|
hasFlags: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(offsetMocks.deleteTelegramUpdateOffset).not.toHaveBeenCalled();
|
||||||
|
promptSpy.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { type OpenClawConfig, writeConfigFile } from "../../config/config.js";
|
import { type OpenClawConfig, writeConfigFile } from "../../config/config.js";
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||||
|
import { deleteTelegramUpdateOffset } from "../../telegram/update-offset-store.js";
|
||||||
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||||
import { type ChatChannel, channelLabel, requireValidConfig, shouldUseWizard } from "./shared.js";
|
import { type ChatChannel, channelLabel, requireValidConfig, shouldUseWizard } from "./shared.js";
|
||||||
|
|
||||||
@@ -112,6 +113,11 @@ export async function channelsRemoveCommand(
|
|||||||
cfg: next,
|
cfg: next,
|
||||||
accountId: resolvedAccountId,
|
accountId: resolvedAccountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clean up Telegram polling offset to prevent stale offset on bot token change (#18233)
|
||||||
|
if (channel === "telegram") {
|
||||||
|
await deleteTelegramUpdateOffset({ accountId: resolvedAccountId });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!plugin.config.setAccountEnabled) {
|
if (!plugin.config.setAccountEnabled) {
|
||||||
runtime.error(`Channel ${channel} does not support disable.`);
|
runtime.error(`Channel ${channel} does not support disable.`);
|
||||||
|
|||||||
55
src/telegram/update-offset-store.test.ts
Normal file
55
src/telegram/update-offset-store.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
deleteTelegramUpdateOffset,
|
||||||
|
readTelegramUpdateOffset,
|
||||||
|
writeTelegramUpdateOffset,
|
||||||
|
} from "./update-offset-store.js";
|
||||||
|
|
||||||
|
async function withTempStateDir<T>(fn: (dir: string) => Promise<T>) {
|
||||||
|
const previous = process.env.OPENCLAW_STATE_DIR;
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-tg-offset-"));
|
||||||
|
process.env.OPENCLAW_STATE_DIR = dir;
|
||||||
|
try {
|
||||||
|
return await fn(dir);
|
||||||
|
} finally {
|
||||||
|
if (previous === undefined) {
|
||||||
|
delete process.env.OPENCLAW_STATE_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.OPENCLAW_STATE_DIR = previous;
|
||||||
|
}
|
||||||
|
await fs.rm(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("deleteTelegramUpdateOffset", () => {
|
||||||
|
it("removes the offset file so a new bot starts fresh", async () => {
|
||||||
|
await withTempStateDir(async () => {
|
||||||
|
await writeTelegramUpdateOffset({ accountId: "default", updateId: 432_000_000 });
|
||||||
|
expect(await readTelegramUpdateOffset({ accountId: "default" })).toBe(432_000_000);
|
||||||
|
|
||||||
|
await deleteTelegramUpdateOffset({ accountId: "default" });
|
||||||
|
expect(await readTelegramUpdateOffset({ accountId: "default" })).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not throw when the offset file does not exist", async () => {
|
||||||
|
await withTempStateDir(async () => {
|
||||||
|
await expect(deleteTelegramUpdateOffset({ accountId: "nonexistent" })).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only removes the targeted account offset, leaving others intact", async () => {
|
||||||
|
await withTempStateDir(async () => {
|
||||||
|
await writeTelegramUpdateOffset({ accountId: "default", updateId: 100 });
|
||||||
|
await writeTelegramUpdateOffset({ accountId: "alerts", updateId: 200 });
|
||||||
|
|
||||||
|
await deleteTelegramUpdateOffset({ accountId: "default" });
|
||||||
|
|
||||||
|
expect(await readTelegramUpdateOffset({ accountId: "default" })).toBeNull();
|
||||||
|
expect(await readTelegramUpdateOffset({ accountId: "alerts" })).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -80,3 +80,19 @@ export async function writeTelegramUpdateOffset(params: {
|
|||||||
await fs.chmod(tmp, 0o600);
|
await fs.chmod(tmp, 0o600);
|
||||||
await fs.rename(tmp, filePath);
|
await fs.rename(tmp, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteTelegramUpdateOffset(params: {
|
||||||
|
accountId?: string;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
}): Promise<void> {
|
||||||
|
const filePath = resolveTelegramUpdateOffsetPath(params.accountId, params.env);
|
||||||
|
try {
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
} catch (err) {
|
||||||
|
const code = (err as { code?: string }).code;
|
||||||
|
if (code === "ENOENT") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user