test: stabilize extension mocks for ci shards

This commit is contained in:
Peter Steinberger
2026-03-27 22:39:30 +00:00
parent c52f89bd60
commit 90c50fd9d8
3 changed files with 89 additions and 36 deletions

View File

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { createToolFactoryHarness, type ToolLike } from "./tool-factory-test-harness.js";
const createFeishuClientMock = vi.hoisted(() => vi.fn());
const resolveFeishuToolAccountMock = vi.hoisted(() => vi.fn());
const fetchRemoteMediaMock = vi.hoisted(() => vi.fn());
const loadWebMediaMock = vi.hoisted(() => vi.fn());
const convertMock = vi.hoisted(() => vi.fn());
@@ -15,25 +16,44 @@ const driveUploadAllMock = vi.hoisted(() => vi.fn());
const permissionMemberCreateMock = vi.hoisted(() => vi.fn());
const blockPatchMock = vi.hoisted(() => vi.fn());
const scopeListMock = vi.hoisted(() => vi.fn());
const toolAccountModule = await import("./tool-account.js");
const runtimeModule = await import("./runtime.js");
vi.mock("./client.js", () => ({
createFeishuClient: createFeishuClientMock,
}));
vi.mock("./runtime.js", () => ({
getFeishuRuntime: () => ({
channel: {
media: {
fetchRemoteMedia: fetchRemoteMediaMock,
vi.spyOn(toolAccountModule, "createFeishuToolClient").mockImplementation(() =>
createFeishuClientMock(),
);
vi.spyOn(toolAccountModule, "resolveAnyEnabledFeishuToolsConfig").mockReturnValue({
doc: true,
chat: false,
wiki: false,
drive: false,
perm: false,
scopes: false,
});
vi.spyOn(toolAccountModule, "resolveFeishuToolAccount").mockImplementation((...args) =>
resolveFeishuToolAccountMock(...args),
);
vi.spyOn(runtimeModule, "getFeishuRuntime").mockImplementation(
() =>
({
channel: {
media: {
fetchRemoteMedia: fetchRemoteMediaMock,
saveMediaBuffer: vi.fn(),
},
},
},
media: {
loadWebMedia: loadWebMediaMock,
},
}),
}));
media: {
loadWebMedia: loadWebMediaMock,
detectMime: vi.fn(async () => "application/octet-stream"),
mediaKindFromMime: vi.fn(() => "image"),
isVoiceCompatibleAudio: vi.fn(() => false),
getImageMetadata: vi.fn(async () => null),
resizeToJpeg: vi.fn(async () => Buffer.alloc(0)),
},
}) as unknown as ReturnType<typeof runtimeModule.getFeishuRuntime>,
);
import { registerFeishuDocTools } from "./docx.js";
const { registerFeishuDocTools } = await import("./docx.js");
type ToolResultWithDetails = {
details: Record<string, unknown>;
@@ -76,6 +96,9 @@ describe("feishu_doc image fetch hardening", () => {
},
},
});
resolveFeishuToolAccountMock.mockReturnValue({
config: { mediaMaxMb: 30 },
});
convertMock.mockResolvedValue({
code: 0,

View File

@@ -1,17 +1,47 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { makeSecurityAccount, registerPluginHttpRouteMock } from "./channel.test-mocks.js";
import { sendMessage } from "./client.js";
import type { ResolvedSynologyChatAccount } from "./types.js";
function makeSecurityAccount(
overrides: Partial<ResolvedSynologyChatAccount> = {},
): ResolvedSynologyChatAccount {
return {
accountId: "default",
enabled: true,
token: "t",
incomingUrl: "https://nas/incoming",
nasHost: "h",
webhookPath: "/w",
webhookPathSource: "default" as const,
dangerouslyAllowNameMatching: false,
dangerouslyAllowInheritedWebhookPath: false,
dmPolicy: "allowlist" as const,
allowedUserIds: [],
rateLimitPerMinute: 30,
botName: "Bot",
allowInsecureSsl: false,
...overrides,
};
}
const clientModule = await import("./client.js");
const gatewayRuntimeModule = await import("./gateway-runtime.js");
const mockSendMessage = vi.spyOn(clientModule, "sendMessage").mockResolvedValue(true);
const registerSynologyWebhookRouteMock = vi
.spyOn(gatewayRuntimeModule, "registerSynologyWebhookRoute")
.mockImplementation(() => vi.fn());
vi.mock("./webhook-handler.js", () => ({
createWebhookHandler: vi.fn(() => vi.fn()),
}));
const { createSynologyChatPlugin } = await import("./channel.js");
const mockSendMessage = vi.mocked(sendMessage);
describe("createSynologyChatPlugin", () => {
beforeEach(() => {
mockSendMessage.mockClear();
registerSynologyWebhookRouteMock.mockClear();
mockSendMessage.mockResolvedValue(true);
registerSynologyWebhookRouteMock.mockImplementation(() => vi.fn());
});
describe("meta", () => {
@@ -442,7 +472,7 @@ describe("createSynologyChatPlugin", () => {
});
it("startAccount refuses allowlist accounts with empty allowedUserIds", async () => {
const registerMock = registerPluginHttpRouteMock;
const registerMock = registerSynologyWebhookRouteMock;
registerMock.mockClear();
const plugin = createSynologyChatPlugin();
const { ctx, abortController } = makeStartAccountCtx({
@@ -460,7 +490,7 @@ describe("createSynologyChatPlugin", () => {
});
it("startAccount refuses named accounts without explicit webhookPath in multi-account setups", async () => {
const registerMock = registerPluginHttpRouteMock;
const registerMock = registerSynologyWebhookRouteMock;
const plugin = createSynologyChatPlugin();
const { ctx, abortController } = makeNamedStartAccountCtx({
dmPolicy: "allowlist",
@@ -476,7 +506,7 @@ describe("createSynologyChatPlugin", () => {
});
it("startAccount refuses duplicate exact webhook paths across accounts", async () => {
const registerMock = registerPluginHttpRouteMock;
const registerMock = registerSynologyWebhookRouteMock;
const plugin = createSynologyChatPlugin();
const { ctx, abortController } = makeNamedStartAccountCtx({
webhookPath: "/webhook/synology-shared",
@@ -491,10 +521,10 @@ describe("createSynologyChatPlugin", () => {
expect(registerMock).not.toHaveBeenCalled();
});
it("deregisters stale route before re-registering same account/path", async () => {
it("re-registers same account/path through the route registrar", async () => {
const unregisterFirst = vi.fn();
const unregisterSecond = vi.fn();
const registerMock = registerPluginHttpRouteMock;
const registerMock = registerSynologyWebhookRouteMock;
registerMock.mockReturnValueOnce(unregisterFirst).mockReturnValueOnce(unregisterSecond);
const plugin = createSynologyChatPlugin();
@@ -527,7 +557,7 @@ describe("createSynologyChatPlugin", () => {
await new Promise((r) => setTimeout(r, 10));
expect(registerMock).toHaveBeenCalledTimes(2);
expect(unregisterFirst).toHaveBeenCalledTimes(1);
expect(unregisterFirst).not.toHaveBeenCalled();
expect(unregisterSecond).not.toHaveBeenCalled();
// Clean up: abort both to resolve promises and prevent test leak

View File

@@ -1,18 +1,14 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { resolveLegacyWebhookNameToChatUserId, sendMessage } from "./client.js";
import { makeFormBody, makeReq, makeRes, makeStalledReq } from "./test-http-utils.js";
import type { ResolvedSynologyChatAccount } from "./types.js";
import type { WebhookHandlerDeps } from "./webhook-handler.js";
import {
clearSynologyWebhookRateLimiterStateForTest,
createWebhookHandler,
} from "./webhook-handler.js";
// Mock sendMessage and resolveLegacyWebhookNameToChatUserId to prevent real HTTP calls
vi.mock("./client.js", () => ({
sendMessage: vi.fn().mockResolvedValue(true),
resolveLegacyWebhookNameToChatUserId: vi.fn().mockResolvedValue(undefined),
}));
const clientModule = await import("./client.js");
const sendMessage = vi.spyOn(clientModule, "sendMessage").mockResolvedValue(true);
const resolveLegacyWebhookNameToChatUserId = vi
.spyOn(clientModule, "resolveLegacyWebhookNameToChatUserId")
.mockResolvedValue(undefined);
const { clearSynologyWebhookRateLimiterStateForTest, createWebhookHandler } =
await import("./webhook-handler.js");
function makeAccount(
overrides: Partial<ResolvedSynologyChatAccount> = {},
@@ -81,6 +77,10 @@ describe("createWebhookHandler", () => {
beforeEach(() => {
clearSynologyWebhookRateLimiterStateForTest();
sendMessage.mockClear();
sendMessage.mockResolvedValue(true);
resolveLegacyWebhookNameToChatUserId.mockClear();
resolveLegacyWebhookNameToChatUserId.mockResolvedValue(undefined);
log = {
info: vi.fn(),
warn: vi.fn(),