Restore channel test module rebinding

This commit is contained in:
Tak Hoffman
2026-03-27 22:09:36 -05:00
parent 22c9be197e
commit 3ccc58ae29
6 changed files with 109 additions and 87 deletions

View File

@@ -1,4 +1,4 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resetInboundDedupe } from "../../../src/auto-reply/reply/inbound-dedupe.js";
import {
flush,
@@ -71,11 +71,9 @@ beforeEach(() => {
resetInboundDedupe();
});
beforeAll(async () => {
({ monitorSlackProvider } = await import("./monitor.js"));
});
beforeEach(async () => {
vi.resetModules();
({ monitorSlackProvider } = await import("./monitor.js"));
resetInboundDedupe();
resetSlackTestState({
messages: { responsePrefix: "PFX" },

View File

@@ -1,4 +1,4 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { expectPairingReplyText } from "../../../test/helpers/pairing-reply.js";
import {
defaultSlackTestConfig,
@@ -21,14 +21,12 @@ let monitorSlackProvider: typeof import("./monitor.js").monitorSlackProvider;
const slackTestState = getSlackTestState();
const { sendMock, replyMock, reactMock, upsertPairingRequestMock } = slackTestState;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ resetInboundDedupe } = await import("../../../src/auto-reply/reply/inbound-dedupe.js"));
({ HISTORY_CONTEXT_MARKER } = await import("../../../src/auto-reply/reply/history.js"));
({ CURRENT_MESSAGE_MARKER } = await import("../../../src/auto-reply/reply/mentions.js"));
({ monitorSlackProvider } = await import("./monitor.js"));
});
beforeEach(() => {
resetInboundDedupe();
resetSlackTestState(defaultSlackTestConfig());
});

View File

@@ -1,4 +1,4 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { SlackMonitorContext } from "./context.js";
const readStoreAllowFromForDmPolicyMock = vi.hoisted(() => vi.fn());
@@ -25,12 +25,10 @@ function makeSlackCtx(allowFrom: string[]): SlackMonitorContext {
describe("resolveSlackEffectiveAllowFrom", () => {
const prevTtl = process.env.OPENCLAW_SLACK_PAIRING_ALLOWFROM_CACHE_TTL_MS;
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ clearSlackAllowFromCacheForTest, resolveSlackEffectiveAllowFrom } =
await import("./auth.js"));
});
beforeEach(() => {
readStoreAllowFromForDmPolicyMock.mockReset();
clearSlackAllowFromCacheForTest();
if (prevTtl === undefined) {

View File

@@ -1,4 +1,4 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const prepareSlackMessageMock =
vi.fn<
@@ -117,11 +117,9 @@ async function createInFlightMessageScenario(ts: string) {
}
describe("createSlackMessageHandler app_mention race handling", () => {
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
({ createSlackMessageHandler } = await import("./message-handler.js"));
});
beforeEach(() => {
prepareSlackMessageMock.mockReset();
dispatchPreparedSlackMessageMock.mockReset();
});

View File

@@ -1,34 +1,32 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as gatewayRuntimeModule from "./gateway-runtime.js";
import {
dispatchReplyWithBufferedBlockDispatcher,
finalizeInboundContextMock,
registerPluginHttpRouteMock,
resolveAgentRouteMock,
} from "./channel.test-mocks.js";
import { makeFormBody, makeReq, makeRes } from "./test-http-utils.js";
const registerSynologyWebhookRouteMock = vi
.spyOn(gatewayRuntimeModule, "registerSynologyWebhookRoute")
.mockImplementation(() => vi.fn());
const freshChannelModulePath = "./channel.js?channel-integration-test";
const { createSynologyChatPlugin } = await import(freshChannelModulePath);
async function expectPendingStartAccountPromise(
result: Promise<unknown>,
abortController: AbortController,
) {
expect(result).toBeInstanceOf(Promise);
const resolved = await Promise.race([
result,
new Promise((r) => setTimeout(() => r("pending"), 50)),
]);
expect(resolved).toBe("pending");
abortController.abort();
await result;
}
type RegisteredRoute = {
path: string;
accountId: string;
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
};
let createSynologyChatPlugin: typeof import("./channel.js").createSynologyChatPlugin;
const freshChannelModulePath: string = "./channel.js?channel-integration-test";
describe("Synology channel wiring integration", () => {
beforeEach(() => {
registerSynologyWebhookRouteMock.mockClear();
registerSynologyWebhookRouteMock.mockImplementation(() => vi.fn());
beforeEach(async () => {
vi.resetModules();
({ createSynologyChatPlugin } = await import(freshChannelModulePath));
registerPluginHttpRouteMock.mockClear();
dispatchReplyWithBufferedBlockDispatcher.mockClear();
finalizeInboundContextMock.mockClear();
resolveAgentRouteMock.mockClear();
});
it("registers the gateway route with resolved named-account config", async () => {
it("registers real webhook handler with resolved account config and enforces allowlist", async () => {
const plugin = createSynologyChatPlugin();
const abortController = new AbortController();
const ctx = {
@@ -55,28 +53,35 @@ describe("Synology channel wiring integration", () => {
};
const started = plugin.gateway.startAccount(ctx);
expect(registerSynologyWebhookRouteMock).toHaveBeenCalledTimes(1);
expect(registerPluginHttpRouteMock).toHaveBeenCalledTimes(1);
const firstCall = registerSynologyWebhookRouteMock.mock.calls[0];
const firstCall = registerPluginHttpRouteMock.mock.calls[0];
expect(firstCall).toBeTruthy();
if (!firstCall) throw new Error("Expected registerSynologyWebhookRoute to be called");
if (!firstCall) throw new Error("Expected registerPluginHttpRoute to be called");
const registered = firstCall[0];
expect(registered.path).toBe("/webhook/synology-alerts");
expect(registered.accountId).toBe("alerts");
expect(registered.account).toMatchObject({
accountId: "alerts",
token: "valid-token",
incomingUrl: "https://nas.example.com/incoming",
webhookPath: "/webhook/synology-alerts",
webhookPathSource: "explicit",
dmPolicy: "allowlist",
allowedUserIds: ["456"],
});
await expectPendingStartAccountPromise(started, abortController);
const req = makeReq(
"POST",
makeFormBody({
token: "valid-token",
user_id: "123",
username: "unauthorized-user",
text: "Hello",
}),
);
const res = makeRes();
await registered.handler(req, res);
expect(res._status).toBe(403);
expect(res._body).toContain("not authorized");
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
abortController.abort();
await started;
});
it("passes distinct resolved accounts for separate named-account starts", async () => {
it("isolates same user_id across different accounts", async () => {
const plugin = createSynologyChatPlugin();
const alphaAbortController = new AbortController();
const betaAbortController = new AbortController();
@@ -90,14 +95,14 @@ describe("Synology channel wiring integration", () => {
token: "token-alpha",
incomingUrl: "https://nas.example.com/incoming-alpha",
webhookPath: "/webhook/synology-alpha",
dmPolicy: "open" as const,
dmPolicy: "open",
},
beta: {
enabled: true,
token: "token-beta",
incomingUrl: "https://nas.example.com/incoming-beta",
webhookPath: "/webhook/synology-beta",
dmPolicy: "open" as const,
dmPolicy: "open",
},
},
},
@@ -120,33 +125,51 @@ describe("Synology channel wiring integration", () => {
abortSignal: betaAbortController.signal,
});
expect(registerSynologyWebhookRouteMock).toHaveBeenCalledTimes(2);
const alphaCall = registerSynologyWebhookRouteMock.mock.calls[0]?.[0];
const betaCall = registerSynologyWebhookRouteMock.mock.calls[1]?.[0];
if (!alphaCall || !betaCall) {
expect(registerPluginHttpRouteMock).toHaveBeenCalledTimes(2);
const alphaRoute = registerPluginHttpRouteMock.mock.calls[0]?.[0];
const betaRoute = registerPluginHttpRouteMock.mock.calls[1]?.[0];
if (!alphaRoute || !betaRoute) {
throw new Error("Expected both Synology Chat routes to register");
}
expect(alphaCall).toMatchObject({
accountId: "alpha",
account: {
accountId: "alpha",
const alphaReq = makeReq(
"POST",
makeFormBody({
token: "token-alpha",
incomingUrl: "https://nas.example.com/incoming-alpha",
webhookPath: "/webhook/synology-alpha",
webhookPathSource: "explicit",
},
});
expect(betaCall).toMatchObject({
accountId: "beta",
account: {
accountId: "beta",
user_id: "123",
username: "alice",
text: "alpha secret",
}),
);
const alphaRes = makeRes();
await alphaRoute.handler(alphaReq, alphaRes);
const betaReq = makeReq(
"POST",
makeFormBody({
token: "token-beta",
incomingUrl: "https://nas.example.com/incoming-beta",
webhookPath: "/webhook/synology-beta",
webhookPathSource: "explicit",
},
user_id: "123",
username: "bob",
text: "beta secret",
}),
);
const betaRes = makeRes();
await betaRoute.handler(betaReq, betaRes);
expect(alphaRes._status).toBe(204);
expect(betaRes._status).toBe(204);
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(2);
expect(finalizeInboundContextMock).toHaveBeenCalledTimes(2);
const alphaCtx = finalizeInboundContextMock.mock.calls[0]?.[0];
const betaCtx = finalizeInboundContextMock.mock.calls[1]?.[0];
expect(alphaCtx).toMatchObject({
AccountId: "alpha",
SessionKey: "agent:agent-alpha:synology-chat:alpha:direct:123",
});
expect(betaCtx).toMatchObject({
AccountId: "beta",
SessionKey: "agent:agent-beta:synology-chat:beta:direct:123",
});
alphaAbortController.abort();

View File

@@ -14,11 +14,12 @@ vi.mock("node:http", () => {
return { default: { request: mockRequest, get: mockGet }, request: mockRequest, get: mockGet };
});
// Import after mocks are set up
const { sendMessage, sendFileUrl, fetchChatUsers, resolveLegacyWebhookNameToChatUserId } =
await import("./client.js");
const https = await import("node:https");
let fakeNowMs = 1_700_000_000_000;
let sendMessage: typeof import("./client.js").sendMessage;
let sendFileUrl: typeof import("./client.js").sendFileUrl;
let fetchChatUsers: typeof import("./client.js").fetchChatUsers;
let resolveLegacyWebhookNameToChatUserId: typeof import("./client.js").resolveLegacyWebhookNameToChatUserId;
async function settleTimers<T>(promise: Promise<T>): Promise<T> {
await Promise.resolve();
@@ -55,6 +56,7 @@ function mockFailureResponse(statusCode = 500) {
function installFakeTimerHarness() {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
vi.useFakeTimers();
fakeNowMs += 10_000;
vi.setSystemTime(fakeNowMs);
@@ -63,6 +65,11 @@ function installFakeTimerHarness() {
afterEach(() => {
vi.useRealTimers();
});
beforeEach(async () => {
({ sendMessage, sendFileUrl, fetchChatUsers, resolveLegacyWebhookNameToChatUserId } =
await import("./client.js"));
});
}
describe("sendMessage", () => {