Skip to content
Closed
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
281 changes: 281 additions & 0 deletions sdk/typescript/tests-ts/cli-progress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import { stripVTControlCharacters } from "node:util";
import { describe, expect, test } from "bun:test";
import { main } from "../src/cli.js";
import { capture, dependencies, fakeResult } from "./cli-fixtures.js";

describe("CLI scan progress", () => {
test("uses plain scan progress in headless, CI, and noninteractive terminals", async () => {
Comment on lines +6 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Remove the already-covered progress suite

Every test in this suite duplicates an existing test byte-for-byte in tests-ts/cli.test.ts (at lines 1503, 1559, 1590, 4273, and 4311). Because bun test ./tests-ts discovers both files, the five scenarios now execute twice without adding coverage, increasing suite time and leaving two copies that can diverge when progress behavior changes.

Useful? React with 👍 / 👎.

for (const { options, environment, isTTY } of [
{ options: ["--headless"], environment: {}, isTTY: true },
{ options: [], environment: { CI: "true" }, isTTY: true },
{ options: [], environment: { TERM: "dumb" }, isTTY: true },
{ options: [], environment: {}, isTTY: false },
]) {
const stdout = capture();
const stderr = capture(isTTY);
const result = fakeResult([], "complete", {
input_tokens: 1_250,
cached_input_tokens: 200,
output_tokens: 30,
});
let timers = 0;
const deps = dependencies({
environment,
result,
costUpdates: [result.cost!],
scanProgress: [
{ phase: "discovery", filesCompleted: 3, filesTotal: 8 },
],
workerStatuses: [
{ kind: "dispatch", phase: "file_review", planned: 2, started: 2 },
],
});
deps.setInterval = () => {
timers += 1;
return {} as NodeJS.Timeout;
};

expect(
await main(
["scan", ".", ...options],
stdout.stream,
stderr.stream,
deps,
),
).toBe(0);
expect(stderr.text()).toContain("[00:00] Preparing scan");
expect(stderr.text()).toContain(
"Scan phase: reviewing files (3/8 files).",
);
expect(stderr.text()).toContain(
"Scan phase: reviewing files (2 workers).",
);
expect(stderr.text()).toContain(
"Running scan: reviewing files | Workers: 2/2 | Files: 3/8 | Tokens: 1,250 input, 200 cached, 30 output | Cost: $0.00625",
);
expect(stderr.text()).not.toContain("CODEX SECURITY");
expect(stderr.text()).not.toContain("\u001B");
expect(stderr.text()).not.toContain("\r");
expect(timers).toBe(0);
}
});

test("uses plain progress when rerunning scans in CI", async () => {
const stdout = capture();
const stderr = capture(true);

expect(
await main(
["scans", "rerun", "scan-original"],
stdout.stream,
stderr.stream,
dependencies({
environment: { CI: "true" },
onWorkbench: () => ({
recipe: {
repository: "/original/repository",
target: { kind: "repository", paths: [] },
mode: "standard",
config: {},
},
}),
scanProgress: [
{ phase: "discovery", filesCompleted: 3, filesTotal: 8 },
],
}),
),
).toBe(0);
expect(stderr.text()).toContain("[00:00] Preparing scan");
expect(stderr.text()).toContain("Scan phase: reviewing files (3/8 files).");
expect(stderr.text()).not.toContain("\u001B");
expect(stderr.text()).not.toContain("\r");
});

test("keeps terminal scans in one live dashboard", async () => {
const stdout = capture();
const stderr = capture(true);
const result = fakeResult([], "complete", {
input_tokens: 17_985,
cached_input_tokens: 10_496,
output_tokens: 236,
});

expect(
await main(
[
"scan",
"/code/juice-shop",
"--model",
"gpt-5.6-terra",
"--codex",
'model_reasoning_effort="low"',
"--max-cost",
"2",
],
stdout.stream,
stderr.stream,
dependencies({
environment: { NO_COLOR: "1" },
result,
activities: [
{
id: "read-1",
kind: "command",
status: "running",
description:
'nl -ba "$CODEX_SECURITY_REPOSITORY/routes/login.ts"',
paths: ["routes/login.ts"],
},
{
id: "worker-1:read-1",
kind: "command",
status: "running",
description:
'rg -n "password" "$CODEX_SECURITY_REPOSITORY/routes/login.ts"',
paths: ["routes/login.ts"],
worker: 1,
},
{
id: "worker-1:thinking-1",
kind: "reasoning",
status: "completed",
description: "Following the login request into the SQL query.",
paths: [],
worker: 1,
},
{
id: "worker-1:message-1",
kind: "message",
status: "completed",
description: "The request reaches the query without validation.",
paths: [],
worker: 1,
},
{
id: "request-1",
kind: "command",
status: "running",
description:
'curl -H "Authorization: Bearer sk-proj-SYNTHETIC_OPENAI_VALUE_123"',
paths: [],
},
],
costUpdates: [result.cost!],
scanProgress: [
{ phase: "preflight", filesCompleted: 0, filesTotal: 1_258 },
{ phase: "discovery", filesCompleted: 3, filesTotal: 1_258 },
],
workerStatuses: [
{ kind: "dispatch", phase: "file_review", planned: 6, started: 3 },
],
}),
),
).toBe(0);

const text = stripVTControlCharacters(stderr.text());
expect(text).toContain(
"CODEX SECURITY · juice-shop · gpt-5.6-terra (low)",
);
expect(text).not.toContain("ACTIVITY");
expect(text).not.toContain("events · live");
expect(text).not.toContain("WORKERS");
expect(text).toContain("routes/login.ts");
expect(text).toMatch(/\[\d{2}:\d{2}:\d{2}\]/u);
expect(text).toContain(
'worker 1 · rg -n "password" "$CODEX_SECURITY_REPOSITORY/routes/login.ts"',
);
expect(text).toContain(
"worker 1 · Following the login request into the SQL query.",
);
expect(text).toContain(
"worker 1 · The request reaches the query without validation.",
);
expect(text).not.toContain("thinking ·");
expect(text).not.toContain("said ·");
expect(text).toContain('curl -H "Authorization: Bearer [redacted]"');
expect(text).not.toContain("SYNTHETIC_OPENAI_VALUE_123");
expect(text).not.toContain("Building the file inventory");
expect(text).not.toContain("Running a scan command");
expect(text).toContain("3 / 1,258 reviewed");
expect(text).not.toContain("opened");
expect(text).not.toContain("3 / 6 active");
expect(text).toContain("17,985 in · 10,496 cached · 236 out");
expect(text).toContain("/ $2.00");
expect(stderr.text()).toContain("\u001B[?1049h");
expect(stderr.text()).toContain("\u001B[?1049l");
expect(text).not.toContain("Running scan: preflight");
expect(text).not.toContain("Estimated cost: $0.0248865 of $2.00 limit");
});

test("shows live stage, files, workers, tokens, and cost without a budget", async () => {
const stdout = capture();
const stderr = capture();
const result = fakeResult([], "complete", {
input_tokens: 1_250,
cached_input_tokens: 200,
output_tokens: 30,
});

expect(
await main(
["scan", ".", "--json"],
stdout.stream,
stderr.stream,
dependencies({
result,
costUpdates: [result.cost!],
scanProgress: [
{ phase: "discovery", filesCompleted: 0, filesTotal: 8 },
{ phase: "discovery", filesCompleted: 3, filesTotal: 8 },
],
workerStatuses: [
{ kind: "dispatch", phase: "file_review", planned: 6, started: 4 },
],
}),
),
).toBe(0);
expect(JSON.parse(stdout.text())).toEqual(result.toJSON());
expect(stderr.text()).toContain(
"Tokens: 1,250 input, 200 cached, 30 output. Estimated cost: $0.00625 USD.",
);
expect(stderr.text()).toContain("Scan phase: reviewing files (0/8 files).");
expect(stderr.text()).toContain("Scan phase: reviewing files (3/8 files).");
expect(stderr.text()).toContain(
"Running scan: reviewing files | Workers: 4/6 | Files: 3/8 | Tokens: 1,250 input, 200 cached, 30 output | Cost: $0.00625",
);
});

test("deduplicates live file progress and reports later scan phases", async () => {
const stdout = capture();
const stderr = capture();
const discovery = {
phase: "discovery",
filesCompleted: 8,
filesTotal: 8,
} as const;

expect(
await main(
["scan", ".", "--json"],
stdout.stream,
stderr.stream,
dependencies({
scanProgress: [
discovery,
discovery,
{ phase: "validation", filesCompleted: 8, filesTotal: 8 },
{ phase: "reporting", filesCompleted: 8, filesTotal: 8 },
],
}),
),
).toBe(0);
expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON());
expect(
stderr.text().match(/Scan phase: reviewing files \(8\/8 files\)/g),
).toHaveLength(1);
expect(stderr.text()).toContain(
"Scan phase: validating findings (8/8 files).",
);
expect(stderr.text()).toContain("Scan phase: writing report (8/8 files).");
});
});
Loading