From a1633baa74291e0cff3b0c56bf1cb5e58cefc5fc Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:51:21 +0000 Subject: [PATCH] fix(cli): honor the JSON format alias in agent logs Verify JSON output through the real CLI and isolated daemon, including the documented --format json spelling. Co-authored-by: hadan8977 --- .../cli/src/commands/agent/logs-json.test.ts | 108 +++++++----------- packages/cli/src/commands/agent/logs.ts | 12 +- 2 files changed, 52 insertions(+), 68 deletions(-) diff --git a/packages/cli/src/commands/agent/logs-json.test.ts b/packages/cli/src/commands/agent/logs-json.test.ts index c60871f957..bbb69f9bd7 100644 --- a/packages/cli/src/commands/agent/logs-json.test.ts +++ b/packages/cli/src/commands/agent/logs-json.test.ts @@ -1,70 +1,50 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import type { Command } from "commander"; -import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { mkdir, mkdtemp } from "node:fs/promises"; +import { resolve } from "node:path"; +import { connectToDaemon } from "../../utils/client.js"; +import { runPaseoCli, startTestDaemon } from "../../../tests/helpers/test-daemon.js"; -const agentId = "11111111-1111-4111-8111-111111111111"; -const daemonTarget = { kind: "endpoint" as const, host: "example.test:12345" }; -const timeline: AgentTimelineItem[] = [ - { type: "user_message", text: "say hi" }, - { type: "reasoning", text: "Greet the user." }, - { type: "assistant_message", text: "Hi!", messageId: "msg_1" }, -]; -const timelineEntries = timeline.map((item) => ({ item, timestamp: "2026-10-19T00:00:00.000Z" })); +describe("paseo logs JSON output", () => { + let daemon: Awaited>; + let agentId: string; + let host: string; -vi.mock("../../utils/client.js", () => ({ - connectToDaemon: vi.fn(async () => ({ - fetchAgent: vi.fn(async () => ({ agent: { id: agentId } })), - fetchAgentTimeline: vi.fn(async () => ({ entries: timelineEntries })), - close: vi.fn(async () => undefined), - })), -})); + beforeAll(async () => { + await mkdir(".artifacts", { recursive: true }); + daemon = await startTestDaemon({ + paseoHome: await mkdtemp(resolve(".artifacts/logs-json-home-")), + workDir: await mkdtemp(resolve(".artifacts/logs-json-work-")), + }); + host = `127.0.0.1:${daemon.port}`; + const client = await connectToDaemon({ target: { kind: "endpoint", host } }); + try { + // Creating an idle agent needs no inference or external provider connection. + const agent = await client.createAgent({ + provider: "claude", + cwd: daemon.workDir, + title: "JSON logs", + }); + agentId = agent.id; + } finally { + await client.close(); + } + }, 60_000); -import { runLogsCommand } from "./logs.js"; - -async function captureStdout(run: () => Promise): Promise { - const lines: string[] = []; - const log = vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => { - lines.push(args.join(" ")); - }); - try { - await run(); - } finally { - log.mockRestore(); - } - return lines.join("\n"); -} - -describe("paseo logs --json", () => { - afterEach(() => { - vi.restoreAllMocks(); + afterAll(async () => { + await daemon?.stop(); }); - it("prints the timeline as JSON instead of the text transcript", async () => { - const stdout = await captureStdout(() => - runLogsCommand("1111", { daemonTarget, json: true }, {} as Command), - ); - - expect(JSON.parse(stdout)).toEqual({ agentId, items: timeline }); - }); - - it("applies --tail and --filter to the JSON items", async () => { - const stdout = await captureStdout(() => - runLogsCommand( - "1111", - { daemonTarget, json: true, tail: "1", filter: "text" }, - {} as Command, - ), - ); - - expect(JSON.parse(stdout)).toEqual({ agentId, items: [timeline[2]] }); - }); - - it("still prints the text transcript without --json", async () => { - const stdout = await captureStdout(() => - runLogsCommand("1111", { daemonTarget }, {} as Command), - ); - - expect(() => JSON.parse(stdout)).toThrow(); - expect(stdout).toContain("Hi!"); - }); + it.each([ + { label: "top-level --json", args: ["logs", "--json"] }, + { label: "agent logs --json", args: ["agent", "logs", "--json"] }, + { label: "--format json alias", args: ["logs", "--format", "json"] }, + ])( + "prints JSON through $label", + async ({ args }) => { + const result = await runPaseoCli(daemon, [...args, agentId, "--host", host]); + expect(result.exitCode, result.stderr).toBe(0); + expect(JSON.parse(result.stdout)).toEqual({ agentId, items: [] }); + }, + 30_000, + ); }); diff --git a/packages/cli/src/commands/agent/logs.ts b/packages/cli/src/commands/agent/logs.ts index 4e88f3c372..cf5a36d0d3 100644 --- a/packages/cli/src/commands/agent/logs.ts +++ b/packages/cli/src/commands/agent/logs.ts @@ -74,7 +74,7 @@ export function formatAgentActivityTranscript( * Picks the items `--tail` keeps, the same way the transcript does: the last n * timeline items, before any of them are dropped from the curated view. */ -export function selectTailItems( +function selectTailItems( timelineItems: AgentTimelineItem[], tailCount?: number, ): AgentTimelineItem[] { @@ -87,7 +87,7 @@ export function selectTailItems( return timelineItems.slice(-tailCount); } -export function formatAgentLogsJson( +function formatAgentLogsJson( agentId: string, timelineItems: AgentTimelineItem[], tailCount?: number, @@ -95,6 +95,10 @@ export function formatAgentLogsJson( return JSON.stringify({ agentId, items: selectTailItems(timelineItems, tailCount) }, null, 2); } +function usesJsonOutput(options: AgentLogsOptions): boolean { + return options.json === true || options.format?.trim().toLowerCase() === "json"; +} + type StreamingTextType = "assistant_message" | "reasoning"; const THOUGHT_PREFIX = "[Thought] "; @@ -346,7 +350,7 @@ export async function runLogsCommand( await client.close(); - if (options.json) { + if (usesJsonOutput(options)) { console.log(formatAgentLogsJson(resolvedId, timelineItems, tailCount)); return; } @@ -395,7 +399,7 @@ async function runFollowMode({ existingItems = existingItems.filter((item) => matchesFilter(item, options.filter)); } - const json = options.json === true; + const json = usesJsonOutput(options); // Print existing transcript (tail-like behavior) if (json) {