test: use synthetic outbound binding fixtures

This commit is contained in:
Peter Steinberger
2026-04-20 23:29:43 +01:00
parent 7e8b58cb25
commit f6c9912e37
4 changed files with 145 additions and 138 deletions

View File

@@ -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,
}),
);

View File

@@ -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,

View File

@@ -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" }],
});

View File

@@ -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",
},