fix(ci): restore channel approval and lifecycle harnesses

This commit is contained in:
Peter Steinberger
2026-04-03 19:14:31 +01:00
parent d20e3d5691
commit 53f8c2047a
6 changed files with 71 additions and 62 deletions

View File

@@ -7,14 +7,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { peekSystemEvents, resetSystemEventsForTest } from "../../../../src/infra/system-events.js";
import { expectPairingReplyText } from "../../../../test/helpers/pairing-reply.js";
import {
enqueueSystemEventMock,
readAllowFromStoreMock,
resetDiscordComponentRuntimeMocks,
upsertPairingRequestMock,
} from "../test-support/component-runtime.js";
import {
resolveAgentComponentRoute,
resolveComponentInteractionContext,
} from "./agent-components-helpers.js";
import { resolveComponentInteractionContext } from "./agent-components-helpers.js";
import { createAgentComponentButton, createAgentSelectMenu } from "./agent-components.js";
describe("agent components", () => {
@@ -32,31 +30,6 @@ describe("agent components", () => {
});
const createCfg = (): OpenClawConfig => ({}) as OpenClawConfig;
const resolvedDefaultDmSessionKey = () =>
resolveAgentComponentRoute({
ctx: { cfg: createCfg(), accountId: "default" },
rawGuildId: undefined,
memberRoleIds: [],
isDirectMessage: true,
isGroupDm: false,
userId: "123456789",
channelId: "dm-channel",
parentId: undefined,
}).sessionKey;
const resolvedDefaultGroupDmSessionKey = () =>
resolveAgentComponentRoute({
ctx: { cfg: createCfg(), accountId: "default" },
rawGuildId: undefined,
memberRoleIds: [],
isDirectMessage: false,
isGroupDm: true,
userId: "123456789",
channelId: "group-dm-channel",
parentId: undefined,
}).sessionKey;
const createBaseDmInteraction = (overrides: Record<string, unknown> = {}) => {
const reply = vi.fn().mockResolvedValue(undefined);
const defer = vi.fn().mockResolvedValue(undefined);
@@ -208,8 +181,8 @@ describe("agent components", () => {
content: "You are not authorized to use this button.",
ephemeral: true,
});
expect(peekSystemEvents(resolvedDefaultGroupDmSessionKey())).toEqual([]);
expect(peekSystemEvents(resolvedDefaultDmSessionKey())).toEqual([]);
expect(peekSystemEvents(defaultGroupDmSessionKey)).toEqual([]);
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
});
@@ -231,10 +204,13 @@ describe("agent components", () => {
expect(defer).not.toHaveBeenCalled();
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
expect(peekSystemEvents(resolvedDefaultGroupDmSessionKey())).toEqual([
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
"[Discord component: hello clicked by Alice#1234 (123456789)]",
]);
expect(peekSystemEvents(resolvedDefaultDmSessionKey())).toEqual([]);
expect.objectContaining({
sessionKey: defaultGroupDmSessionKey,
}),
);
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
});
@@ -251,9 +227,12 @@ describe("agent components", () => {
expect(defer).not.toHaveBeenCalled();
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
expect(peekSystemEvents(resolvedDefaultDmSessionKey())).toEqual([
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
"[Discord component: hello clicked by Alice#1234 (123456789)]",
]);
expect.objectContaining({
sessionKey: defaultDmSessionKey,
}),
);
expect(upsertPairingRequestMock).not.toHaveBeenCalled();
expect(readAllowFromStoreMock).toHaveBeenCalledWith("discord", "default");
});
@@ -271,9 +250,12 @@ describe("agent components", () => {
expect(defer).not.toHaveBeenCalled();
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
expect(peekSystemEvents(resolvedDefaultDmSessionKey())).toEqual([
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
"[Discord component: hello clicked by Alice#1234 (123456789)]",
]);
expect.objectContaining({
sessionKey: defaultDmSessionKey,
}),
);
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
});
@@ -293,7 +275,7 @@ describe("agent components", () => {
content: "DM interactions are disabled.",
ephemeral: true,
});
expect(peekSystemEvents(resolvedDefaultDmSessionKey())).toEqual([]);
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
});
@@ -311,9 +293,12 @@ describe("agent components", () => {
expect(defer).not.toHaveBeenCalled();
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
expect(peekSystemEvents(resolvedDefaultDmSessionKey())).toEqual([
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
"[Discord select menu: hello interacted by Alice#1234 (123456789) (selected: alpha)]",
]);
expect.objectContaining({
sessionKey: defaultDmSessionKey,
}),
);
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
});
@@ -330,9 +315,12 @@ describe("agent components", () => {
expect(defer).not.toHaveBeenCalled();
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
expect(peekSystemEvents(resolvedDefaultDmSessionKey())).toEqual([
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
"[Discord component: hello_cid clicked by Alice#1234 (123456789)]",
]);
expect.objectContaining({
sessionKey: defaultDmSessionKey,
}),
);
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
});
@@ -349,9 +337,12 @@ describe("agent components", () => {
expect(defer).not.toHaveBeenCalled();
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
expect(peekSystemEvents(resolvedDefaultDmSessionKey())).toEqual([
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
"[Discord component: hello%2G clicked by Alice#1234 (123456789)]",
]);
expect.objectContaining({
sessionKey: defaultDmSessionKey,
}),
);
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
});
});

View File

