Skip to content
Merged
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 cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bproxy/cli",
"version": "0.7.5",
"version": "0.8.0",
"private": true,
"type": "module",
"bin": {
Expand Down
1 change: 1 addition & 0 deletions cli/src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const T1 = "t1" as TabHandle;

function makeGlobals(overrides: Partial<ClientGlobalArgs> = {}): ClientGlobalArgs {
return {
nick: "halbot" as ClientGlobalArgs["nick"],
session: "m4q7z2",
timeout: "5000",
home: "/home/testuser/.bproxy",
Expand Down
1 change: 1 addition & 0 deletions cli/src/__tests__/command-test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function makeGlobals(
overrides: Partial<ClientGlobalArgs> = {},
): ClientGlobalArgs {
return {
nick: "halbot" as ClientGlobalArgs["nick"],
session: "m4q7z2",
timeout: "5000",
home,
Expand Down
38 changes: 29 additions & 9 deletions cli/src/__tests__/commands-read-parsing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,63 @@ import { extractGlobals, parseSessionId, parseTabHandle } from "../globals.js";
describe("extractGlobals", () => {
it("extracts all global args when present", () => {
const result = extractGlobals({
nick: "halbot",
session: "m4q7z2",
timeout: "5000",
home: "/home/testuser/.bproxy",
verbose: true,
});
expect(result).toEqual({
nick: "halbot",
session: "m4q7z2",
timeout: "5000",
home: "/home/testuser/.bproxy",
verbose: true,
});
});

it("returns undefined for missing string args", () => {
const result = extractGlobals({});
expect(result).toEqual({
session: undefined,
timeout: undefined,
home: undefined,
verbose: undefined,
});
it("fails when nick is missing", () => {
expect(() =>
extractGlobals(
{},
{
onUsageError: (message) => {
throw new Error(message);
},
},
),
).toThrow("Missing required --nick (-n). Every command requires an agent nickname.");
});

it("ignores non-string values for string args", () => {
it("ignores non-string values for non-nick args", () => {
const result = extractGlobals({
nick: "halbot",
session: 123,
timeout: true,
home: null,
verbose: "yes",
});
expect(result).toEqual({
nick: "halbot",
session: undefined,
timeout: undefined,
home: undefined,
verbose: undefined,
});
});

it("fails when nick format is invalid", () => {
expect(() =>
extractGlobals(
{ nick: "bad" },
{
onUsageError: (message) => {
throw new Error(message);
},
},
),
).toThrow("Invalid --nick value: bad. Must match /^[a-z][a-z0-9]{5}$/.");
});
});

describe("Phase 5 id parsing", () => {
Expand Down
16 changes: 14 additions & 2 deletions cli/src/__tests__/design-assertions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,23 @@ function setupTempHome(): string {
}

function makeGlobals(home: string): ClientGlobalArgs {
return { session: "m4q7z2", timeout: "5000", home, verbose: false };
return {
nick: "halbot" as ClientGlobalArgs["nick"],
session: "m4q7z2",
timeout: "5000",
home,
verbose: false,
};
}

function makeVerboseGlobals(home: string): ClientGlobalArgs {
return { session: "m4q7z2", timeout: "5000", home, verbose: true };
return {
nick: "halbot" as ClientGlobalArgs["nick"],
session: "m4q7z2",
timeout: "5000",
home,
verbose: true,
};
}

function successResponse(id: string) {
Expand Down
49 changes: 21 additions & 28 deletions cli/src/__tests__/screenshot-file.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { type SendOptions, sendAction } from "../client.js";
import { type ClientGlobalArgs, type SendOptions, sendAction } from "../client.js";
import { writeScreenshotFile } from "../screenshot-file.js";
import type { BproxyResponse } from "../types.js";
import { createTestStateDir } from "./helpers/test-state-dir.js";
Expand Down Expand Up @@ -122,23 +122,28 @@ function mockFetch(responseBody: unknown): typeof globalThis.fetch {
};
}

async function takeScreenshot(requestId: string) {
const home = setupHome();
const opts: SendOptions = {
fetch: mockFetch(screenshotResponse(requestId)),
requestId,
};
const plan = await sendAction(
"screenshot",
{},
{ home, nick: "halbot" as ClientGlobalArgs["nick"], session: "abc234" },
opts,
);
expect(plan.code).toBe(0);
const response = plan.stdout as BproxyResponse<"screenshot">;
expect(response.ok).toBe(true);
return response;
}

describe("screenshot --output-dir integration", () => {
it("command writes file and stdout has file path instead of base64", async () => {
const home = setupHome();
const outputDir = join(makeTempDir(), "screens");
const requestId = "ss-file-test-1";

const opts: SendOptions = {
fetch: mockFetch(screenshotResponse(requestId)),
requestId,
};

const plan = await sendAction("screenshot", {}, { home, session: "abc234" }, opts);

// Simulate what screenshot command does with --output-dir
expect(plan.code).toBe(0);
const response = plan.stdout as BproxyResponse<"screenshot">;
expect(response.ok).toBe(true);
const response = await takeScreenshot("ss-file-test-1");
if (!response.ok) return;

const result = writeScreenshotFile(outputDir, response.data.base64, response.data.format);
Expand All @@ -159,19 +164,7 @@ describe("screenshot --output-dir integration", () => {
});

it("without --output-dir, base64 remains in stdout", async () => {
const home = setupHome();
const requestId = "ss-no-dir-1";

const opts: SendOptions = {
fetch: mockFetch(screenshotResponse(requestId)),
requestId,
};

const plan = await sendAction("screenshot", {}, { home, session: "abc234" }, opts);

expect(plan.code).toBe(0);
const response = plan.stdout as BproxyResponse<"screenshot">;
expect(response.ok).toBe(true);
const response = await takeScreenshot("ss-no-dir-1");
if (!response.ok) return;
expect(response.data.base64).toBe(INT_TINY_PNG);
});
Expand Down
51 changes: 42 additions & 9 deletions cli/src/__tests__/smoke.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* - Stop through CLI, verify status becomes running:false
*/
import { execSync, spawn } from "node:child_process";
import { existsSync, rmSync, statSync } from "node:fs";
import { existsSync, rmSync, statSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import WebSocket from "ws";
Expand Down Expand Up @@ -120,6 +120,32 @@ describe("CLI integration smoke", () => {

beforeEach(() => {
tempHome = createTestStateDir("bproxy-smoke-");
// Relax safety guards so sequential CLI calls don't hit minInterval rejection
writeFileSync(
join(tempHome, "config.json"),
JSON.stringify({
pacing: {
human: {
navigate: { min: 1, max: 2 },
scroll: { min: 1, max: 2 },
interaction: { min: 1, max: 2 },
fill: { min: 1, max: 2 },
},
fast: {
navigate: { min: 1, max: 2 },
scroll: { min: 1, max: 2 },
interaction: { min: 1, max: 2 },
fill: { min: 1, max: 2 },
},
},
safety: {
minInterval: { ms: 1 },
rateCap: { requestsPerMinute: 600 },
errorDelay: { minMs: 1, maxMs: 1 },
metronome: { tolerance: 0.1, consecutiveEqual: 100, maxIntervalMs: 60000 },
},
}),
);
});

afterEach(async () => {
Expand Down Expand Up @@ -176,7 +202,7 @@ describe("CLI integration smoke", () => {
it("session list returns valid JSON without extension", () => {
runCli(["service", "start"], tempHome);

const result = runCli(["session", "list"], tempHome);
const result = runCli(["session", "list", "-n", "halbot"], tempHome);
expect(result.exitCode).toBe(0);

const parsed = parseJson(result.stdout) as Record<string, unknown>;
Expand All @@ -189,7 +215,10 @@ describe("CLI integration smoke", () => {
it("session create and close work against running daemon", () => {
runCli(["service", "start"], tempHome);

const createResult = runCli(["session", "create", "--label", "research"], tempHome);
const createResult = runCli(
["session", "create", "-n", "halbot", "--label", "research"],
tempHome,
);
expect(createResult.exitCode).toBe(0);
const createParsed = parseJson(createResult.stdout) as Record<string, unknown>;
expect(createParsed["ok"]).toBe(true);
Expand All @@ -198,7 +227,7 @@ describe("CLI integration smoke", () => {
expect(createData["label"]).toBe("research");

const sessionId = createData["session"] as string;
const closeResult = runCli(["session", "close", "-s", sessionId], tempHome);
const closeResult = runCli(["session", "close", "-n", "halbot", "-s", sessionId], tempHome);
expect(closeResult.exitCode).toBe(0);
const closeParsed = parseJson(closeResult.stdout) as Record<string, unknown>;
expect(closeParsed["ok"]).toBe(true);
Expand All @@ -209,7 +238,7 @@ describe("CLI integration smoke", () => {
it("debug status returns daemon info without extension", () => {
runCli(["service", "start"], tempHome);

const result = runCli(["status"], tempHome);
const result = runCli(["status", "-n", "halbot"], tempHome);
expect(result.exitCode).toBe(0);

const parsed = parseJson(result.stdout) as Record<string, unknown>;
Expand All @@ -224,7 +253,7 @@ describe("CLI integration smoke", () => {
it("debug last returns valid response structure", () => {
runCli(["service", "start"], tempHome);

const result = runCli(["debug", "last", "--count", "5"], tempHome);
const result = runCli(["debug", "last", "-n", "halbot", "--count", "5"], tempHome);
expect(result.exitCode).toBe(0);

const parsed = parseJson(result.stdout) as Record<string, unknown>;
Expand Down Expand Up @@ -315,20 +344,24 @@ describe("CLI integration smoke", () => {
);
});

const createResult = runCli(["session", "create"], tempHome);
const createResult = runCli(["session", "create", "-n", "halbot"], tempHome);
expect(createResult.exitCode).toBe(0);
const createParsed = parseJson(createResult.stdout) as Record<string, unknown>;
const sessionId = (createParsed["data"] as Record<string, unknown>)["session"] as string;

const openResult = await runCliAsync(
["tab", "open", "-s", sessionId, "--url", "https://example.com"],
["tab", "open", "-n", "halbot", "-s", sessionId, "--url", "https://example.com"],
tempHome,
10_000,
);
expect(openResult.exitCode).toBe(0);

// Send a forwarded read command (text) using async spawn
const textResult = await runCliAsync(["text", "-s", sessionId], tempHome, 10_000);
const textResult = await runCliAsync(
["text", "-n", "halbot", "-s", sessionId],
tempHome,
10_000,
);
expect(textResult.exitCode).toBe(0);

const textParsed = parseJson(textResult.stdout) as Record<string, unknown>;
Expand Down
5 changes: 5 additions & 0 deletions cli/src/bproxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const main = defineCommand({
description: "Browser proxy CLI for code agents",
},
args: {
nick: {
type: "string",
alias: "n",
description: "Agent nickname for request scoping",
},
session: {
type: "string",
alias: "s",
Expand Down
3 changes: 3 additions & 0 deletions cli/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const ABORT_BUFFER_MS = 2_000;
interface RequestContext {
requestId: string;
action: string;
nick: ClientGlobalArgs["nick"];
session: string;
port: number;
token: string;
Expand Down Expand Up @@ -119,6 +120,7 @@ function resolveContext(
return {
requestId: opts.requestId ?? generateRequestId(),
action,
nick: globals.nick,
session,
port,
token: tokenResult.token,
Expand Down Expand Up @@ -170,6 +172,7 @@ function buildRequest<A extends Action>(
protocol_version: 1,
id: ctx.requestId,
action,
nick: ctx.nick,
params,
session: ctx.session as BproxyRequest<A>["session"],
deadline: Date.now() + ctx.deadlineMs,
Expand Down
1 change: 1 addition & 0 deletions cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ async function probeDaemonHttp(
protocol_version: CLI_PROTOCOL_VERSION,
id: "doctor-check",
action: "debug.status",
nick: "doctor",
params: {},
session: "",
deadline: Date.now() + 5000,
Expand Down
Loading