mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-28 10:01:13 +00:00
186 lines
5.6 KiB
TypeScript
186 lines
5.6 KiB
TypeScript
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const resolveFeishuRuntimeAccountMock = vi.hoisted(() => vi.fn());
|
|
const createFeishuClientMock = vi.hoisted(() => vi.fn());
|
|
const createReplyPrefixContextMock = vi.hoisted(() => vi.fn());
|
|
const createCommentTypingReactionLifecycleMock = vi.hoisted(() => vi.fn());
|
|
const deliverCommentThreadTextMock = vi.hoisted(() => vi.fn());
|
|
const createReplyDispatcherWithTypingMock = vi.hoisted(() => vi.fn());
|
|
const getFeishuRuntimeMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("./accounts.js", () => ({
|
|
resolveFeishuRuntimeAccount: resolveFeishuRuntimeAccountMock,
|
|
}));
|
|
|
|
vi.mock("./client.js", () => ({
|
|
createFeishuClient: createFeishuClientMock,
|
|
}));
|
|
|
|
vi.mock("./comment-dispatcher-runtime-api.js", () => ({
|
|
createReplyPrefixContext: createReplyPrefixContextMock,
|
|
}));
|
|
|
|
vi.mock("./comment-reaction.js", () => ({
|
|
createCommentTypingReactionLifecycle: createCommentTypingReactionLifecycleMock,
|
|
}));
|
|
|
|
vi.mock("./drive.js", () => ({
|
|
deliverCommentThreadText: deliverCommentThreadTextMock,
|
|
}));
|
|
|
|
vi.mock("./runtime.js", () => ({
|
|
getFeishuRuntime: getFeishuRuntimeMock,
|
|
}));
|
|
|
|
import { createFeishuCommentReplyDispatcher } from "./comment-dispatcher.js";
|
|
|
|
async function raceWithNextMacrotask<T>(promise: Promise<T>): Promise<T | "pending"> {
|
|
return await Promise.race([
|
|
promise,
|
|
new Promise<"pending">((resolve) => {
|
|
setImmediate(() => resolve("pending"));
|
|
}),
|
|
]);
|
|
}
|
|
|
|
describe("createFeishuCommentReplyDispatcher", () => {
|
|
afterAll(() => {
|
|
vi.doUnmock("./accounts.js");
|
|
vi.doUnmock("./client.js");
|
|
vi.doUnmock("./comment-dispatcher-runtime-api.js");
|
|
vi.doUnmock("./comment-reaction.js");
|
|
vi.doUnmock("./drive.js");
|
|
vi.doUnmock("./runtime.js");
|
|
vi.resetModules();
|
|
});
|
|
|
|
function createTestCommentReplyDispatcher() {
|
|
createFeishuCommentReplyDispatcher({
|
|
cfg: {} as never,
|
|
agentId: "main",
|
|
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
accountId: "main",
|
|
fileToken: "doc_token_1",
|
|
fileType: "docx",
|
|
commentId: "comment_1",
|
|
replyId: "reply_1",
|
|
isWholeComment: false,
|
|
});
|
|
}
|
|
|
|
function latestReplyDispatcherOptions() {
|
|
const options = createReplyDispatcherWithTypingMock.mock.calls.at(-1)?.[0];
|
|
if (!options) {
|
|
throw new Error("expected reply dispatcher options");
|
|
}
|
|
return options as {
|
|
deliver: (payload: { text: string }, phase: { kind: string }) => Promise<void> | void;
|
|
onCleanup?: () => Promise<void> | void;
|
|
onReplyStart?: () => Promise<void> | void;
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
resolveFeishuRuntimeAccountMock.mockReturnValue({
|
|
accountId: "main",
|
|
appId: "app_id",
|
|
appSecret: "app_secret",
|
|
domain: "feishu",
|
|
config: {},
|
|
});
|
|
createFeishuClientMock.mockReturnValue({});
|
|
createReplyPrefixContextMock.mockReturnValue({
|
|
responsePrefix: undefined,
|
|
responsePrefixContextProvider: undefined,
|
|
});
|
|
deliverCommentThreadTextMock.mockResolvedValue({
|
|
delivery_mode: "reply_comment",
|
|
reply_id: "reply_1",
|
|
});
|
|
createCommentTypingReactionLifecycleMock.mockReturnValue({
|
|
start: vi.fn(async () => {}),
|
|
cleanup: vi.fn(async () => {}),
|
|
});
|
|
createReplyDispatcherWithTypingMock.mockImplementation(() => ({
|
|
dispatcher: {
|
|
markComplete: vi.fn(),
|
|
waitForIdle: vi.fn(async () => {}),
|
|
},
|
|
replyOptions: {},
|
|
markDispatchIdle: vi.fn(),
|
|
markRunComplete: vi.fn(),
|
|
}));
|
|
getFeishuRuntimeMock.mockReturnValue({
|
|
channel: {
|
|
text: {
|
|
resolveTextChunkLimit: vi.fn(() => 4000),
|
|
resolveChunkMode: vi.fn(() => "line"),
|
|
chunkTextWithMode: vi.fn((text: string) => [text]),
|
|
},
|
|
reply: {
|
|
createReplyDispatcherWithTyping: createReplyDispatcherWithTypingMock,
|
|
resolveHumanDelayConfig: vi.fn(() => undefined),
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("sends final comment text without waiting for typing cleanup", async () => {
|
|
let resolveCleanup: (() => void) | undefined;
|
|
const cleanup = vi.fn(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
resolveCleanup = resolve;
|
|
}),
|
|
);
|
|
createCommentTypingReactionLifecycleMock.mockReturnValue({
|
|
start: vi.fn(async () => {}),
|
|
cleanup,
|
|
});
|
|
|
|
createTestCommentReplyDispatcher();
|
|
|
|
const options = latestReplyDispatcherOptions();
|
|
const deliverPromise = Promise.resolve(
|
|
options.deliver({ text: "hello world" }, { kind: "final" }),
|
|
);
|
|
const status = await raceWithNextMacrotask(deliverPromise.then(() => "done"));
|
|
|
|
expect(status).toBe("done");
|
|
const client = createFeishuClientMock.mock.results[0]?.value;
|
|
if (!client) {
|
|
throw new Error("Expected Feishu client");
|
|
}
|
|
expect(deliverCommentThreadTextMock).toHaveBeenCalledWith(client, {
|
|
file_token: "doc_token_1",
|
|
file_type: "docx",
|
|
comment_id: "comment_1",
|
|
content: "hello world",
|
|
is_whole_comment: false,
|
|
});
|
|
expect(cleanup).not.toHaveBeenCalled();
|
|
|
|
void options.onCleanup?.();
|
|
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
|
|
resolveCleanup?.();
|
|
await deliverPromise;
|
|
});
|
|
|
|
it("starts the typing reaction from dispatcher onReplyStart", async () => {
|
|
const start = vi.fn(async () => {});
|
|
createCommentTypingReactionLifecycleMock.mockReturnValue({
|
|
start,
|
|
cleanup: vi.fn(async () => {}),
|
|
});
|
|
|
|
createTestCommentReplyDispatcher();
|
|
|
|
const options = latestReplyDispatcherOptions();
|
|
await options.onReplyStart?.();
|
|
|
|
expect(start).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|