@@ -33,11 +33,11 @@ function resolveMatrixExecApprovalConfig(params: {
const account = resolveMatrixAccount(params);
const config = account.config.execApprovals;
if (!config) {
return { enabled: false } as const;
return undefined;
}
return {
...config,
enabled: account.enabled && account.configured && config.enabled === true,
enabled: account.enabled && account.configured ? config.enabled : false,
};
}
@@ -46,7 +46,7 @@ function countMatrixExecApprovalEligibleAccounts(params: {
request: ApprovalRequest;
}): number {
return listMatrixAccountIds(params.cfg).filter((accountId) => {
const account = resolveMatrixAccount({ cfg, accountId });
const account = resolveMatrixAccount({ cfg: params.cfg, accountId });
if (!account.enabled || !account.configured) {
return false;
}
@@ -56,13 +56,13 @@ function countMatrixExecApprovalEligibleAccounts(params: {
});
return (
isChannelExecApprovalClientEnabledFromConfig({
enabled: config.enabled,
enabled: config?.enabled,
approverCount: getMatrixExecApprovalApprovers({ cfg: params.cfg, accountId }).length,
}) &&
matchesApprovalRequestFilters({
request: params.request.request,
agentFilter: config.agentFilter,
sessionFilter: config.sessionFilter,
agentFilter: config?.agentFilter,
sessionFilter: config?.sessionFilter,
})
);
}).length;

View File

@@ -107,10 +107,12 @@ describe("mattermost websocket monitor", () => {
});
it("retries when first attempt errors before open and next attempt succeeds", async () => {
const abort = new AbortController();
const reconnectDelays: number[] = [];
const onError = vi.fn();
const patches: Array<Record<string, unknown>> = [];
const sockets: FakeWebSocket[] = [];
let disconnects = 0;
const connectOnce = createMattermostConnectOnce({
wsUrl: "wss://example.invalid/api/v4/websocket",
@@ -121,8 +123,15 @@ describe("mattermost websocket monitor", () => {
return () => seq++;
})(),
onPosted: async () => {},
abortSignal: abort.signal,
statusSink: (patch) => {
patches.push(patch as Record<string, unknown>);
if (patch.lastDisconnect) {
disconnects++;
if (disconnects >= 2) {
abort.abort();
}
}
},
webSocketFactory: () => {
const socket = new FakeWebSocket();
@@ -135,23 +144,19 @@ describe("mattermost websocket monitor", () => {
return;
}
socket.emitOpen();
socket.emitClose(1000);
});
return socket;
},
});
const run = runWithReconnect(connectOnce, {
await runWithReconnect(connectOnce, {
abortSignal: abort.signal,
initialDelayMs: 1,
onError,
onReconnect: (delay) => reconnectDelays.push(delay),
shouldReconnect: ({ attempt, outcome }) => outcome === "rejected" && attempt === 0,
});
await vi.waitFor(() => expect(sockets).toHaveLength(2));
await vi.waitFor(() => expect(sockets[1]?.sent).toHaveLength(1));
sockets[1]?.emitClose(1000);
await run;
expect(sockets).toHaveLength(2);
expect(sockets[0].closeCalls).toBe(1);
expect(sockets[1].sent).toHaveLength(1);

View File

@@ -39,7 +39,7 @@ export function resolveTelegramExecApprovalConfig(params: {
}
return {
...config,
enabled: account.enabled && account.tokenSource !== "none" && config.enabled === true,
enabled: account.enabled && account.tokenSource !== "none" ? config.enabled : false,
};
}

View File

@@ -5,7 +5,6 @@ import {
createImageUpdate,
createLifecycleMonitorSetup,
expectImageLifecycleDelivery,
settleAsyncWork,
} from "../test-support/lifecycle-test-support.js";
import {
getUpdatesMock,
@@ -96,12 +95,11 @@ describe("Zalo polling image handling", () => {
abortSignal: abort.signal,
});
await settleAsyncWork();
await vi.waitFor(() => expect(sendMessageMock).toHaveBeenCalledTimes(1));
expect(fetchRemoteMediaMock).not.toHaveBeenCalled();
expect(saveMediaBufferMock).not.toHaveBeenCalled();
expect(finalizeInboundContextMock).not.toHaveBeenCalled();
expect(recordInboundSessionMock).not.toHaveBeenCalled();
expect(sendMessageMock).toHaveBeenCalledTimes(1);
abort.abort();
await run;

View File

@@ -122,8 +122,21 @@ export function createImageLifecycleCore() {
path: "/tmp/zalo-photo.jpg",
contentType: "image/jpeg",
}));
const readAllowFromStoreMock = vi.fn(async () => [] as string[]);
const upsertPairingRequestMock = vi.fn(async () => ({ code: "PAIRCODE", created: true }));
const core = {
logging: {
shouldLogVerbose: vi.fn(
() => false,
) as unknown as PluginRuntime["logging"]["shouldLogVerbose"],
},
channel: {
pairing: {
readAllowFromStore:
readAllowFromStoreMock as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
upsertPairingRequest:
upsertPairingRequestMock as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
},
routing: {
resolveAgentRoute: vi.fn(() => ({
agentId: "main",
@@ -184,6 +197,8 @@ export function createImageLifecycleCore() {
recordInboundSessionMock,
fetchRemoteMediaMock,
saveMediaBufferMock,
readAllowFromStoreMock,
upsertPairingRequestMock,
};
}