mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-04 22:01:15 +00:00
fix(feishu): restore tsgo test typing
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { EnvelopeFormatOptions } from "../../../src/auto-reply/envelope.js";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js";
|
||||
@@ -15,6 +16,7 @@ const { mockCreateFeishuReplyDispatcher, mockCreateFeishuClient, mockResolveAgen
|
||||
sendFinalReply: vi.fn(),
|
||||
waitForIdle: vi.fn(),
|
||||
getQueuedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })),
|
||||
getFailedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })),
|
||||
markComplete: vi.fn(),
|
||||
},
|
||||
replyOptions: {},
|
||||
@@ -33,28 +35,37 @@ vi.mock("./client.js", () => ({
|
||||
}));
|
||||
|
||||
describe("broadcast dispatch", () => {
|
||||
const mockFinalizeInboundContext = vi.fn((ctx: unknown) => ctx);
|
||||
const finalizeInboundContextCalls: Array<Record<string, unknown>> = [];
|
||||
const mockFinalizeInboundContext: PluginRuntime["channel"]["reply"]["finalizeInboundContext"] = (
|
||||
ctx,
|
||||
) => {
|
||||
finalizeInboundContextCalls.push(ctx);
|
||||
return {
|
||||
...ctx,
|
||||
CommandAuthorized: typeof ctx.CommandAuthorized === "boolean" ? ctx.CommandAuthorized : false,
|
||||
};
|
||||
};
|
||||
const mockDispatchReplyFromConfig = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ queuedFinal: false, counts: { final: 1 } });
|
||||
const mockWithReplyDispatcher = vi.fn(
|
||||
async ({
|
||||
dispatcher,
|
||||
run,
|
||||
onSettled,
|
||||
}: Parameters<PluginRuntime["channel"]["reply"]["withReplyDispatcher"]>[0]) => {
|
||||
const mockWithReplyDispatcher: PluginRuntime["channel"]["reply"]["withReplyDispatcher"] = async ({
|
||||
dispatcher,
|
||||
run,
|
||||
onSettled,
|
||||
}) => {
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
dispatcher.markComplete();
|
||||
try {
|
||||
return await run();
|
||||
await dispatcher.waitForIdle();
|
||||
} finally {
|
||||
dispatcher.markComplete();
|
||||
try {
|
||||
await dispatcher.waitForIdle();
|
||||
} finally {
|
||||
await onSettled?.();
|
||||
}
|
||||
await onSettled?.();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
const resolveEnvelopeFormatOptionsMock: PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"] =
|
||||
() => ({}) satisfies EnvelopeFormatOptions;
|
||||
const mockShouldComputeCommandAuthorized = vi.fn(() => false);
|
||||
const mockSaveMediaBuffer = vi.fn().mockResolvedValue({
|
||||
path: "/tmp/inbound-clip.mp4",
|
||||
@@ -108,12 +119,14 @@ describe("broadcast dispatch", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
finalizeInboundContextCalls.length = 0;
|
||||
mockResolveAgentRoute.mockReturnValue({
|
||||
agentId: "main",
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:feishu:group:oc-broadcast-group",
|
||||
mainSessionKey: "agent:main:main",
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "default",
|
||||
});
|
||||
mockCreateFeishuClient.mockReturnValue({
|
||||
@@ -133,7 +146,7 @@ describe("broadcast dispatch", () => {
|
||||
resolveAgentRoute: (params) => mockResolveAgentRoute(params),
|
||||
},
|
||||
reply: {
|
||||
resolveEnvelopeFormatOptions: vi.fn(() => ({ template: "channel+name+time" })),
|
||||
resolveEnvelopeFormatOptions: resolveEnvelopeFormatOptionsMock,
|
||||
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
|
||||
finalizeInboundContext: mockFinalizeInboundContext,
|
||||
dispatchReplyFromConfig: mockDispatchReplyFromConfig,
|
||||
@@ -175,9 +188,7 @@ describe("broadcast dispatch", () => {
|
||||
});
|
||||
|
||||
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(2);
|
||||
const sessionKeys = mockFinalizeInboundContext.mock.calls.map(
|
||||
(call: unknown[]) => (call[0] as { SessionKey: string }).SessionKey,
|
||||
);
|
||||
const sessionKeys = finalizeInboundContextCalls.map((call) => call.SessionKey);
|
||||
expect(sessionKeys).toContain("agent:susan:feishu:group:oc-broadcast-group");
|
||||
expect(sessionKeys).toContain("agent:main:feishu:group:oc-broadcast-group");
|
||||
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledTimes(1);
|
||||
@@ -253,7 +264,7 @@ describe("broadcast dispatch", () => {
|
||||
|
||||
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledTimes(1);
|
||||
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
||||
expect(finalizeInboundContextCalls).toContainEqual(
|
||||
expect.objectContaining({
|
||||
SessionKey: "agent:main:feishu:group:oc-broadcast-group",
|
||||
}),
|
||||
@@ -295,7 +306,7 @@ describe("broadcast dispatch", () => {
|
||||
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(2);
|
||||
|
||||
mockDispatchReplyFromConfig.mockClear();
|
||||
mockFinalizeInboundContext.mockClear();
|
||||
finalizeInboundContextCalls.length = 0;
|
||||
|
||||
await handleFeishuMessage({
|
||||
cfg,
|
||||
@@ -339,8 +350,7 @@ describe("broadcast dispatch", () => {
|
||||
});
|
||||
|
||||
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
||||
const sessionKey = (mockFinalizeInboundContext.mock.calls[0]?.[0] as { SessionKey: string })
|
||||
.SessionKey;
|
||||
const sessionKey = String(finalizeInboundContextCalls[0]?.SessionKey ?? "");
|
||||
expect(sessionKey).toBe("agent:susan:feishu:group:oc-broadcast-group");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
|
||||
import {
|
||||
handleFeishuCardAction,
|
||||
@@ -35,7 +36,7 @@ import { handleFeishuMessage } from "./bot.js";
|
||||
|
||||
describe("Feishu Card Action Handler", () => {
|
||||
const cfg: ClawdbotConfig = {};
|
||||
const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn() };
|
||||
const runtime: RuntimeEnv = createRuntimeEnv();
|
||||
|
||||
function createCardActionEvent(params: {
|
||||
token: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type * as ConversationRuntime from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ResolvedAgentRoute } from "../../../src/routing/resolve-route.js";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
@@ -31,6 +32,7 @@ function createReplyDispatcher(): ReplyDispatcher {
|
||||
sendFinalReply: vi.fn(),
|
||||
waitForIdle: vi.fn(),
|
||||
getQueuedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })),
|
||||
getFailedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })),
|
||||
markComplete: vi.fn(),
|
||||
};
|
||||
}
|
||||
@@ -38,42 +40,59 @@ function createReplyDispatcher(): ReplyDispatcher {
|
||||
function createConfiguredFeishuRoute(): NonNullable<ConfiguredBindingRoute> {
|
||||
return {
|
||||
bindingResolution: {
|
||||
configuredBinding: {
|
||||
spec: {
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
conversationId: "ou_sender_1",
|
||||
agentId: "codex",
|
||||
mode: "persistent",
|
||||
},
|
||||
record: {
|
||||
bindingId: "config:acp:feishu:default:ou_sender_1",
|
||||
targetSessionKey: "agent:codex:acp:binding:feishu:default:abc123",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
conversationId: "ou_sender_1",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: 0,
|
||||
metadata: { source: "config" },
|
||||
},
|
||||
},
|
||||
statefulTarget: {
|
||||
kind: "stateful",
|
||||
driverId: "acp",
|
||||
sessionKey: "agent:codex:acp:binding:feishu:default:abc123",
|
||||
agentId: "codex",
|
||||
},
|
||||
},
|
||||
configuredBinding: {
|
||||
spec: {
|
||||
conversation: {
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
conversationId: "ou_sender_1",
|
||||
},
|
||||
compiledBinding: {
|
||||
channel: "feishu",
|
||||
accountPattern: "default",
|
||||
binding: {
|
||||
type: "acp",
|
||||
agentId: "codex",
|
||||
match: {
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
peer: { kind: "direct", id: "ou_sender_1" },
|
||||
},
|
||||
},
|
||||
bindingConversationId: "ou_sender_1",
|
||||
target: {
|
||||
conversationId: "ou_sender_1",
|
||||
},
|
||||
agentId: "codex",
|
||||
mode: "persistent",
|
||||
provider: {
|
||||
compileConfiguredBinding: () => ({ conversationId: "ou_sender_1" }),
|
||||
matchInboundConversation: () => ({ conversationId: "ou_sender_1" }),
|
||||
},
|
||||
targetFactory: {
|
||||
driverId: "acp",
|
||||
materialize: () => ({
|
||||
record: {
|
||||
bindingId: "config:acp:feishu:default:ou_sender_1",
|
||||
targetSessionKey: "agent:codex:acp:binding:feishu:default:abc123",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
conversationId: "ou_sender_1",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: 0,
|
||||
metadata: { source: "config" },
|
||||
},
|
||||
statefulTarget: {
|
||||
kind: "stateful",
|
||||
driverId: "acp",
|
||||
sessionKey: "agent:codex:acp:binding:feishu:default:abc123",
|
||||
agentId: "codex",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
match: {
|
||||
conversationId: "ou_sender_1",
|
||||
},
|
||||
record: {
|
||||
bindingId: "config:acp:feishu:default:ou_sender_1",
|
||||
@@ -88,6 +107,12 @@ function createConfiguredFeishuRoute(): NonNullable<ConfiguredBindingRoute> {
|
||||
boundAt: 0,
|
||||
metadata: { source: "config" },
|
||||
},
|
||||
statefulTarget: {
|
||||
kind: "stateful",
|
||||
driverId: "acp",
|
||||
sessionKey: "agent:codex:acp:binding:feishu:default:abc123",
|
||||
agentId: "codex",
|
||||
},
|
||||
},
|
||||
route: {
|
||||
agentId: "codex",
|
||||
@@ -95,6 +120,7 @@ function createConfiguredFeishuRoute(): NonNullable<ConfiguredBindingRoute> {
|
||||
accountId: "default",
|
||||
sessionKey: "agent:codex:acp:binding:feishu:default:abc123",
|
||||
mainSessionKey: "agent:codex:main",
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "binding.channel",
|
||||
},
|
||||
};
|
||||
@@ -120,13 +146,14 @@ function createBoundConversation(): NonNullable<BoundConversation> {
|
||||
};
|
||||
}
|
||||
|
||||
function buildDefaultResolveRoute() {
|
||||
function buildDefaultResolveRoute(): ResolvedAgentRoute {
|
||||
return {
|
||||
agentId: "main",
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:feishu:dm:ou-attacker",
|
||||
mainSessionKey: "agent:main:main",
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "default",
|
||||
};
|
||||
}
|
||||
@@ -138,14 +165,11 @@ const readSessionUpdatedAtMock: PluginRuntime["channel"]["session"]["readSession
|
||||
) => mockReadSessionUpdatedAt(params);
|
||||
const resolveStorePathMock: PluginRuntime["channel"]["session"]["resolveStorePath"] = (params) =>
|
||||
mockResolveStorePath(params);
|
||||
const resolveEnvelopeFormatOptionsMock: PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"] =
|
||||
() => ({});
|
||||
const finalizeInboundContextMock: PluginRuntime["channel"]["reply"]["finalizeInboundContext"] = (
|
||||
ctx,
|
||||
) => ctx;
|
||||
const withReplyDispatcherMock: PluginRuntime["channel"]["reply"]["withReplyDispatcher"] = async ({
|
||||
const resolveEnvelopeFormatOptionsMock = () => ({});
|
||||
const finalizeInboundContextMock = (ctx: Record<string, unknown>) => ctx;
|
||||
const withReplyDispatcherMock = async ({
|
||||
run,
|
||||
}) => await run();
|
||||
}: Parameters<PluginRuntime["channel"]["reply"]["withReplyDispatcher"]>[0]) => await run();
|
||||
|
||||
const {
|
||||
mockCreateFeishuReplyDispatcher,
|
||||
@@ -176,17 +200,22 @@ const {
|
||||
fileName: "clip.mp4",
|
||||
}),
|
||||
mockCreateFeishuClient: vi.fn(),
|
||||
mockResolveAgentRoute: vi.fn(buildDefaultResolveRoute),
|
||||
mockReadSessionUpdatedAt: vi.fn(),
|
||||
mockResolveStorePath: vi.fn(() => "/tmp/feishu-sessions.json"),
|
||||
mockResolveAgentRoute: vi.fn((_params?: unknown) => buildDefaultResolveRoute()),
|
||||
mockReadSessionUpdatedAt: vi.fn((_params?: unknown): number | undefined => undefined),
|
||||
mockResolveStorePath: vi.fn((_params?: unknown) => "/tmp/feishu-sessions.json"),
|
||||
mockResolveConfiguredBindingRoute: vi.fn(
|
||||
({ route }: { route: NonNullable<ConfiguredBindingRoute>["route"] }) => ({
|
||||
({
|
||||
route,
|
||||
}: {
|
||||
route: NonNullable<ConfiguredBindingRoute>["route"];
|
||||
}): ConfiguredBindingRoute => ({
|
||||
bindingResolution: null,
|
||||
configuredBinding: null,
|
||||
route,
|
||||
}),
|
||||
),
|
||||
mockEnsureConfiguredBindingRouteReady: vi.fn(async () => ({ ok: true as const })),
|
||||
mockEnsureConfiguredBindingRouteReady: vi.fn(
|
||||
async (_params?: unknown): Promise<BindingReadiness> => ({ ok: true }),
|
||||
),
|
||||
mockResolveBoundConversation: vi.fn(() => null as BoundConversation),
|
||||
mockTouchBinding: vi.fn(),
|
||||
}));
|
||||
@@ -213,7 +242,8 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
resolveConfiguredBindingRoute: (params: unknown) => mockResolveConfiguredBindingRoute(params),
|
||||
resolveConfiguredBindingRoute: (params: unknown) =>
|
||||
mockResolveConfiguredBindingRoute(params as { route: ResolvedAgentRoute }),
|
||||
ensureConfiguredBindingRouteReady: (params: unknown) =>
|
||||
mockEnsureConfiguredBindingRouteReady(params),
|
||||
getSessionBindingService: () => ({
|
||||
@@ -243,13 +273,16 @@ async function dispatchMessage(params: { cfg: ClawdbotConfig; event: FeishuMessa
|
||||
describe("handleFeishuMessage ACP routing", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockResolveConfiguredBindingRoute
|
||||
.mockReset()
|
||||
.mockImplementation(({ route }: { route: NonNullable<ConfiguredBindingRoute>["route"] }) => ({
|
||||
bindingResolution: null,
|
||||
configuredBinding: null,
|
||||
mockResolveConfiguredBindingRoute.mockReset().mockImplementation(
|
||||
({
|
||||
route,
|
||||
}));
|
||||
}: {
|
||||
route: NonNullable<ConfiguredBindingRoute>["route"];
|
||||
}): ConfiguredBindingRoute => ({
|
||||
bindingResolution: null,
|
||||
route,
|
||||
}),
|
||||
);
|
||||
mockEnsureConfiguredBindingRouteReady.mockReset().mockResolvedValue({ ok: true });
|
||||
mockResolveBoundConversation.mockReset().mockReturnValue(null);
|
||||
mockTouchBinding.mockReset();
|
||||
@@ -279,12 +312,12 @@ describe("handleFeishuMessage ACP routing", () => {
|
||||
reply: {
|
||||
resolveEnvelopeFormatOptions: resolveEnvelopeFormatOptionsMock,
|
||||
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
|
||||
finalizeInboundContext: finalizeInboundContextMock,
|
||||
finalizeInboundContext: finalizeInboundContextMock as never,
|
||||
dispatchReplyFromConfig: vi.fn().mockResolvedValue({
|
||||
queuedFinal: false,
|
||||
counts: { final: 1 },
|
||||
}),
|
||||
withReplyDispatcher: withReplyDispatcherMock,
|
||||
withReplyDispatcher: withReplyDispatcherMock as never,
|
||||
},
|
||||
commands: {
|
||||
shouldComputeCommandAuthorized: vi.fn(() => false),
|
||||
@@ -399,7 +432,10 @@ describe("handleFeishuMessage ACP routing", () => {
|
||||
});
|
||||
|
||||
describe("handleFeishuMessage command authorization", () => {
|
||||
const mockFinalizeInboundContext = vi.fn((ctx: unknown) => ctx);
|
||||
const mockFinalizeInboundContext = vi.fn((ctx: Record<string, unknown>) => ({
|
||||
...ctx,
|
||||
CommandAuthorized: typeof ctx.CommandAuthorized === "boolean" ? ctx.CommandAuthorized : false,
|
||||
}));
|
||||
const mockDispatchReplyFromConfig = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ queuedFinal: false, counts: { final: 1 } });
|
||||
@@ -441,13 +477,16 @@ describe("handleFeishuMessage command authorization", () => {
|
||||
mockListFeishuThreadMessages.mockReset().mockResolvedValue([]);
|
||||
mockReadSessionUpdatedAt.mockReturnValue(undefined);
|
||||
mockResolveStorePath.mockReturnValue("/tmp/feishu-sessions.json");
|
||||
mockResolveConfiguredBindingRoute
|
||||
.mockReset()
|
||||
.mockImplementation(({ route }: { route: NonNullable<ConfiguredBindingRoute>["route"] }) => ({
|
||||
bindingResolution: null,
|
||||
configuredBinding: null,
|
||||
mockResolveConfiguredBindingRoute.mockReset().mockImplementation(
|
||||
({
|
||||
route,
|
||||
}));
|
||||
}: {
|
||||
route: NonNullable<ConfiguredBindingRoute>["route"];
|
||||
}): ConfiguredBindingRoute => ({
|
||||
bindingResolution: null,
|
||||
route,
|
||||
}),
|
||||
);
|
||||
mockEnsureConfiguredBindingRouteReady.mockReset().mockResolvedValue({ ok: true });
|
||||
mockResolveBoundConversation.mockReset().mockReturnValue(null);
|
||||
mockTouchBinding.mockReset();
|
||||
@@ -476,9 +515,9 @@ describe("handleFeishuMessage command authorization", () => {
|
||||
reply: {
|
||||
resolveEnvelopeFormatOptions: resolveEnvelopeFormatOptionsMock,
|
||||
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
|
||||
finalizeInboundContext: mockFinalizeInboundContext,
|
||||
finalizeInboundContext: mockFinalizeInboundContext as never,
|
||||
dispatchReplyFromConfig: mockDispatchReplyFromConfig,
|
||||
withReplyDispatcher: mockWithReplyDispatcher,
|
||||
withReplyDispatcher: mockWithReplyDispatcher as never,
|
||||
},
|
||||
commands: {
|
||||
shouldComputeCommandAuthorized: mockShouldComputeCommandAuthorized,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
|
||||
import {
|
||||
createQuickActionLauncherCard,
|
||||
@@ -86,7 +87,7 @@ describe("feishu quick-action launcher", () => {
|
||||
|
||||
it("falls back to legacy menu handling when launcher send fails", async () => {
|
||||
sendCardFeishuMock.mockRejectedValueOnce(new Error("network"));
|
||||
const runtime: RuntimeEnv = { log: vi.fn() };
|
||||
const runtime: RuntimeEnv = createRuntimeEnv();
|
||||
|
||||
const handled = await maybeHandleFeishuQuickActionMenu({
|
||||
cfg,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import type { OpenClawPluginApi } from "../runtime-api.js";
|
||||
import { registerFeishuChatTools } from "./chat.js";
|
||||
|
||||
@@ -22,8 +23,8 @@ describe("registerFeishuChatTools", () => {
|
||||
name: "Feishu Test",
|
||||
source: "local",
|
||||
config: params.config,
|
||||
runtime: { log: vi.fn(), error: vi.fn() },
|
||||
logger: { debug: vi.fn(), info: vi.fn() },
|
||||
runtime: createPluginRuntimeMock(),
|
||||
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
registerTool: params.registerTool,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import type { OpenClawPluginApi } from "../runtime-api.js";
|
||||
import { FeishuConfigSchema } from "./config-schema.js";
|
||||
import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
type CreateFeishuClient = typeof import("./client.js").createFeishuClient;
|
||||
@@ -100,13 +102,18 @@ const baseAccount: ResolvedFeishuAccount = {
|
||||
appId: "app_123",
|
||||
appSecret: "secret_123", // pragma: allowlist secret
|
||||
domain: "feishu",
|
||||
config: {},
|
||||
config: FeishuConfigSchema.parse({}),
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
type HttpInstanceLike = {
|
||||
get: (url: string, options?: Record<string, unknown>) => Promise<unknown>;
|
||||
post: (url: string, body?: unknown, options?: Record<string, unknown>) => Promise<unknown>;
|
||||
};
|
||||
|
||||
function readCallOptions(
|
||||
mock: { mock: { calls: unknown[][] } },
|
||||
index = -1,
|
||||
@@ -191,9 +198,19 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("createFeishuClient HTTP timeout", () => {
|
||||
const getLastClientHttpInstance = () => {
|
||||
const getLastClientHttpInstance = (): HttpInstanceLike | undefined => {
|
||||
const httpInstance = readCallOptions(clientCtorMock).httpInstance;
|
||||
return isRecord(httpInstance) ? httpInstance : undefined;
|
||||
if (
|
||||
isRecord(httpInstance) &&
|
||||
typeof httpInstance.get === "function" &&
|
||||
typeof httpInstance.post === "function"
|
||||
) {
|
||||
return {
|
||||
get: httpInstance.get as HttpInstanceLike["get"],
|
||||
post: httpInstance.post as HttpInstanceLike["post"],
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const expectGetCallTimeout = async (timeout: number) => {
|
||||
@@ -218,7 +235,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
const httpInstance = getLastClientHttpInstance();
|
||||
|
||||
expect(httpInstance).toBeDefined();
|
||||
await httpInstance?.post?.(
|
||||
await httpInstance?.post(
|
||||
"https://example.com/api",
|
||||
{ data: 1 },
|
||||
{ headers: { "X-Custom": "yes" } },
|
||||
@@ -237,7 +254,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
const httpInstance = getLastClientHttpInstance();
|
||||
|
||||
expect(httpInstance).toBeDefined();
|
||||
await httpInstance?.get?.("https://example.com/api", { timeout: 5_000 });
|
||||
await httpInstance?.get("https://example.com/api", { timeout: 5_000 });
|
||||
|
||||
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
|
||||
"https://example.com/api",
|
||||
@@ -323,7 +340,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
expect(clientCtorMock.mock.calls.length).toBe(2);
|
||||
const httpInstance = getLastClientHttpInstance();
|
||||
expect(httpInstance).toBeDefined();
|
||||
await httpInstance?.get?.("https://example.com/api");
|
||||
await httpInstance?.get("https://example.com/api");
|
||||
|
||||
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
|
||||
"https://example.com/api",
|
||||
@@ -340,7 +357,7 @@ describe("feishu plugin register", () => {
|
||||
id: "feishu-test",
|
||||
name: "Feishu Test",
|
||||
source: "local",
|
||||
runtime: { log: vi.fn() },
|
||||
runtime: createPluginRuntimeMock(),
|
||||
on: vi.fn(),
|
||||
config: {},
|
||||
registerChannel,
|
||||
|
||||
@@ -3,20 +3,19 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js";
|
||||
import type { FeishuDocxBlock } from "./docx-types.js";
|
||||
|
||||
type InsertBlocksClient = Parameters<typeof insertBlocksInBatches>[0];
|
||||
type DocxDescendantCreate = Lark.Client["docx"]["documentBlockDescendant"]["create"];
|
||||
type DocxDescendantCreateParams = Parameters<DocxDescendantCreate>[0];
|
||||
type DocxDescendantCreateResponse = Awaited<ReturnType<DocxDescendantCreate>>;
|
||||
|
||||
function createDocxDescendantClient(
|
||||
create: (params: DocxDescendantCreateParams) => Promise<DocxDescendantCreateResponse>,
|
||||
): Pick<Lark.Client, "docx"> {
|
||||
function createDocxDescendantClient(create: DocxDescendantCreate): InsertBlocksClient {
|
||||
return {
|
||||
docx: {
|
||||
documentBlockDescendant: {
|
||||
create,
|
||||
},
|
||||
},
|
||||
};
|
||||
} as InsertBlocksClient;
|
||||
}
|
||||
|
||||
function createCountingIterable<T>(values: T[]) {
|
||||
@@ -40,13 +39,18 @@ describe("insertBlocksInBatches", () => {
|
||||
block_type: 2,
|
||||
}));
|
||||
const counting = createCountingIterable(blocks);
|
||||
const createMock = vi.fn(async ({ data }: { data: { children_id: string[] } }) => ({
|
||||
code: 0,
|
||||
data: {
|
||||
children: data.children_id.map((id) => ({ block_id: id })),
|
||||
},
|
||||
}));
|
||||
const client = createDocxDescendantClient(createMock);
|
||||
const createMock = vi.fn(
|
||||
async (params?: DocxDescendantCreateParams): Promise<DocxDescendantCreateResponse> => ({
|
||||
code: 0,
|
||||
data: {
|
||||
children: (params?.data?.children_id ?? []).map((id) => ({
|
||||
block_id: id,
|
||||
block_type: 2,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
);
|
||||
const client = createDocxDescendantClient((params) => createMock(params));
|
||||
|
||||
const result = await insertBlocksInBatches(
|
||||
client,
|
||||
@@ -64,18 +68,17 @@ describe("insertBlocksInBatches", () => {
|
||||
|
||||
it("keeps nested descendants grouped with their root blocks", async () => {
|
||||
const createMock = vi.fn(
|
||||
async ({
|
||||
data,
|
||||
}: {
|
||||
data: { children_id: string[]; descendants: Array<{ block_id: string }> };
|
||||
}) => ({
|
||||
async (params?: DocxDescendantCreateParams): Promise<DocxDescendantCreateResponse> => ({
|
||||
code: 0,
|
||||
data: {
|
||||
children: data.children_id.map((id) => ({ block_id: id })),
|
||||
children: (params?.data?.children_id ?? []).map((id) => ({
|
||||
block_id: id,
|
||||
block_type: 2,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
);
|
||||
const client = createDocxDescendantClient(createMock);
|
||||
const client = createDocxDescendantClient((params) => createMock(params));
|
||||
const blocks: FeishuDocxBlock[] = [
|
||||
{ block_id: "root_a", block_type: 1, children: ["child_a"] },
|
||||
{ block_id: "child_a", block_type: 2 },
|
||||
@@ -88,9 +91,7 @@ describe("insertBlocksInBatches", () => {
|
||||
expect(createMock).toHaveBeenCalledTimes(1);
|
||||
expect(createMock.mock.calls[0]?.[0]?.data.children_id).toEqual(["root_a", "root_b"]);
|
||||
expect(
|
||||
createMock.mock.calls[0]?.[0]?.data.descendants.map(
|
||||
(block: { block_id: string }) => block.block_id,
|
||||
),
|
||||
createMock.mock.calls[0]?.[0]?.data.descendants.map((block) => block.block_id ?? ""),
|
||||
).toEqual(["root_a", "child_a", "root_b", "child_b"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../../../test/helpers/extensions/feishu-lifecycle.js";
|
||||
import { createNonExitingRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
|
||||
import { FeishuConfigSchema } from "./config-schema.js";
|
||||
import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js";
|
||||
import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
@@ -88,7 +89,7 @@ function createLifecycleConfig(): ClawdbotConfig {
|
||||
}
|
||||
|
||||
function createLifecycleAccount(accountId: "account-A" | "account-B"): ResolvedFeishuAccount {
|
||||
const config: FeishuConfig = {
|
||||
const config: FeishuConfig = FeishuConfigSchema.parse({
|
||||
enabled: true,
|
||||
connectionMode: "websocket",
|
||||
groupPolicy: "open",
|
||||
@@ -99,7 +100,7 @@ function createLifecycleAccount(accountId: "account-A" | "account-B"): ResolvedF
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
return {
|
||||
accountId,
|
||||
selectionSource: "explicit",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { FeishuConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
isFeishuGroupAllowed,
|
||||
resolveFeishuAllowlistMatch,
|
||||
@@ -88,12 +89,12 @@ describe("resolveFeishuReplyPolicy", () => {
|
||||
|
||||
describe("resolveFeishuGroupConfig", () => {
|
||||
it("falls back to wildcard group config when direct match is missing", () => {
|
||||
const cfg: FeishuConfig = {
|
||||
const cfg: FeishuConfig = FeishuConfigSchema.parse({
|
||||
groups: {
|
||||
"*": { requireMention: false },
|
||||
"oc-explicit": { requireMention: true },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const resolved = resolveFeishuGroupConfig({
|
||||
cfg,
|
||||
@@ -104,12 +105,12 @@ describe("resolveFeishuGroupConfig", () => {
|
||||
});
|
||||
|
||||
it("prefers exact group config over wildcard", () => {
|
||||
const cfg: FeishuConfig = {
|
||||
const cfg: FeishuConfig = FeishuConfigSchema.parse({
|
||||
groups: {
|
||||
"*": { requireMention: false },
|
||||
"oc-explicit": { requireMention: true },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const resolved = resolveFeishuGroupConfig({
|
||||
cfg,
|
||||
@@ -120,12 +121,12 @@ describe("resolveFeishuGroupConfig", () => {
|
||||
});
|
||||
|
||||
it("keeps case-insensitive matching for explicit group ids", () => {
|
||||
const cfg: FeishuConfig = {
|
||||
const cfg: FeishuConfig = FeishuConfigSchema.parse({
|
||||
groups: {
|
||||
"*": { requireMention: false },
|
||||
OC_UPPER: { requireMention: true },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const resolved = resolveFeishuGroupConfig({
|
||||
cfg,
|
||||
|
||||
@@ -511,17 +511,17 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
||||
await options.deliver({ text: "answer part final" }, { kind: "final" });
|
||||
|
||||
expect(streamingInstances).toHaveLength(1);
|
||||
const updateCalls = streamingInstances[0].update.mock.calls.map((c: unknown[]) => c[0]);
|
||||
const reasoningUpdate = updateCalls.find((c: string) => c.includes("Thinking"));
|
||||
const updateCalls = streamingInstances[0].update.mock.calls.map((c: unknown[]) =>
|
||||
String(c[0] ?? ""),
|
||||
);
|
||||
const reasoningUpdate = updateCalls.find((c) => c.includes("Thinking"));
|
||||
expect(reasoningUpdate).toContain("> 💭 **Thinking**");
|
||||
// formatReasoningPrefix strips "Reasoning:" prefix and italic markers
|
||||
expect(reasoningUpdate).toContain("> thinking step");
|
||||
expect(reasoningUpdate).not.toContain("Reasoning:");
|
||||
expect(reasoningUpdate).not.toMatch(/> _.*_/);
|
||||
|
||||
const combinedUpdate = updateCalls.find(
|
||||
(c: string) => c.includes("Thinking") && c.includes("---"),
|
||||
);
|
||||
const combinedUpdate = updateCalls.find((c) => c.includes("Thinking") && c.includes("---"));
|
||||
expect(combinedUpdate).toBeDefined();
|
||||
|
||||
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -8,7 +8,10 @@ type ToolFactoryLike = (ctx: ToolContextLike) => AnyAgentTool | AnyAgentTool[] |
|
||||
|
||||
export type ToolLike = {
|
||||
name: string;
|
||||
execute: (toolCallId: string, params: unknown) => Promise<unknown> | unknown;
|
||||
execute: (
|
||||
toolCallId: string,
|
||||
params: unknown,
|
||||
) => Promise<{ details: Record<string, unknown> }> | { details: Record<string, unknown> };
|
||||
};
|
||||
|
||||
type RegisteredTool = {
|
||||
|
||||
Reference in New Issue
Block a user