mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
test: use synthetic outbound binding fixtures
This commit is contained in:
@@ -18,10 +18,10 @@ function setMinimalCurrentConversationRegistry(): void {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "slack",
|
||||
pluginId: "workspace",
|
||||
source: "test",
|
||||
plugin: {
|
||||
id: "slack",
|
||||
id: "workspace",
|
||||
meta: { aliases: [] },
|
||||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
@@ -61,7 +61,7 @@ describe("generic current-conversation bindings", () => {
|
||||
it("advertises support only for channels that opt into current-conversation binds", () => {
|
||||
expect(
|
||||
getGenericCurrentConversationBindingCapabilities({
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -83,7 +83,7 @@ describe("generic current-conversation bindings", () => {
|
||||
|
||||
expect(
|
||||
getGenericCurrentConversationBindingCapabilities({
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
}),
|
||||
).toBeNull();
|
||||
@@ -91,36 +91,36 @@ describe("generic current-conversation bindings", () => {
|
||||
|
||||
it("reloads persisted bindings after the in-memory cache is cleared", async () => {
|
||||
const bound = await bindGenericCurrentConversation({
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
},
|
||||
metadata: {
|
||||
label: "slack-dm",
|
||||
label: "workspace-dm",
|
||||
},
|
||||
});
|
||||
|
||||
expect(bound).toMatchObject({
|
||||
bindingId: "generic:slack\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
bindingId: "generic:workspace\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
});
|
||||
|
||||
__testing.resetCurrentConversationBindingsForTests();
|
||||
|
||||
expect(
|
||||
resolveGenericCurrentConversationBinding({
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
}),
|
||||
).toMatchObject({
|
||||
bindingId: "generic:slack\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
bindingId: "generic:workspace\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
metadata: expect.objectContaining({
|
||||
label: "slack-dm",
|
||||
label: "workspace-dm",
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -134,18 +134,18 @@ describe("generic current-conversation bindings", () => {
|
||||
version: 1,
|
||||
bindings: [
|
||||
{
|
||||
bindingId: "generic:slack\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: " agent:codex:acp:slack-dm ",
|
||||
bindingId: "generic:workspace\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: " agent:codex:acp:workspace-dm ",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: 1234,
|
||||
metadata: {
|
||||
label: "slack-dm",
|
||||
label: "workspace-dm",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -153,32 +153,34 @@ describe("generic current-conversation bindings", () => {
|
||||
);
|
||||
|
||||
const resolved = resolveGenericCurrentConversationBinding({
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
});
|
||||
|
||||
expect(resolved).toMatchObject({
|
||||
bindingId: "generic:slack\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
bindingId: "generic:workspace\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
metadata: expect.objectContaining({
|
||||
label: "slack-dm",
|
||||
label: "workspace-dm",
|
||||
}),
|
||||
});
|
||||
expect(listGenericCurrentConversationBindingsBySession("agent:codex:acp:slack-dm")).toEqual([
|
||||
expect.objectContaining({
|
||||
bindingId: "generic:slack\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
}),
|
||||
]);
|
||||
expect(listGenericCurrentConversationBindingsBySession("agent:codex:acp:workspace-dm")).toEqual(
|
||||
[
|
||||
expect.objectContaining({
|
||||
bindingId: "generic:workspace\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
}),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it("drops self-parent conversation refs when storing generic current bindings", async () => {
|
||||
const bound = await bindGenericCurrentConversation({
|
||||
targetSessionKey: "agent:codex:acp:telegram-dm",
|
||||
targetSessionKey: "agent:codex:acp:forum-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
accountId: "default",
|
||||
conversationId: "6098642967",
|
||||
parentConversationId: "6098642967",
|
||||
@@ -186,9 +188,9 @@ describe("generic current-conversation bindings", () => {
|
||||
});
|
||||
|
||||
expect(bound).toMatchObject({
|
||||
bindingId: "generic:telegram\u241fdefault\u241f\u241f6098642967",
|
||||
bindingId: "generic:forum\u241fdefault\u241f\u241f6098642967",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
accountId: "default",
|
||||
conversationId: "6098642967",
|
||||
},
|
||||
@@ -196,13 +198,13 @@ describe("generic current-conversation bindings", () => {
|
||||
expect(bound?.conversation.parentConversationId).toBeUndefined();
|
||||
expect(
|
||||
resolveGenericCurrentConversationBinding({
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
accountId: "default",
|
||||
conversationId: "6098642967",
|
||||
}),
|
||||
).toMatchObject({
|
||||
bindingId: "generic:telegram\u241fdefault\u241f\u241f6098642967",
|
||||
targetSessionKey: "agent:codex:acp:telegram-dm",
|
||||
bindingId: "generic:forum\u241fdefault\u241f\u241f6098642967",
|
||||
targetSessionKey: "agent:codex:acp:forum-dm",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -215,11 +217,11 @@ describe("generic current-conversation bindings", () => {
|
||||
version: 1,
|
||||
bindings: [
|
||||
{
|
||||
bindingId: "generic:telegram\u241fdefault\u241f6098642967\u241f6098642967",
|
||||
targetSessionKey: "agent:codex:acp:telegram-dm",
|
||||
bindingId: "generic:forum\u241fdefault\u241f6098642967\u241f6098642967",
|
||||
targetSessionKey: "agent:codex:acp:forum-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
accountId: "default",
|
||||
conversationId: "6098642967",
|
||||
parentConversationId: "6098642967",
|
||||
@@ -227,7 +229,7 @@ describe("generic current-conversation bindings", () => {
|
||||
status: "active",
|
||||
boundAt: 1234,
|
||||
metadata: {
|
||||
label: "telegram-dm",
|
||||
label: "forum-dm",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -235,16 +237,16 @@ describe("generic current-conversation bindings", () => {
|
||||
);
|
||||
|
||||
const resolved = resolveGenericCurrentConversationBinding({
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
accountId: "default",
|
||||
conversationId: "6098642967",
|
||||
});
|
||||
|
||||
expect(resolved).toMatchObject({
|
||||
bindingId: "generic:telegram\u241fdefault\u241f\u241f6098642967",
|
||||
targetSessionKey: "agent:codex:acp:telegram-dm",
|
||||
bindingId: "generic:forum\u241fdefault\u241f\u241f6098642967",
|
||||
targetSessionKey: "agent:codex:acp:forum-dm",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
accountId: "default",
|
||||
conversationId: "6098642967",
|
||||
},
|
||||
@@ -258,14 +260,14 @@ describe("generic current-conversation bindings", () => {
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
bindingId: "generic:telegram\u241fdefault\u241f\u241f6098642967",
|
||||
bindingId: "generic:forum\u241fdefault\u241f\u241f6098642967",
|
||||
}),
|
||||
]);
|
||||
|
||||
__testing.resetCurrentConversationBindingsForTests();
|
||||
expect(
|
||||
resolveGenericCurrentConversationBinding({
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
accountId: "default",
|
||||
conversationId: "6098642967",
|
||||
}),
|
||||
@@ -301,22 +303,22 @@ describe("generic current-conversation bindings", () => {
|
||||
|
||||
it("persists touched activity across reloads", async () => {
|
||||
const bound = await bindGenericCurrentConversation({
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
},
|
||||
metadata: {
|
||||
label: "slack-dm",
|
||||
label: "workspace-dm",
|
||||
},
|
||||
});
|
||||
|
||||
expect(bound).not.toBeNull();
|
||||
|
||||
touchGenericCurrentConversationBinding(
|
||||
"generic:slack\u241fdefault\u241f\u241fuser:U123",
|
||||
"generic:workspace\u241fdefault\u241f\u241fuser:U123",
|
||||
1_234_567_890,
|
||||
);
|
||||
|
||||
@@ -324,13 +326,13 @@ describe("generic current-conversation bindings", () => {
|
||||
|
||||
expect(
|
||||
resolveGenericCurrentConversationBinding({
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
})?.metadata,
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
label: "slack-dm",
|
||||
label: "workspace-dm",
|
||||
lastActivityAt: 1_234_567_890,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -17,13 +17,13 @@ import {
|
||||
} from "./delivery-queue.test-helpers.js";
|
||||
|
||||
const stubCfg = {} as OpenClawConfig;
|
||||
const NO_LISTENER_ERROR = "No active WhatsApp Web listener";
|
||||
const NO_LISTENER_ERROR = "No active DirectChat listener";
|
||||
|
||||
function normalizeReconnectAccountIdForTest(accountId?: string | null): string {
|
||||
return (accountId ?? "").trim() || "default";
|
||||
}
|
||||
|
||||
async function drainWhatsAppReconnectPending(opts: {
|
||||
async function drainDirectChatReconnectPending(opts: {
|
||||
accountId: string;
|
||||
deliver: DeliverFn;
|
||||
log: RecoveryLogger;
|
||||
@@ -31,15 +31,15 @@ async function drainWhatsAppReconnectPending(opts: {
|
||||
}) {
|
||||
const normalizedAccountId = normalizeReconnectAccountIdForTest(opts.accountId);
|
||||
await drainPendingDeliveries({
|
||||
drainKey: `whatsapp:${normalizedAccountId}`,
|
||||
logLabel: "WhatsApp reconnect drain",
|
||||
drainKey: `directchat:${normalizedAccountId}`,
|
||||
logLabel: "DirectChat reconnect drain",
|
||||
cfg: stubCfg,
|
||||
log: opts.log,
|
||||
stateDir: opts.stateDir,
|
||||
deliver: opts.deliver,
|
||||
selectEntry: (entry) => ({
|
||||
match:
|
||||
entry.channel === "whatsapp" &&
|
||||
entry.channel === "directchat" &&
|
||||
normalizeReconnectAccountIdForTest(entry.accountId) === normalizedAccountId,
|
||||
bypassBackoff:
|
||||
typeof entry.lastError === "string" && entry.lastError.includes(NO_LISTENER_ERROR),
|
||||
@@ -53,20 +53,25 @@ function createTransientFailureDeliver(): DeliverFn {
|
||||
});
|
||||
}
|
||||
|
||||
async function enqueueFailedWhatsAppDelivery(params: {
|
||||
async function enqueueFailedDirectChatDelivery(params: {
|
||||
accountId: string;
|
||||
stateDir: string;
|
||||
error?: string;
|
||||
}): Promise<string> {
|
||||
const id = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: params.accountId },
|
||||
{
|
||||
channel: "directchat",
|
||||
to: "+1555",
|
||||
payloads: [{ text: "hi" }],
|
||||
accountId: params.accountId,
|
||||
},
|
||||
params.stateDir,
|
||||
);
|
||||
await failDelivery(id, params.error ?? NO_LISTENER_ERROR, params.stateDir);
|
||||
return id;
|
||||
}
|
||||
|
||||
describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
describe("drainPendingDeliveries for reconnect", () => {
|
||||
let tmpDir: string;
|
||||
const fixtures = installDeliveryQueueTmpDirHooks();
|
||||
|
||||
@@ -79,12 +84,12 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
const deliver = vi.fn<DeliverFn>(async () => {});
|
||||
|
||||
const id = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
await failDelivery(id, "No active WhatsApp Web listener", tmpDir);
|
||||
await failDelivery(id, NO_LISTENER_ERROR, tmpDir);
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -93,7 +98,7 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
expect(deliver).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ channel: "whatsapp", to: "+1555", skipQueue: true }),
|
||||
expect.objectContaining({ channel: "directchat", to: "+1555", skipQueue: true }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -102,12 +107,12 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
const deliver = vi.fn<DeliverFn>(async () => {});
|
||||
|
||||
const id = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "other" },
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "other" },
|
||||
tmpDir,
|
||||
);
|
||||
await failDelivery(id, "No active WhatsApp Web listener", tmpDir);
|
||||
await failDelivery(id, NO_LISTENER_ERROR, tmpDir);
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -122,7 +127,7 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
const log = createRecoveryLog();
|
||||
const deliver = createTransientFailureDeliver();
|
||||
|
||||
const id = await enqueueFailedWhatsAppDelivery({ accountId: "acct1", stateDir: tmpDir });
|
||||
const id = await enqueueFailedDirectChatDelivery({ accountId: "acct1", stateDir: tmpDir });
|
||||
const queueDir = path.join(tmpDir, "delivery-queue");
|
||||
const filePath = path.join(queueDir, `${id}.json`);
|
||||
const before = JSON.parse(fs.readFileSync(filePath, "utf-8")) as {
|
||||
@@ -131,7 +136,7 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
lastError?: string;
|
||||
};
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -155,11 +160,11 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
const log = createRecoveryLog();
|
||||
const deliver = createTransientFailureDeliver();
|
||||
|
||||
await enqueueFailedWhatsAppDelivery({ accountId: "acct1", stateDir: tmpDir });
|
||||
await enqueueFailedDirectChatDelivery({ accountId: "acct1", stateDir: tmpDir });
|
||||
|
||||
// Should not throw
|
||||
await expect(
|
||||
drainWhatsAppReconnectPending({
|
||||
drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -173,16 +178,16 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
const deliver = vi.fn<DeliverFn>(async () => {});
|
||||
|
||||
const id = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
|
||||
// Bump retryCount to MAX_RETRIES
|
||||
for (let i = 0; i < MAX_RETRIES; i++) {
|
||||
await failDelivery(id, "No active WhatsApp Web listener", tmpDir);
|
||||
await failDelivery(id, NO_LISTENER_ERROR, tmpDir);
|
||||
}
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -207,7 +212,7 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
});
|
||||
|
||||
await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
// Fail it so it matches the "no listener" filter
|
||||
@@ -219,16 +224,16 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
}
|
||||
const entryPath = path.join(tmpDir, "delivery-queue", pending);
|
||||
const entry = JSON.parse(fs.readFileSync(entryPath, "utf-8"));
|
||||
entry.lastError = "No active WhatsApp Web listener";
|
||||
entry.lastError = NO_LISTENER_ERROR;
|
||||
entry.retryCount = 1;
|
||||
fs.writeFileSync(entryPath, JSON.stringify(entry, null, 2));
|
||||
|
||||
const opts = { accountId: "acct1", log, stateDir: tmpDir, deliver };
|
||||
|
||||
// Start first drain (will block on deliver)
|
||||
const first = drainWhatsAppReconnectPending(opts);
|
||||
const first = drainDirectChatReconnectPending(opts);
|
||||
// Start second drain immediately — should be skipped
|
||||
const second = drainWhatsAppReconnectPending(opts);
|
||||
const second = drainDirectChatReconnectPending(opts);
|
||||
await second;
|
||||
|
||||
expect(log.info).toHaveBeenCalledWith(expect.stringContaining("already in progress"));
|
||||
@@ -250,7 +255,7 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
});
|
||||
|
||||
const id = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
const queuePath = path.join(tmpDir, "delivery-queue", `${id}.json`);
|
||||
@@ -264,7 +269,7 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
retryCount: number;
|
||||
lastError?: string;
|
||||
};
|
||||
entry.lastError = "No active WhatsApp Web listener";
|
||||
entry.lastError = NO_LISTENER_ERROR;
|
||||
fs.writeFileSync(queuePath, JSON.stringify(entry, null, 2));
|
||||
|
||||
const startupRecovery = recoverPendingDeliveries({
|
||||
@@ -278,7 +283,7 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -313,23 +318,23 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
{ channel: "demo-channel-a", to: "+1000", payloads: [{ text: "blocker" }] },
|
||||
tmpDir,
|
||||
);
|
||||
const whatsappId = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
const directChatId = await enqueueDelivery(
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
const queueDir = path.join(tmpDir, "delivery-queue");
|
||||
const blockerPath = path.join(queueDir, `${blockerId}.json`);
|
||||
const whatsappPath = path.join(queueDir, `${whatsappId}.json`);
|
||||
const directChatPath = path.join(queueDir, `${directChatId}.json`);
|
||||
const blockerEntry = JSON.parse(fs.readFileSync(blockerPath, "utf-8")) as {
|
||||
enqueuedAt: number;
|
||||
};
|
||||
const whatsappEntry = JSON.parse(fs.readFileSync(whatsappPath, "utf-8")) as {
|
||||
const directChatEntry = JSON.parse(fs.readFileSync(directChatPath, "utf-8")) as {
|
||||
enqueuedAt: number;
|
||||
};
|
||||
blockerEntry.enqueuedAt = 1;
|
||||
whatsappEntry.enqueuedAt = 2;
|
||||
directChatEntry.enqueuedAt = 2;
|
||||
fs.writeFileSync(blockerPath, JSON.stringify(blockerEntry, null, 2));
|
||||
fs.writeFileSync(whatsappPath, JSON.stringify(whatsappEntry, null, 2));
|
||||
fs.writeFileSync(directChatPath, JSON.stringify(directChatEntry, null, 2));
|
||||
|
||||
const startupRecovery = recoverPendingDeliveries({
|
||||
cfg: stubCfg,
|
||||
@@ -344,7 +349,7 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
);
|
||||
});
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -360,16 +365,16 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
expect.stringContaining("Recovery skipped for delivery"),
|
||||
);
|
||||
});
|
||||
it("drains fresh pending WhatsApp entries for the reconnecting account", async () => {
|
||||
it("drains fresh pending entries for the reconnecting account", async () => {
|
||||
const log = createRecoveryLog();
|
||||
const deliver = vi.fn<DeliverFn>(async () => {});
|
||||
|
||||
await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -382,12 +387,12 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("drains backoff-eligible WhatsApp retries on reconnect", async () => {
|
||||
it("drains backoff-eligible retries on reconnect", async () => {
|
||||
const log = createRecoveryLog();
|
||||
const deliver = vi.fn<DeliverFn>(async () => {});
|
||||
|
||||
const id = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
await failDelivery(id, "network down", tmpDir);
|
||||
@@ -398,7 +403,7 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
entry.lastAttemptAt = Date.now() - 30_000;
|
||||
fs.writeFileSync(entryPath, JSON.stringify(entry, null, 2));
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -413,12 +418,12 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
const deliver = vi.fn<DeliverFn>(async () => {});
|
||||
|
||||
const id = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
await failDelivery(id, "network down", tmpDir);
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -434,12 +439,12 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
const deliver = vi.fn<DeliverFn>(async () => {});
|
||||
|
||||
const id = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
{ channel: "directchat", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
await failDelivery(id, NO_LISTENER_ERROR, tmpDir);
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
@@ -449,16 +454,16 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("ignores non-WhatsApp entries even when reconnect drain runs", async () => {
|
||||
it("ignores other channels even when reconnect drain runs", async () => {
|
||||
const log = createRecoveryLog();
|
||||
const deliver = vi.fn<DeliverFn>(async () => {});
|
||||
|
||||
await enqueueDelivery(
|
||||
{ channel: "telegram", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
{ channel: "forum", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
|
||||
await drainWhatsAppReconnectPending({
|
||||
await drainDirectChatReconnectPending({
|
||||
accountId: "acct1",
|
||||
deliver,
|
||||
log,
|
||||
|
||||
@@ -21,7 +21,7 @@ describe("delivery-queue storage", () => {
|
||||
it("creates and removes a queue entry", async () => {
|
||||
const id = await enqueueTextDelivery(
|
||||
{
|
||||
channel: "whatsapp",
|
||||
channel: "directchat",
|
||||
to: "+1555",
|
||||
payloads: [{ text: "hello" }],
|
||||
bestEffort: true,
|
||||
@@ -48,7 +48,7 @@ describe("delivery-queue storage", () => {
|
||||
const entry = readQueuedEntry(tmpDir(), id);
|
||||
expect(entry).toMatchObject({
|
||||
id,
|
||||
channel: "whatsapp",
|
||||
channel: "directchat",
|
||||
to: "+1555",
|
||||
bestEffort: true,
|
||||
gifPlayback: true,
|
||||
@@ -80,18 +80,18 @@ describe("delivery-queue storage", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "ack cleans up leftover .delivered marker when .json is already gone",
|
||||
payload: { channel: "whatsapp", to: "+1", payloads: [{ text: "stale-marker" }] },
|
||||
payload: { channel: "directchat", to: "+1", payloads: [{ text: "stale-marker" }] },
|
||||
prepareDeliveredMarker: true,
|
||||
action: (id: string) => ackDelivery(id, tmpDir()),
|
||||
},
|
||||
{
|
||||
name: "ack removes .delivered marker so recovery does not replay",
|
||||
payload: { channel: "whatsapp", to: "+1", payloads: [{ text: "ack-test" }] },
|
||||
payload: { channel: "directchat", to: "+1", payloads: [{ text: "ack-test" }] },
|
||||
action: (id: string) => ackDelivery(id, tmpDir()),
|
||||
},
|
||||
{
|
||||
name: "loadPendingDeliveries cleans up stale .delivered markers without replaying",
|
||||
payload: { channel: "telegram", to: "99", payloads: [{ text: "stale" }] },
|
||||
payload: { channel: "forum", to: "99", payloads: [{ text: "stale" }] },
|
||||
prepareDeliveredMarker: true,
|
||||
action: () => loadPendingDeliveries(tmpDir()),
|
||||
expectedEntriesLength: 0,
|
||||
@@ -118,7 +118,7 @@ describe("delivery-queue storage", () => {
|
||||
it("increments retryCount, records attempt time, and sets lastError", async () => {
|
||||
const id = await enqueueTextDelivery(
|
||||
{
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
to: "123",
|
||||
payloads: [{ text: "test" }],
|
||||
},
|
||||
@@ -139,7 +139,7 @@ describe("delivery-queue storage", () => {
|
||||
it("moves entry to failed/ subdirectory", async () => {
|
||||
const id = await enqueueTextDelivery(
|
||||
{
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
to: "#general",
|
||||
payloads: [{ text: "hi" }],
|
||||
},
|
||||
@@ -160,8 +160,8 @@ describe("delivery-queue storage", () => {
|
||||
});
|
||||
|
||||
it("loads multiple entries", async () => {
|
||||
await enqueueTextDelivery({ channel: "whatsapp", to: "+1", payloads: [{ text: "a" }] });
|
||||
await enqueueTextDelivery({ channel: "telegram", to: "2", payloads: [{ text: "b" }] });
|
||||
await enqueueTextDelivery({ channel: "directchat", to: "+1", payloads: [{ text: "a" }] });
|
||||
await enqueueTextDelivery({ channel: "forum", to: "2", payloads: [{ text: "b" }] });
|
||||
|
||||
expect(await loadPendingDeliveries(tmpDir())).toHaveLength(2);
|
||||
});
|
||||
@@ -169,7 +169,7 @@ describe("delivery-queue storage", () => {
|
||||
it("persists gateway caller scopes for replay", async () => {
|
||||
const id = await enqueueTextDelivery(
|
||||
{
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
to: "2",
|
||||
payloads: [{ text: "b" }],
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
@@ -184,7 +184,7 @@ describe("delivery-queue storage", () => {
|
||||
it("persists session context for recovery replay", async () => {
|
||||
const id = await enqueueTextDelivery(
|
||||
{
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
to: "2",
|
||||
payloads: [{ text: "b" }],
|
||||
session: {
|
||||
@@ -214,7 +214,7 @@ describe("delivery-queue storage", () => {
|
||||
|
||||
it("backfills lastAttemptAt for legacy retry entries during load", async () => {
|
||||
const id = await enqueueTextDelivery({
|
||||
channel: "whatsapp",
|
||||
channel: "directchat",
|
||||
to: "+1",
|
||||
payloads: [{ text: "legacy" }],
|
||||
});
|
||||
|
||||
@@ -26,10 +26,10 @@ function setMinimalCurrentConversationRegistry(): void {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "slack",
|
||||
pluginId: "workspace",
|
||||
source: "test",
|
||||
plugin: {
|
||||
id: "slack",
|
||||
id: "workspace",
|
||||
meta: { aliases: [] },
|
||||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
@@ -37,10 +37,10 @@ function setMinimalCurrentConversationRegistry(): void {
|
||||
},
|
||||
},
|
||||
{
|
||||
pluginId: "msteams",
|
||||
pluginId: "teamchat",
|
||||
source: "test",
|
||||
plugin: {
|
||||
id: "msteams",
|
||||
id: "teamchat",
|
||||
meta: { aliases: [] },
|
||||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
@@ -251,12 +251,12 @@ describe("session binding service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to generic current-conversation bindings for built-in channels", async () => {
|
||||
it("falls back to generic current-conversation bindings for registered channels", async () => {
|
||||
const service = getSessionBindingService();
|
||||
|
||||
expect(
|
||||
service.getCapabilities({
|
||||
channel: "Slack",
|
||||
channel: "Workspace",
|
||||
accountId: " DEFAULT ",
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -267,62 +267,62 @@ describe("session binding service", () => {
|
||||
});
|
||||
|
||||
const bound = await service.bind({
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: " Slack ",
|
||||
channel: " Workspace ",
|
||||
accountId: " DEFAULT ",
|
||||
conversationId: " user:U123 ",
|
||||
},
|
||||
metadata: {
|
||||
label: "slack-dm",
|
||||
label: "workspace-dm",
|
||||
},
|
||||
ttlMs: 60_000,
|
||||
});
|
||||
|
||||
expect(bound).toMatchObject({
|
||||
bindingId: "generic:slack\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
bindingId: "generic:workspace\u241fdefault\u241f\u241fuser:U123",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
},
|
||||
status: "active",
|
||||
metadata: expect.objectContaining({
|
||||
label: "slack-dm",
|
||||
label: "workspace-dm",
|
||||
}),
|
||||
});
|
||||
|
||||
const resolved = service.resolveByConversation({
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
});
|
||||
expect(resolved).toMatchObject({
|
||||
bindingId: bound.bindingId,
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
});
|
||||
expect(service.listBySession("agent:codex:acp:slack-dm")).toEqual([resolved]);
|
||||
expect(service.listBySession("agent:codex:acp:workspace-dm")).toEqual([resolved]);
|
||||
|
||||
service.touch(bound.bindingId, 1234);
|
||||
expect(
|
||||
service.resolveByConversation({
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
})?.metadata,
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
label: "slack-dm",
|
||||
label: "workspace-dm",
|
||||
lastActivityAt: 1234,
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.unbind({
|
||||
targetSessionKey: "agent:codex:acp:slack-dm",
|
||||
targetSessionKey: "agent:codex:acp:workspace-dm",
|
||||
reason: "test cleanup",
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
@@ -332,7 +332,7 @@ describe("session binding service", () => {
|
||||
]);
|
||||
expect(
|
||||
service.resolveByConversation({
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
accountId: "default",
|
||||
conversationId: "user:U123",
|
||||
}),
|
||||
@@ -344,7 +344,7 @@ describe("session binding service", () => {
|
||||
|
||||
expect(
|
||||
service.getCapabilities({
|
||||
channel: "msteams",
|
||||
channel: "teamchat",
|
||||
accountId: "default",
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -356,10 +356,10 @@ describe("session binding service", () => {
|
||||
|
||||
await expect(
|
||||
service.bind({
|
||||
targetSessionKey: "agent:codex:acp:msteams-room",
|
||||
targetSessionKey: "agent:codex:acp:teamchat-room",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "msteams",
|
||||
channel: "teamchat",
|
||||
accountId: "default",
|
||||
conversationId: "19:chatid@thread.v2",
|
||||
},
|
||||
@@ -368,7 +368,7 @@ describe("session binding service", () => {
|
||||
).rejects.toMatchObject({
|
||||
code: "BINDING_CAPABILITY_UNSUPPORTED",
|
||||
details: {
|
||||
channel: "msteams",
|
||||
channel: "teamchat",
|
||||
accountId: "default",
|
||||
placement: "child",
|
||||
},
|
||||
@@ -376,17 +376,17 @@ describe("session binding service", () => {
|
||||
|
||||
await expect(
|
||||
service.bind({
|
||||
targetSessionKey: "agent:codex:acp:msteams-room",
|
||||
targetSessionKey: "agent:codex:acp:teamchat-room",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "msteams",
|
||||
channel: "teamchat",
|
||||
accountId: "default",
|
||||
conversationId: "19:chatid@thread.v2",
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
conversation: {
|
||||
channel: "msteams",
|
||||
channel: "teamchat",
|
||||
accountId: "default",
|
||||
conversationId: "19:chatid@thread.v2",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user