import type { LogPage } from "@umt/contracts"; import { sql } from "@umt/db"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { type LogQueryInput, logQueries } from "../../src/modules/logs/query.ts"; import { createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts"; import { legacyLogQueries } from "./legacy-query.ts"; import { seedLogVolume } from "./volume-seed.ts"; // 改写后的日志查询与改写前(先算派生列再筛选)在同一份小数据上逐条一致:筛选、关键词、对象、时间、游标分页。 const hasPg = await pgAvailable(); describe.skipIf(!hasPg)("日志查询改写前后口径一致", () => { let h: Harness; let feedId = ""; let downloadId = ""; let agentId = ""; beforeAll(async () => { h = await createHarness({ engine: true }); await seedLogVolume(h.db, h.engineSchema, 400); const wr = sql.raw(`"${h.engineSchema}".workflow_runs`); feedId = (await sql<{ id: string }>`select id::text from feed_source where name like 'my%'`.execute(h.db)).rows[0]! .id; downloadId = (await sql<{ id: string }>`select id::text from download_task order by name limit 1`.execute(h.db)) .rows[0]!.id; agentId = ( await sql<{ id: string }>`select id::text from agent_run where status = 'failed' order by goal limit 1`.execute( h.db, ) ).rows[0]!.id; // 没有业务 id 的运行靠业务任务行(task.engine_run_id)关联对象:一条指向失败的智能体会话、一条指向下载任务 await sql`insert into task (kind, idempotency_key, engine_run_id, subject_type, subject_id) values ('agent', 'parity-1', 'vol-000060', 'agent_run', ${agentId}), ('download', 'parity-2', 'vol-000061', 'download_task', ${downloadId})`.execute(h.db); // 失败的智能体会话对应的引擎运行(完成)按智能体结果算失败;定时任务的手动重跑不算定时触发 await sql`update ${wr} set input = jsonb_build_object('input', jsonb_build_object('agentRunId', ${agentId}::text)) where id = 'vol-000077'`.execute(h.db); await sql`insert into tasks_run_meta (run_id, task_name, task_version, idempotency_key) values ('vol-000089', 'purgeTrash', 1, 'manual:1')`.execute(h.db); // 审计里指向下载任务的一条(按对象筛选时与任务一起列出) await sql`insert into audit_event (actor_type, actor_id, action, target_type, target_id, payload) values ('agent', 'run-1', 'capability.denied', 'download_task', ${downloadId}, '{"reason":"超出授权"}'::jsonb)`.execute( h.db, ); }, 120_000); afterAll(async () => { await h?.close(); }); const at = (hoursAgo: number) => new Date(Date.now() - hoursAgo * 3600_000).toISOString(); const filters = (): [string, Omit][] => [ ["全部", {}], ["含例行", { system: "1" }], ["error", { level: ["error"] }], ["warn", { level: ["warn"] }], ["success", { level: ["success"] }], ["running + info", { level: ["running", "info"] }], ["error 含例行", { level: ["error"], system: "1" }], ["task", { type: ["task"] }], ["task 含例行", { type: ["task"], system: "1" }], ["auto", { type: ["auto"], system: "1" }], ["auth", { type: ["auth"] }], ["audit", { type: ["audit"] }], ["task + auth + warn", { type: ["task", "auth"], level: ["warn"] }], ["近 6 小时", { from: at(6), system: "1" }], ["6 到 2 小时前", { from: at(6), to: at(2) }], ["q 业务标题", { q: "feeds", system: "1" }], ["q 下载名", { q: "下载 1" }], ["q 智能体目标", { q: "调查 1", system: "1" }], ["q 运行 id", { q: "VOL-00012", system: "1" }], ["q 工作流名", { q: "ingest" }], ["q 错误", { q: "失败原因 9", system: "1" }], ["q 审计负载", { q: "10.1.0.5" }], ["q 定时任务", { q: "超时", system: "1" }], ["q 例行定时", { q: "清理回收站", system: "1" }], ["q 字面 %", { q: "%" }], ["q 不命中", { q: "zz-nomatch", system: "1" }], ["对象 feed", { objectType: "feed_source", objectId: feedId }], ["对象 下载(只给 id)", { objectId: downloadId }], ["对象 下载 + 类型", { objectType: "download_task", objectId: downloadId }], ["对象 定时任务", { objectType: "scheduled_job", objectId: "trash.purge" }], ["对象 智能体", { objectType: "agent_run", objectId: agentId }], ["对象类型不存在", { objectType: "nope" }], ]; it("各种筛选一次取全:逐条一致", async () => { const next = logQueries(h.db, { engineSchema: h.engineSchema }); const old = legacyLogQueries(h.db, { engineSchema: h.engineSchema }); const empty: string[] = []; for (const [name, f] of filters()) { const a: LogPage = await next.list({ ...f, limit: 200 }); const b: LogPage = await old.list({ ...f, limit: 200 }); expect(a, name).toEqual(b); if (!a.items.length) empty.push(name); } // 对照有意义:除了故意不命中的,筛选都有结果 expect(empty).toEqual(["q 字面 %", "q 不命中", "对象类型不存在"]); }, 120_000); it("游标分页:每一页与改写前一致", async () => { const next = logQueries(h.db, { engineSchema: h.engineSchema }); const old = legacyLogQueries(h.db, { engineSchema: h.engineSchema }); for (const f of [{ system: "1" as const }, { level: ["error" as const] }, { q: "下载", system: "1" as const }]) { let cursor: string | null = null; let pages = 0; do { const a: LogPage = await next.list({ ...f, limit: 37, cursor }); const b: LogPage = await old.list({ ...f, limit: 37, cursor }); expect(a).toEqual(b); cursor = a.nextCursor; pages++; } while (cursor && pages < 40); expect(pages).toBeGreaterThan(1); } }, 120_000); });