fix: support fuzzy logs user search

This commit is contained in:
2026-05-08 20:20:24 +08:00
parent 63eec82931
commit b1539f3969
2 changed files with 63 additions and 6 deletions

View File

@@ -1,6 +1,9 @@
import { beforeEach, describe, expect, mock, test } from "bun:test"; import { beforeEach, describe, expect, mock, test } from "bun:test";
const queryMock = mock(async () => [ type QueryRow = Record<string, unknown>;
type QueryParams = Array<string | number | boolean | null>;
const queryMock = mock(async (): Promise<QueryRow[]> => [
{ {
date: "2026-04-01 13:00:00", date: "2026-04-01 13:00:00",
calls: 1, calls: 1,
@@ -16,12 +19,12 @@ mock.module("./db", () => ({
query: queryMock, query: queryMock,
})); }));
const { getTrends, getUserDetail } = await import("./queries"); const { getLogs, getTrends, getUserDetail } = await import("./queries");
describe("getTrends", () => { describe("getTrends", () => {
beforeEach(() => { beforeEach(() => {
queryMock.mockClear(); queryMock.mockClear();
queryMock.mockImplementation(async () => [ queryMock.mockImplementation(async (): Promise<QueryRow[]> => [
{ {
date: "2026-04-01 13:00:00", date: "2026-04-01 13:00:00",
calls: 1, calls: 1,
@@ -62,7 +65,7 @@ describe("getUserDetail", () => {
}); });
test("returns token breakdown with nested model rows for user details", async () => { test("returns token breakdown with nested model rows for user details", async () => {
queryMock.mockImplementation(async (sql: string) => { queryMock.mockImplementation(async (sql: string): Promise<QueryRow[]> => {
if (sql.includes("token_models AS")) { if (sql.includes("token_models AS")) {
return [ return [
{ token_name: "prod-key", model: "claude-sonnet-4", calls: 3, tokens: 100, cache_creation: 5, cache_read: 7, quota: 50 }, { token_name: "prod-key", model: "claude-sonnet-4", calls: 3, tokens: 100, cache_creation: 5, cache_read: 7, quota: 50 },
@@ -123,3 +126,51 @@ describe("getUserDetail", () => {
expect(queryMock.mock.calls.some(([sql]) => String(sql).includes("token_name"))).toBe(true); expect(queryMock.mock.calls.some(([sql]) => String(sql).includes("token_name"))).toBe(true);
}); });
}); });
describe("getLogs", () => {
beforeEach(() => {
queryMock.mockClear();
queryMock.mockImplementation(async (sql: string): Promise<QueryRow[]> => {
if (sql.includes("SELECT COUNT(*)::int as total")) {
return [{ total: 0 }];
}
if (sql.includes("SELECT id, display_name FROM users")) {
return [];
}
if (sql.includes("SELECT id, name FROM channels")) {
return [];
}
return [];
});
});
test("filters logs by fuzzy display name through the users table", async () => {
await getLogs({ startTs: 501, endTs: 601, username: "张三" });
const countCall = queryMock.mock.calls[0];
const countSql = String(countCall[0]);
const countParams = countCall[1] as QueryParams;
expect(countSql).toContain("username ILIKE $3");
expect(countSql).toContain("SELECT id FROM users");
expect(countSql).toContain("display_name ILIKE $3");
expect(countSql).toContain("users.username ILIKE $3");
expect(countParams).toEqual([501, 601, "%张三%"]);
});
test("uses the same fuzzy user filter for paginated log rows", async () => {
await getLogs({ page: 2, pageSize: 25, username: "adm" });
const dataCall = queryMock.mock.calls[3];
const dataSql = String(dataCall[0]);
const dataParams = dataCall[1] as QueryParams;
expect(dataSql).toContain("username ILIKE $1");
expect(dataSql).toContain("display_name ILIKE $1");
expect(dataSql).toContain("users.username ILIKE $1");
expect(dataParams).toEqual(["%adm%", 25, 25]);
});
});

View File

@@ -692,8 +692,14 @@ export async function getLogs(options: {
let where = timeWhere(params, options.startTs, options.endTs); let where = timeWhere(params, options.startTs, options.endTs);
if (options.username) { if (options.username) {
params.push(options.username); params.push(`%${options.username}%`);
where += ` AND username = $${params.length}`; where += ` AND (
username ILIKE $${params.length}
OR user_id IN (
SELECT id FROM users
WHERE display_name ILIKE $${params.length} OR users.username ILIKE $${params.length}
)
)`;
} }
if (options.model) { if (options.model) {
params.push(options.model); params.push(options.model);