forked from bai/curriculum-project-hub
feat(hub): 事件去重(A-4) + 审计写入路径(A-8)
A-4 FeishuEventReceipt: - 新增 FeishuEventReceipt 表(eventId @unique),lark ws 至少一次投递的幂等兜底 - MessageReceiveEvent 加 header.event_id 字段 - trigger 入口查重:已存在的 event_id → 跳过(不建第二次 run) - resetDb 加入 FeishuEventReceipt A-8 审计写入: - AuditEntry 加 projectId(权限拒绝/slash 命令等无 run 的审计点可定位) - 新建 src/audit.ts:writeAudit(prisma, entry) best-effort 写入(吞错,不阻断 trigger) - trigger 五个审计点:trigger.denied / trigger.role_denied / run.created / run.lock_race / run.finished / run.failed - 新增 audit unit 测试(3) + trigger 集成测试(去重 + 审计,2) 59 tests 全过。
This commit is contained in:
@@ -8,8 +8,10 @@ import type { ModelFactory } from "../../src/agent/runner.js";
|
||||
|
||||
const bot = { key: "@_user_1", id: { open_id: "ou_bot" }, name: "Bot" };
|
||||
|
||||
function makeEvent(chatId: string, text: string, senderOpenId = "ou_test_user"): MessageReceiveEvent {
|
||||
function makeEvent(chatId: string, text: string, senderOpenId = "ou_test_user", eventId?: string): MessageReceiveEvent {
|
||||
const header = eventId !== undefined ? { event_id: eventId, event_type: "im.message.receive_v1" } : undefined;
|
||||
return {
|
||||
header,
|
||||
message: {
|
||||
message_id: "m_" + Math.random().toString(36).slice(2),
|
||||
chat_id: chatId,
|
||||
@@ -248,6 +250,44 @@ describe("trigger full lifecycle (integration)", () => {
|
||||
expect(runs[0]?.metadata).toMatchObject({ roleId: "draft" });
|
||||
});
|
||||
});
|
||||
|
||||
it("dedups a redelivered event by event_id (no second run)", async () => {
|
||||
await seedProject("proj-13", "chat-13");
|
||||
const trigger = makeTriggerHandler({ prisma, modelFactory, tools, models, logger: silentLogger });
|
||||
|
||||
// First delivery: processes normally.
|
||||
await trigger(makeEvent("chat-13", "@_user_1 写教案", "ou_test_user", "evt-dedup-1"), rt);
|
||||
await vi.waitFor(async () => {
|
||||
const runs = await prisma.agentRun.findMany();
|
||||
expect(runs).toHaveLength(1);
|
||||
expect(runs[0]?.status).toBe("COMPLETED");
|
||||
});
|
||||
|
||||
// Redelivery of the same event_id: must not create a second run.
|
||||
await trigger(makeEvent("chat-13", "@_user_1 写教案", "ou_test_user", "evt-dedup-1"), rt);
|
||||
const runs2 = await prisma.agentRun.findMany();
|
||||
expect(runs2).toHaveLength(1);
|
||||
|
||||
// Only one event receipt row.
|
||||
const receipts = await prisma.feishuEventReceipt.findMany();
|
||||
expect(receipts).toHaveLength(1);
|
||||
expect(receipts[0]?.eventId).toBe("evt-dedup-1");
|
||||
});
|
||||
|
||||
it("writes audit entries across the run lifecycle", async () => {
|
||||
await seedProject("proj-14", "chat-14");
|
||||
const trigger = makeTriggerHandler({ prisma, modelFactory, tools, models, logger: silentLogger });
|
||||
|
||||
await trigger(makeEvent("chat-14", "@_user_1 写教案"), rt);
|
||||
await vi.waitFor(async () => {
|
||||
const audit = await prisma.auditEntry.findMany();
|
||||
expect(audit.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
const audit = await prisma.auditEntry.findMany({ orderBy: { createdAt: "asc" } });
|
||||
expect(audit.some((a) => a.action === "run.created")).toBe(true);
|
||||
expect(audit.some((a) => a.action === "run.finished")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
Reference in New Issue
Block a user