Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/sidecar/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "./services/infra/logger";
import { assertSidecarNativeRuntime } from "./services/infra/native-runtime";
import { createProcessRpcTransport, MAX_RPC_MESSAGE_BYTES } from "./rpc/process-transport";
import { classifyBrowserRpcResponse } from "./rpc/browser-rpc-sequence";
import { browserRpcErrorFromPayload, classifyBrowserRpcResponse } from "./rpc/browser-rpc-sequence";
import { createReverseRpcRenderClient } from "./services/agent-runtime/tools/web/reverse-rpc-render-client";
import { setSidecarRenderClient } from "./services/agent-runtime/tools/web/render-client-holder";
import { setPersistedSettingsMutationWriter } from "./services/system/settings-store";
Expand Down Expand Up @@ -204,7 +204,7 @@ async function handleRpcLine(line: string): Promise<void> {
return;
}
const response = responsePayload;
if (response.error) pending.reject(new Error("browser request failed"));
if (response.error) pending.reject(browserRpcErrorFromPayload(response.error));
else pending.resolve(response.result);
return;
}
Expand Down
18 changes: 17 additions & 1 deletion apps/sidecar/src/rpc/browser-rpc-sequence.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { classifyBrowserRpcResponse } from "./browser-rpc-sequence";
import { browserRpcErrorFromPayload, classifyBrowserRpcResponse } from "./browser-rpc-sequence";

describe("classifyBrowserRpcResponse(#156 序列号单调判定)", () => {
test("正常路径:MAC 有效且序号 = 当前 +1 → advance", () => {
Expand All @@ -21,3 +21,19 @@ describe("classifyBrowserRpcResponse(#156 序列号单调判定)", () => {
expect(classifyBrowserRpcResponse(9, false, 0)).toBe("reject-pending");
});
});

describe("browserRpcErrorFromPayload(#252 错误码传播)", () => {
test("保留 Desktop 返回的稳定错误码", () => {
const error = browserRpcErrorFromPayload({ code: "actionability_failed" });

expect(error.code).toBe("actionability_failed");
expect(error.message).toBe("actionability_failed");
});

test("无效错误载荷安全退化为 browser_internal_error", () => {
const error = browserRpcErrorFromPayload(undefined);

expect(error.code).toBe("browser_internal_error");
expect(error.message).toBe("browser_internal_error");
});
});
16 changes: 16 additions & 0 deletions apps/sidecar/src/rpc/browser-rpc-sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,19 @@ export function classifyBrowserRpcResponse(
if (!macOk || sequence <= inboundSequence) return "reject-pending";
return "advance";
}

export class BrowserRpcError extends Error {
readonly code: string;

constructor(code: string, message = code) {
super(message);
this.name = "BrowserRpcError";
this.code = code;
}
}

export function browserRpcErrorFromPayload(error: { code?: unknown; message?: unknown } | undefined): BrowserRpcError {
const code = typeof error?.code === "string" && error.code ? error.code : "browser_internal_error";
const message = typeof error?.message === "string" && error.message ? error.message : code;
return new BrowserRpcError(code, message);
}
13 changes: 13 additions & 0 deletions apps/sidecar/src/services/browser/browser-broker.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { BrowserBroker } from "./browser-broker";
import { BrowserRpcError } from "../../rpc/browser-rpc-sequence";

test("extension backend is absent until browser/chrome plugins, setting, and live host agree", async () => {
const calls: unknown[] = [];
Expand All @@ -18,6 +19,18 @@ test("extension backend is absent until browser/chrome plugins, setting, and liv
assert.equal(calls.length, 0);
});

test("broker preserves a structured Desktop browser error code", async () => {
const broker = new BrowserBroker({
request: async () => { throw new BrowserRpcError("actionability_failed", "browser request failed"); },
});
broker.setPluginState({ browserEnabled: true });

await assert.rejects(
() => broker.dispatch({ method: "playwright_locator_fill", params: { tabId: "tab-1", locator: { version: 1, steps: [{ kind: "css", selector: "#kw" }] }, value: "agent loop" }, browserSessionId: "s", browserTurnId: "t" }),
/actionability_failed/,
);
});

test("connected Chrome cookie export is capability-gated, user-only, and paged", async () => {
const calls: any[] = []
const broker = new BrowserBroker(
Expand Down
5 changes: 4 additions & 1 deletion apps/sidecar/src/services/browser/browser-broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,10 @@ function stableJson(value: unknown): string {
return JSON.stringify(value) ?? "null"
}
function stableBrowserErrorCode(error: unknown): string {
const value = error instanceof Error ? error.message : ""
const structuredCode = error && typeof error === "object" && typeof (error as { code?: unknown }).code === "string"
? (error as { code: string }).code
: ""
const value = structuredCode || (error instanceof Error ? error.message : "")
return new Set(["browser_unavailable", "invalid_browser_request", "invalid_url", "private_origin_confirmation_required", "stale_target", "tab_not_found", "tab_generation_changed", "confirmation_unavailable", "reference_grant_expired", "action_denied", "strict_locator_violation", "actionability_failed", "dialog_blocking", "unsupported", "executed_unknown"]).has(value) ? value : "browser_internal_error"
}

Expand Down
Loading