forked from EduCraft/curriculum-project-hub
fix: reply to Feishu trigger messages
This commit is contained in:
@@ -62,6 +62,12 @@ export interface MockFeishuRuntime extends FeishuRuntime {
|
||||
readonly sentTexts: string[];
|
||||
readonly sentCards: unknown[];
|
||||
readonly sentPatches: unknown[];
|
||||
readonly sentReplies: Array<{
|
||||
readonly messageId: string;
|
||||
readonly msgType: string;
|
||||
readonly content: string;
|
||||
readonly replyInThread: unknown;
|
||||
}>;
|
||||
readonly reactions: Array<{ readonly messageId: string; readonly emoji: string }>;
|
||||
readonly readableMessages: Map<string, MockFeishuMessage>;
|
||||
readonly threadMessages: Map<string, readonly MockFeishuMessage[]>;
|
||||
@@ -84,6 +90,7 @@ export function mockFeishuRuntime(): MockFeishuRuntime {
|
||||
const sentTexts: string[] = [];
|
||||
const sentCards: unknown[] = [];
|
||||
const sentPatches: unknown[] = [];
|
||||
const sentReplies: MockFeishuRuntime["sentReplies"] = [];
|
||||
const reactions: Array<{ messageId: string; emoji: string }> = [];
|
||||
const readableMessages = new Map<string, MockFeishuMessage>();
|
||||
const threadMessages = new Map<string, readonly MockFeishuMessage[]>();
|
||||
@@ -108,19 +115,21 @@ export function mockFeishuRuntime(): MockFeishuRuntime {
|
||||
message: {
|
||||
create: async (p: unknown) => {
|
||||
const payload = p as { data?: { msg_type?: string; content?: string } };
|
||||
if (payload.data?.msg_type === "interactive") {
|
||||
const card = payload.data.content ? JSON.parse(payload.data.content) : null;
|
||||
sentCards.push(card);
|
||||
const text = textFromCard(card);
|
||||
if (text !== null) sentTexts.push(text);
|
||||
} else {
|
||||
try {
|
||||
const c = JSON.parse(payload.data?.content ?? "{}") as { text?: string };
|
||||
sentTexts.push(c.text ?? payload.data?.content ?? "");
|
||||
} catch {
|
||||
sentTexts.push(payload.data?.content ?? "");
|
||||
}
|
||||
}
|
||||
recordSentMessage(payload.data, sentTexts, sentCards);
|
||||
return { data: { message_id: "mock-msg-id" } };
|
||||
},
|
||||
reply: async (p: unknown) => {
|
||||
const payload = p as {
|
||||
path?: { message_id?: string };
|
||||
data?: { msg_type?: string; content?: string; reply_in_thread?: unknown };
|
||||
};
|
||||
sentReplies.push({
|
||||
messageId: payload.path?.message_id ?? "",
|
||||
msgType: payload.data?.msg_type ?? "",
|
||||
content: payload.data?.content ?? "",
|
||||
replyInThread: payload.data?.reply_in_thread,
|
||||
});
|
||||
recordSentMessage(payload.data, sentTexts, sentCards);
|
||||
return { data: { message_id: "mock-msg-id" } };
|
||||
},
|
||||
patch: async (p: unknown) => {
|
||||
@@ -153,6 +162,7 @@ export function mockFeishuRuntime(): MockFeishuRuntime {
|
||||
sentTexts,
|
||||
sentCards,
|
||||
sentPatches,
|
||||
sentReplies,
|
||||
reactions,
|
||||
readableMessages,
|
||||
threadMessages,
|
||||
@@ -160,6 +170,27 @@ export function mockFeishuRuntime(): MockFeishuRuntime {
|
||||
return rt;
|
||||
}
|
||||
|
||||
function recordSentMessage(
|
||||
data: { msg_type?: string; content?: string } | undefined,
|
||||
sentTexts: string[],
|
||||
sentCards: unknown[],
|
||||
): void {
|
||||
if (data?.msg_type === "interactive") {
|
||||
const card = data.content ? JSON.parse(data.content) : null;
|
||||
sentCards.push(card);
|
||||
const text = textFromCard(card);
|
||||
if (text !== null) sentTexts.push(text);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const c = JSON.parse(data?.content ?? "{}") as { text?: string };
|
||||
sentTexts.push(c.text ?? data?.content ?? "");
|
||||
} catch {
|
||||
sentTexts.push(data?.content ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
function textFromCard(card: unknown): string | null {
|
||||
if (typeof card !== "object" || card === null) return null;
|
||||
const elements = (card as { elements?: unknown }).elements;
|
||||
|
||||
@@ -104,8 +104,9 @@ describe("trigger full lifecycle (integration)", () => {
|
||||
it("creates a run, acquires + releases the lock, sends status card", async () => {
|
||||
await seedProject("proj-1", "chat-1");
|
||||
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
|
||||
const event = makeEvent("chat-1", "@_user_1 写教案");
|
||||
|
||||
await trigger(makeEvent("chat-1", "@_user_1 写教案"), rt);
|
||||
await trigger(event, rt);
|
||||
|
||||
// Wait for the async run to complete (fire-and-forget in trigger).
|
||||
await vi.waitFor(async () => {
|
||||
@@ -122,6 +123,11 @@ describe("trigger full lifecycle (integration)", () => {
|
||||
|
||||
// A status card was sent.
|
||||
expect(rt.sentCards.length).toBeGreaterThanOrEqual(1);
|
||||
expect(rt.sentReplies[0]).toMatchObject({
|
||||
messageId: event.message.message_id,
|
||||
msgType: "interactive",
|
||||
replyInThread: undefined,
|
||||
});
|
||||
expect(rt.sentTexts).toContain("mock response");
|
||||
expect(runAgentCalls).toHaveLength(1);
|
||||
expect(runAgentCalls[0]?.providerEnv).toMatchObject({
|
||||
|
||||
@@ -34,6 +34,23 @@ describe("Feishu client helpers", () => {
|
||||
await expect(sendTextMessage(rt, "chat-1", "hello")).resolves.toBe("message-1");
|
||||
});
|
||||
|
||||
it("replies to a message without enabling Feishu topic mode", async () => {
|
||||
const messageCreate = vi.fn();
|
||||
const messageReply = vi.fn(async () => ({ data: { message_id: "reply-1" } }));
|
||||
const rt = mockRuntime({ fileCreate: vi.fn(), messageCreate, messageReply });
|
||||
|
||||
await expect(sendTextMessage(rt, "chat-1", "hello", { replyToMessageId: "m-trigger" })).resolves.toBe("reply-1");
|
||||
|
||||
expect(messageCreate).not.toHaveBeenCalled();
|
||||
expect(messageReply).toHaveBeenCalledWith({
|
||||
path: { message_id: "m-trigger" },
|
||||
data: {
|
||||
msg_type: "text",
|
||||
content: JSON.stringify({ text: "hello" }),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("sends long text in chunks and returns the last successful message id", async () => {
|
||||
let messageNumber = 0;
|
||||
const messageCreate = vi.fn(async () => ({ data: { message_id: `message-${++messageNumber}` } }));
|
||||
@@ -70,6 +87,7 @@ describe("Feishu client helpers", () => {
|
||||
function mockRuntime(options: {
|
||||
readonly fileCreate: (payload: unknown) => Promise<unknown>;
|
||||
readonly messageCreate: (payload: unknown) => Promise<unknown>;
|
||||
readonly messageReply?: (payload: unknown) => Promise<unknown>;
|
||||
readonly contactBasicBatch?: (payload: unknown) => Promise<unknown>;
|
||||
readonly request?: (payload: unknown) => Promise<unknown>;
|
||||
}): FeishuRuntime {
|
||||
@@ -86,7 +104,10 @@ function mockRuntime(options: {
|
||||
im: {
|
||||
v1: {
|
||||
file: { create: options.fileCreate },
|
||||
message: { create: options.messageCreate },
|
||||
message: {
|
||||
create: options.messageCreate,
|
||||
reply: options.messageReply ?? vi.fn(async () => ({ data: { message_id: "reply-1" } })),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as FeishuRuntime["client"],
|
||||
|
||||
Reference in New Issue
Block a user