fix(feishu): restore tsgo test typing

This commit is contained in:
Ayaan Zaidi
2026-03-27 12:11:22 +05:30
parent a3e73daa6b
commit cfddce4196
11 changed files with 209 additions and 134 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {