From d0ade6b2b6db78c3f113f3f7713311886b15b003 Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Fri, 7 Aug 2026 13:28:43 +0300 Subject: [PATCH] fix(cli): resolve traces --server through the shared id-or-slug resolver `servers traces --server ` passed its option straight to the database layer, which compares it to `traces.server_id` with exact equality. That column always holds the full UUID, so a slug or a short ID matched nothing and the command printed an empty table at rc=0. The traces table renders the truncated 8-char ID in its own SERVER column, so the value a reader copies off the screen was precisely the one that returned nothing. A real server addressed that way and a server that does not exist produced byte-identical output, which reads as "this server has no audit trail" when the trail exists. Resolve through the existing resolveServerOrExit helper, the same one every other --server site in this file already uses. That accepts full UUID, short ID, name and slug, and turns an unknown identifier into a stderr message at exit 1 instead of a silent empty result. Also aligns the --server help string with trace:add's documented contract. Regression covers both directions: full UUID (positive control), short ID, slug and name each return only their own server's traces, and an unknown identifier must exit non-zero. Asserting the exact event list rather than a non-empty result means a fix that merely dropped the filter would also fail. Scope: `operations --server` was measured and already resolves, so it does not share this defect. The identical unresolved call at src/mcp/tools/traces.ts:71 is noted on the task for a separate lane. Task: 9b7c68db-3ad5-4bd9-9d47-f50d7fa4804b Agent: vespasian --- src/cli/index.ts | 8 ++++++-- test/cli-integration.test.ts | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index a08b64d..e32067f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -997,7 +997,7 @@ program program .command("traces") .description("List audit trail entries") - .option("-s, --server ", "Filter by server") + .option("-s, --server ", "Filter by server ID, partial ID, or slug") .option("-a, --agent ", "Filter by agent") .option("-l, --limit ", `Limit rows shown (default: ${DEFAULT_LIST_LIMIT}; JSON keeps legacy ${LEGACY_JSON_TRACE_LIMIT})`) .option("--cursor ", "Zero-based row offset for pagination") @@ -1012,7 +1012,11 @@ program if (opts.agent) { fetched = listTracesByAgent(opts.agent, cursor + limit + (json ? 0 : 1), db); } else { - fetched = listTraces(opts.server, undefined, cursor + limit + (json ? 0 : 1), db); + // traces.server_id always holds the full UUID, so an unresolved slug or the + // 8-char ID this table renders would match nothing and print an empty result + // at rc=0 — indistinguishable from a server with no traces. + const serverId = opts.server ? resolveServerOrExit(opts.server, db).id : undefined; + fetched = listTraces(serverId, undefined, cursor + limit + (json ? 0 : 1), db); } const traces = fetched.slice(cursor, cursor + limit); if (json) { diff --git a/test/cli-integration.test.ts b/test/cli-integration.test.ts index a9854c9..5ea34c8 100644 --- a/test/cli-integration.test.ts +++ b/test/cli-integration.test.ts @@ -259,6 +259,44 @@ describe("CLI integration tests", () => { } }); + test("traces --server resolves full ID, short ID, slug, and name, and rejects unknown identifiers", async () => { + const { dbFlag, cleanup } = withTmpDb(); + try { + await run(`${CLI} ${dbFlag} servers:add -n "Alpha Server" --slug alpha-srv`); + await run(`${CLI} ${dbFlag} servers:add -n "Beta Server" --slug beta-srv`); + await run(`${CLI} ${dbFlag} trace:add --server alpha-srv --event "alpha.event"`); + await run(`${CLI} ${dbFlag} trace:add --server beta-srv --event "beta.event"`); + + const { stdout: allJson } = await run(`${CLI} ${dbFlag} traces --json`); + const all = JSON.parse(allJson) as { event: string; server_id: string }[]; + expect(all.map(t => t.event).sort()).toEqual(["alpha.event", "beta.event"]); + const alphaId = all.find(t => t.event === "alpha.event")!.server_id; + + const events = async (identifier: string) => { + const { stdout } = await run(`${CLI} ${dbFlag} traces --server ${identifier} --json`); + return (JSON.parse(stdout) as { event: string }[]).map(t => t.event); + }; + + // Positive control: the full UUID already resolved before this fix, so a + // failure here means the fixture or the harness is wrong, not the filter. + expect(await events(alphaId)).toEqual(["alpha.event"]); + + // The short ID is the value the traces table itself renders in its SERVER column. + expect(await events(alphaId.slice(0, 8))).toEqual(["alpha.event"]); + + expect(await events("alpha-srv")).toEqual(["alpha.event"]); + expect(await events('"Beta Server"')).toEqual(["beta.event"]); + + // An unknown identifier must fail loudly. Printing an empty table at rc=0 is + // indistinguishable from a real server that genuinely has no traces. + const unknown = await runExpectFailure(`${CLI} ${dbFlag} traces --server zzz-not-a-server-zzz`); + expect(unknown.stderr).toContain("Server not found"); + expect(unknown.code).not.toBe(0); + } finally { + cleanup(); + } + }); + test("list commands default to compact paginated output while JSON can stay complete", async () => { const { dbFlag, cleanup } = withTmpDb(); try {