Files
Moltbot/src/telegram/thread-bindings.test.ts
Harold Hunt d58dafae88 feat(telegram/acp): Topic Binding, Pin Binding Message, Fix Spawn Param Parsing (#36683)
* fix(acp): normalize unicode flags and Telegram topic binding

* feat(telegram/acp): restore topic-bound ACP and session bindings

* fix(acpx): clarify permission-denied guidance

* feat(telegram/acp): pin spawn bind notice in topics

* docs(telegram): document ACP topic thread binding behavior

* refactor(reply): share Telegram conversation-id resolver

* fix(telegram/acp): preserve bound session routing semantics

* fix(telegram): respect binding persistence and expiry reporting

* refactor(telegram): simplify binding lifecycle persistence

* fix(telegram): bind acp spawns in direct messages

* fix: document telegram ACP topic binding changelog (#36683) (thanks @huntharo)

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-06 02:17:50 +01:00

167 lines
5.3 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { resolveStateDir } from "../config/paths.js";
import { getSessionBindingService } from "../infra/outbound/session-binding-service.js";
import {
__testing,
createTelegramThreadBindingManager,
setTelegramThreadBindingIdleTimeoutBySessionKey,
setTelegramThreadBindingMaxAgeBySessionKey,
} from "./thread-bindings.js";
describe("telegram thread bindings", () => {
let stateDirOverride: string | undefined;
beforeEach(() => {
__testing.resetTelegramThreadBindingsForTests();
});
afterEach(() => {
vi.useRealTimers();
if (stateDirOverride) {
delete process.env.OPENCLAW_STATE_DIR;
fs.rmSync(stateDirOverride, { recursive: true, force: true });
stateDirOverride = undefined;
}
});
it("registers a telegram binding adapter and binds current conversations", async () => {
const manager = createTelegramThreadBindingManager({
accountId: "work",
persist: false,
enableSweeper: false,
idleTimeoutMs: 30_000,
maxAgeMs: 0,
});
const bound = await getSessionBindingService().bind({
targetSessionKey: "agent:main:subagent:child-1",
targetKind: "subagent",
conversation: {
channel: "telegram",
accountId: "work",
conversationId: "-100200300:topic:77",
},
placement: "current",
metadata: {
boundBy: "user-1",
},
});
expect(bound.conversation.channel).toBe("telegram");
expect(bound.conversation.accountId).toBe("work");
expect(bound.conversation.conversationId).toBe("-100200300:topic:77");
expect(bound.targetSessionKey).toBe("agent:main:subagent:child-1");
expect(manager.getByConversationId("-100200300:topic:77")?.boundBy).toBe("user-1");
});
it("does not support child placement", async () => {
createTelegramThreadBindingManager({
accountId: "default",
persist: false,
enableSweeper: false,
});
await expect(
getSessionBindingService().bind({
targetSessionKey: "agent:main:subagent:child-1",
targetKind: "subagent",
conversation: {
channel: "telegram",
accountId: "default",
conversationId: "-100200300:topic:77",
},
placement: "child",
}),
).rejects.toMatchObject({
code: "BINDING_CAPABILITY_UNSUPPORTED",
});
});
it("updates lifecycle windows by session key", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z"));
const manager = createTelegramThreadBindingManager({
accountId: "work",
persist: false,
enableSweeper: false,
});
await getSessionBindingService().bind({
targetSessionKey: "agent:main:subagent:child-1",
targetKind: "subagent",
conversation: {
channel: "telegram",
accountId: "work",
conversationId: "1234",
},
});
const original = manager.listBySessionKey("agent:main:subagent:child-1")[0];
expect(original).toBeDefined();
const idleUpdated = setTelegramThreadBindingIdleTimeoutBySessionKey({
accountId: "work",
targetSessionKey: "agent:main:subagent:child-1",
idleTimeoutMs: 2 * 60 * 60 * 1000,
});
vi.setSystemTime(new Date("2026-03-06T12:00:00.000Z"));
const maxAgeUpdated = setTelegramThreadBindingMaxAgeBySessionKey({
accountId: "work",
targetSessionKey: "agent:main:subagent:child-1",
maxAgeMs: 6 * 60 * 60 * 1000,
});
expect(idleUpdated).toHaveLength(1);
expect(idleUpdated[0]?.idleTimeoutMs).toBe(2 * 60 * 60 * 1000);
expect(maxAgeUpdated).toHaveLength(1);
expect(maxAgeUpdated[0]?.maxAgeMs).toBe(6 * 60 * 60 * 1000);
expect(maxAgeUpdated[0]?.boundAt).toBe(original?.boundAt);
expect(maxAgeUpdated[0]?.lastActivityAt).toBe(Date.parse("2026-03-06T12:00:00.000Z"));
expect(manager.listBySessionKey("agent:main:subagent:child-1")[0]?.maxAgeMs).toBe(
6 * 60 * 60 * 1000,
);
});
it("does not persist lifecycle updates when manager persistence is disabled", async () => {
stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-"));
process.env.OPENCLAW_STATE_DIR = stateDirOverride;
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z"));
createTelegramThreadBindingManager({
accountId: "no-persist",
persist: false,
enableSweeper: false,
});
await getSessionBindingService().bind({
targetSessionKey: "agent:main:subagent:child-2",
targetKind: "subagent",
conversation: {
channel: "telegram",
accountId: "no-persist",
conversationId: "-100200300:topic:88",
},
});
setTelegramThreadBindingIdleTimeoutBySessionKey({
accountId: "no-persist",
targetSessionKey: "agent:main:subagent:child-2",
idleTimeoutMs: 60 * 60 * 1000,
});
setTelegramThreadBindingMaxAgeBySessionKey({
accountId: "no-persist",
targetSessionKey: "agent:main:subagent:child-2",
maxAgeMs: 2 * 60 * 60 * 1000,
});
const statePath = path.join(
resolveStateDir(process.env, os.homedir),
"telegram",
"thread-bindings-no-persist.json",
);
expect(fs.existsSync(statePath)).toBe(false);
});
});