Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion skills/ocx/references/01_management_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Recent request log rows, filterable by provider, model, conversation, and status
| `--conversation` | string | Restrict to one conversation id (`--conversationId` is accepted too). |
| `--status` | string | An exact code (429) or a class (5xx). |
| `--limit` | number | Row cap; defaults to 200. |
| `--follow` | boolean | Stream new rows as JSONL; implies --jsonl. |
| `--follow` | boolean | Poll for new rows; add --jsonl to emit JSONL. |
| `--json` | boolean | Emit the server payload as JSON. |
| `--jsonl` | boolean | Emit one row per line. |

Expand Down
2 changes: 1 addition & 1 deletion src/cli/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ export const CAPABILITIES: readonly Capability[] = [
{ name: "--conversation", value: "string", summary: "Restrict to one conversation id (`--conversationId` is accepted too)." },
{ name: "--status", value: "string", summary: "An exact code (429) or a class (5xx)." },
{ name: "--limit", value: "number", summary: "Row cap; defaults to 200." },
{ name: "--follow", value: "boolean", summary: "Stream new rows as JSONL; implies --jsonl." },
{ name: "--follow", value: "boolean", summary: "Poll for new rows; add --jsonl to emit JSONL." },
{ name: "--json", value: "boolean", summary: "Emit the server payload as JSON." },
{ name: "--jsonl", value: "boolean", summary: "Emit one row per line." },
],
Expand Down
4 changes: 3 additions & 1 deletion src/cli/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
const limit = takeIntegerOption(args, "--limit", { min: 1 }) ?? 200;
rejectArgs(args, USAGE);
if (wantsJson && wantsJsonl) throw new CliUsageError("--json and --jsonl cannot be combined", USAGE);
if (follow && wantsJson) throw new CliUsageError("--follow uses --jsonl, not --json", USAGE);
if (follow && wantsJson) {
throw new CliUsageError("--follow cannot be combined with --json; use --jsonl for streaming JSONL", USAGE);
}
let seen = new Set<string>();
do {
const data = await runtimeRequest(`/api/logs${query({ provider, model, status, conversationId, limit })}`, {}, deps);
Expand Down
6 changes: 6 additions & 0 deletions tests/cli-capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ describe("capability table is a leaf data module", () => {
expect(CAPABILITIES.some(c => c.command[0] === "capabilities")).toBe(true);
});

test("logs follow does not claim to imply JSONL output", () => {
const logs = CAPABILITIES.find(c => c.command.length === 1 && c.command[0] === "logs");
const follow = logs?.flags.find(flag => flag.name === "--follow");
expect(follow?.summary).toBe("Poll for new rows; add --jsonl to emit JSONL.");
});

test("the check-only Codex CLI updater is declared as a local read capability", () => {
const cap = CAPABILITIES.find(c => c.command.join(" ") === "system codex-cli-update check");
expect(cap).toBeDefined();
Expand Down
62 changes: 61 additions & 1 deletion tests/cli-usage-report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* cost the server computes was discarded before reaching the terminal. These
* tests pin the cost down where a user can see it.
*/
import { describe, expect, test } from "bun:test";
import { describe, expect, spyOn, test } from "bun:test";

import { handleObserveCommand } from "../src/cli/observe";
import { formatUsageReport } from "../src/cli/usage-report";
Expand Down Expand Up @@ -222,3 +222,63 @@ describe("ocx logs --conversation", () => {
expect(out).not.toContain("conv=");
});
});

describe("ocx logs --follow output contract", () => {
test("--follow --json names the conflict without implying that follow enables JSONL", async () => {
const errors: string[] = [];
const originalError = console.error;
console.error = (...args: unknown[]) => { errors.push(args.map(String).join(" ")); };
try {
const code = await handleObserveCommand(
["logs", "--follow", "--json"],
{ baseUrl: "http://cli.test", fetchImpl: async () => new Response("[]") },
);
expect(code).toBe(2);
expect(errors.join("\n"))
.toContain("--follow cannot be combined with --json; use --jsonl for streaming JSONL");
} finally {
console.error = originalError;
}
});

test("--follow alone keeps human-readable rows", async () => {
const rows = [{
id: "row-1",
timestamp: "t0",
status: 200,
provider: "xai",
model: "grok-4.6",
durationMs: 12,
conversationId: "conv-7",
}];
const lines: string[] = [];
const errors: string[] = [];
const originalLog = console.log;
const originalError = console.error;
const sleep = spyOn(Bun, "sleep").mockImplementation(async () => {
throw new Error("stop after first follow poll");
});
console.log = (...args: unknown[]) => { lines.push(args.map(String).join(" ")); };
console.error = (...args: unknown[]) => { errors.push(args.map(String).join(" ")); };
try {
const code = await handleObserveCommand(
["logs", "--follow"],
{
baseUrl: "http://cli.test",
fetchImpl: async () => new Response(JSON.stringify(rows), {
status: 200,
headers: { "content-type": "application/json" },
}),
},
);
expect(code).toBe(1);
expect(lines).toEqual(["t0 200 xai/grok-4.6 12ms conv=conv-7"]);
expect(lines[0]?.startsWith("{")).toBe(false);
expect(errors.join("\n")).toContain("stop after first follow poll");
} finally {
console.log = originalLog;
console.error = originalError;
sleep.mockRestore();
}
});
});
Loading