Files
UHD_Media_Tool/apps/server/test/logs/logs-legacy-parity.test.ts
T
rootandClaude Opus 5.5 2bd09c89e9 fix(logs): 日志查询先按原始列筛选取前 n 条再算标题
带筛选(级别、关键词、类型)时原查询要对全部引擎运行算标题子查询与 search 拼接,
3 万条运行上超时。改为每个来源两段:第一段只用原始列与一次性算好的业务引用筛选并
order + limit,第二段只对取出的行算标题 / 对象标签;关键词先在业务对象小表里查出
命中 id 集合;查询内关掉 JIT。新增 3 万条运行的耗时测试与改写前口径对照测试。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 19:34:07 +08:00

118 lines
5.7 KiB
TypeScript

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<LogQueryInput, "limit">][] => [
["全部", {}],
["含例行", { 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);
});