mirror of https://github.com/openclaw/openclaw.git
test(acp): cover generic conversation binds
This commit is contained in:
parent
ec9f96cb2a
commit
dee2bde2f5
|
|
@ -373,6 +373,21 @@ async function runTelegramDmAcpCommand(commandBody: string, cfg: OpenClawConfig
|
|||
);
|
||||
}
|
||||
|
||||
async function runSlackDmAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(
|
||||
createConversationParams(
|
||||
commandBody,
|
||||
{
|
||||
channel: "slack",
|
||||
originatingTo: "user:U123",
|
||||
senderId: "U123",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function createMatrixThreadParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = createConversationParams(
|
||||
commandBody,
|
||||
|
|
@ -802,7 +817,7 @@ describe("/acp command", () => {
|
|||
conversation: expect.objectContaining({
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
conversationId: "parent-1",
|
||||
conversationId: "channel:parent-1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
|
@ -824,6 +839,22 @@ describe("/acp command", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("binds Slack DMs with --bind here through the generic conversation path", async () => {
|
||||
const result = await runSlackDmAcpCommand("/acp spawn codex --bind here");
|
||||
|
||||
expect(result?.reply?.text).toContain("Bound this conversation to");
|
||||
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
placement: "current",
|
||||
conversation: expect.objectContaining({
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "U123",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("binds iMessage DMs with --bind here", async () => {
|
||||
const result = await runIMessageDmAcpCommand("/acp spawn codex --bind here");
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,6 @@ describe("commands-acp context", () => {
|
|||
accountId: "default",
|
||||
threadId: undefined,
|
||||
conversationId: "123456789",
|
||||
parentConversationId: "123456789",
|
||||
});
|
||||
expect(resolveAcpCommandConversationId(params)).toBe("123456789");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { SessionEntry } from "../../config/sessions.js";
|
|||
import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.ts";
|
||||
import {
|
||||
__testing as sessionBindingTesting,
|
||||
getSessionBindingService,
|
||||
registerSessionBindingAdapter,
|
||||
} from "../../infra/outbound/session-binding-service.js";
|
||||
import { enqueueSystemEvent, resetSystemEventsForTest } from "../../infra/system-events.js";
|
||||
|
|
@ -61,6 +62,10 @@ async function writeSessionStoreFast(
|
|||
await fs.writeFile(storePath, JSON.stringify(store), "utf-8");
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
});
|
||||
|
||||
describe("initSessionState thread forking", () => {
|
||||
it("forks a new session from the parent session file", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
|
@ -515,7 +520,7 @@ describe("initSessionState RawBody", () => {
|
|||
expect(result.isNewSession).toBe(false);
|
||||
});
|
||||
|
||||
it("does not rotate local session state for ACP /new when conversation IDs are unavailable", async () => {
|
||||
it("rotates local session state for ACP /new when no matching conversation binding exists", async () => {
|
||||
const root = await makeCaseDir("openclaw-rawbody-acp-reset-no-conversation-");
|
||||
const storePath = path.join(root, "sessions.json");
|
||||
const sessionKey = "agent:codex:acp:binding:discord:default:feedface";
|
||||
|
|
@ -555,9 +560,9 @@ describe("initSessionState RawBody", () => {
|
|||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.resetTriggered).toBe(false);
|
||||
expect(result.sessionId).toBe(existingSessionId);
|
||||
expect(result.isNewSession).toBe(false);
|
||||
expect(result.resetTriggered).toBe(true);
|
||||
expect(result.sessionId).not.toBe(existingSessionId);
|
||||
expect(result.isNewSession).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps custom reset triggers working on bound ACP sessions", async () => {
|
||||
|
|
@ -846,6 +851,83 @@ describe("initSessionState RawBody", () => {
|
|||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "Slack DM",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
},
|
||||
ctx: {
|
||||
Provider: "slack",
|
||||
Surface: "slack",
|
||||
From: "slack:user:U123",
|
||||
To: "user:U123",
|
||||
OriginatingTo: "user:U123",
|
||||
SenderId: "U123",
|
||||
ChatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Signal DM",
|
||||
conversation: {
|
||||
channel: "signal",
|
||||
accountId: "default",
|
||||
conversationId: "+15550001111",
|
||||
},
|
||||
ctx: {
|
||||
Provider: "signal",
|
||||
Surface: "signal",
|
||||
From: "signal:+15550001111",
|
||||
To: "+15550001111",
|
||||
OriginatingTo: "signal:+15550001111",
|
||||
SenderId: "+15550001111",
|
||||
ChatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Google Chat room",
|
||||
conversation: {
|
||||
channel: "googlechat",
|
||||
accountId: "default",
|
||||
conversationId: "spaces/AAAAAAA",
|
||||
},
|
||||
ctx: {
|
||||
Provider: "googlechat",
|
||||
Surface: "googlechat",
|
||||
From: "googlechat:users/123",
|
||||
To: "spaces/AAAAAAA",
|
||||
OriginatingTo: "googlechat:spaces/AAAAAAA",
|
||||
SenderId: "users/123",
|
||||
ChatType: "group",
|
||||
},
|
||||
},
|
||||
])("routes generic current-conversation bindings for $name", async ({ conversation, ctx }) => {
|
||||
const storePath = await createStorePath("openclaw-generic-current-binding-");
|
||||
const boundSessionKey = `agent:codex:acp:binding:${conversation.channel}:default:test`;
|
||||
|
||||
await getSessionBindingService().bind({
|
||||
targetSessionKey: boundSessionKey,
|
||||
targetKind: "session",
|
||||
conversation,
|
||||
});
|
||||
|
||||
const result = await initSessionState({
|
||||
ctx: {
|
||||
RawBody: "hello",
|
||||
SessionKey: `agent:main:${conversation.channel}:seed`,
|
||||
...ctx,
|
||||
},
|
||||
cfg: {
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.sessionKey).toBe(boundSessionKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initSessionState reset policy", () => {
|
||||
|
|
@ -1160,7 +1242,7 @@ describe("initSessionState reset triggers in WhatsApp groups", () => {
|
|||
senderName: "Other",
|
||||
senderE164: "+1555123456",
|
||||
senderId: "123@lid",
|
||||
expectedIsNewSession: false,
|
||||
expectedIsNewSession: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { setDefaultChannelPluginRegistryForTests } from "../../commands/channel-test-helpers.js";
|
||||
import {
|
||||
__testing,
|
||||
getSessionBindingService,
|
||||
|
|
@ -46,6 +47,7 @@ function createRecord(input: SessionBindingBindInput): SessionBindingRecord {
|
|||
describe("session binding service", () => {
|
||||
beforeEach(() => {
|
||||
__testing.resetSessionBindingAdaptersForTests();
|
||||
setDefaultChannelPluginRegistryForTests();
|
||||
});
|
||||
|
||||
it("normalizes conversation refs and infers current placement", async () => {
|
||||
|
|
@ -214,6 +216,148 @@ describe("session binding service", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("falls back to generic current-conversation bindings for built-in channels", async () => {
|
||||
const service = getSessionBindingService();
|
||||
|
||||
expect(
|
||||
service.getCapabilities({
|
||||
channel: "Slack",
|
||||
accountId: " DEFAULT ",
|
||||
}),
|
||||
).toEqual({
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current"],
|
||||
});
|
||||
|
||||
const bound = await service.bind({
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: " Slack ",
|
||||
accountId: " DEFAULT ",
|
||||
conversationId: " user:U123 ",
|
||||
},
|
||||
metadata: {
|
||||
label: "slack-dm",
|
||||
},
|
||||
ttlMs: 60_000,
|
||||
});
|
||||
|
||||
expect(bound).toMatchObject({
|
||||
bindingId: "generic:slack\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
},
|
||||
status: "active",
|
||||
metadata: expect.objectContaining({
|
||||
label: "slack-dm",
|
||||
}),
|
||||
});
|
||||
|
||||
const resolved = service.resolveByConversation({
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
});
|
||||
expect(resolved).toMatchObject({
|
||||
bindingId: bound.bindingId,
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
});
|
||||
expect(service.listBySession("agent:codex:acp:slack-dm")).toEqual([resolved]);
|
||||
|
||||
service.touch(bound.bindingId, 1234);
|
||||
expect(
|
||||
service.resolveByConversation({
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
})?.metadata,
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
label: "slack-dm",
|
||||
lastActivityAt: 1234,
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.unbind({
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
reason: "test cleanup",
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
bindingId: bound.bindingId,
|
||||
}),
|
||||
]);
|
||||
expect(
|
||||
service.resolveByConversation({
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("supports registered plugin channels through the generic current-conversation path", async () => {
|
||||
const service = getSessionBindingService();
|
||||
|
||||
expect(
|
||||
service.getCapabilities({
|
||||
channel: "msteams",
|
||||
accountId: "default",
|
||||
}),
|
||||
).toEqual({
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current"],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.bind({
|
||||
targetSessionKey: "agent:codex:acp:msteams-room",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "msteams",
|
||||
accountId: "default",
|
||||
conversationId: "19:chatid@thread.v2",
|
||||
},
|
||||
placement: "child",
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "BINDING_CAPABILITY_UNSUPPORTED",
|
||||
details: {
|
||||
channel: "msteams",
|
||||
accountId: "default",
|
||||
placement: "child",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.bind({
|
||||
targetSessionKey: "agent:codex:acp:msteams-room",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "msteams",
|
||||
accountId: "default",
|
||||
conversationId: "19:chatid@thread.v2",
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
conversation: {
|
||||
channel: "msteams",
|
||||
accountId: "default",
|
||||
conversationId: "19:chatid@thread.v2",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the first live adapter authoritative until it unregisters", () => {
|
||||
const firstBinding = {
|
||||
bindingId: "first-binding",
|
||||
|
|
|
|||
Loading…
Reference in New Issue