From 1be68bd2c6700fcf64f7bde0d006666932709458 Mon Sep 17 00:00:00 2001 From: NianJiuZst <3235467914@qq.com> Date: Fri, 10 Jul 2026 22:11:51 +0800 Subject: [PATCH 1/5] fix(extension): make tool cancellation cooperative --- .../session-manager/__tests__/manager.test.ts | 32 +++++++++++ apps/extension/src/session-manager/manager.ts | 52 +++++++++++++----- .../src/tools/__tests__/dispatcher.test.ts | 20 ++++--- .../src/tools/__tests__/observation.test.ts | 35 ++++++++++++ apps/extension/src/tools/console.ts | 3 ++ apps/extension/src/tools/dispatcher.ts | 53 +++++-------------- apps/extension/src/tools/observation.ts | 10 ++++ apps/extension/src/tools/session.ts | 10 +++- 8 files changed, 155 insertions(+), 60 deletions(-) diff --git a/apps/extension/src/session-manager/__tests__/manager.test.ts b/apps/extension/src/session-manager/__tests__/manager.test.ts index cfeed29..292ba0d 100644 --- a/apps/extension/src/session-manager/__tests__/manager.test.ts +++ b/apps/extension/src/session-manager/__tests__/manager.test.ts @@ -57,6 +57,38 @@ describe("SessionManager", () => { await expect(sm.start("aa11")).rejects.toThrow(/already exists/); }); + it("removes a newly created Agent Window when startup is aborted", async () => { + const aw = fakeAgentWindow(); + let resolveCreate: (windowId: number) => void = () => {}; + aw.createMock.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveCreate = resolve; + }), + ); + const sm = new SessionManager({ agentWindow: aw }); + const controller = new AbortController(); + const pending = sm.start("aa11", { signal: controller.signal }); + + controller.abort(); + resolveCreate(777); + + await expect(pending).rejects.toMatchObject({ name: "AbortError" }); + expect(aw.removeMock).toHaveBeenCalledWith(777); + expect(sm.has("aa11")).toBe(false); + }); + + it("removes an incomplete Agent Window when active-tab setup fails", async () => { + const aw = fakeAgentWindow(); + aw.ensureActiveTabMock.mockRejectedValueOnce(new Error("tab setup failed")); + const sm = new SessionManager({ agentWindow: aw }); + + await expect(sm.start("aa11")).rejects.toThrow("tab setup failed"); + + expect(aw.removeMock).toHaveBeenCalledWith(100); + expect(sm.has("aa11")).toBe(false); + }); + it("stop() closes the Agent Window and forgets the session", async () => { const aw = fakeAgentWindow(); const sm = new SessionManager({ agentWindow: aw }); diff --git a/apps/extension/src/session-manager/manager.ts b/apps/extension/src/session-manager/manager.ts index 0cbdb72..87e0a72 100644 --- a/apps/extension/src/session-manager/manager.ts +++ b/apps/extension/src/session-manager/manager.ts @@ -25,6 +25,16 @@ export interface SessionManagerOptions { now?: () => number; } +export interface SessionStartOptions { + signal?: AbortSignal; +} + +function sessionStartAbortError(): Error { + const err = new Error("session_start aborted"); + err.name = "AbortError"; + return err; +} + /** * Owner of all live agent sessions inside the extension. * @@ -128,22 +138,38 @@ export class SessionManager { * Returns the created window id so callers can echo it back to the * daemon in the `tool.session_start` reply. */ - async start(sessionId: string): Promise { + async start(sessionId: string, options: SessionStartOptions = {}): Promise { if (this.sessions.has(sessionId)) { throw new Error(`[bh] session ${sessionId} already exists`); } - const windowId = await this.agentWindow.create(AGENT_WINDOW_HOME); - await this.agentWindow.ensureActiveTab(windowId, AGENT_WINDOW_HOME); - const ctx: SessionContext = { - sessionId, - agentWindowId: windowId, - refStore: new RefStore(), - borrowedTabs: new Map(), - createdAtMs: this.now(), - }; - this.sessions.set(sessionId, ctx); - this.windowIndex.set(windowId, sessionId); - return ctx; + if (options.signal?.aborted) throw sessionStartAbortError(); + + let windowId: number | null = null; + try { + windowId = await this.agentWindow.create(AGENT_WINDOW_HOME); + if (options.signal?.aborted) throw sessionStartAbortError(); + await this.agentWindow.ensureActiveTab(windowId, AGENT_WINDOW_HOME); + if (options.signal?.aborted) throw sessionStartAbortError(); + const ctx: SessionContext = { + sessionId, + agentWindowId: windowId, + refStore: new RefStore(), + borrowedTabs: new Map(), + createdAtMs: this.now(), + }; + this.sessions.set(sessionId, ctx); + this.windowIndex.set(windowId, sessionId); + return ctx; + } catch (err) { + if (windowId !== null) { + try { + await this.agentWindow.remove(windowId); + } catch (cleanupErr) { + console.warn(`[bh] failed to remove incomplete Agent Window ${windowId}`, cleanupErr); + } + } + throw err; + } } /** diff --git a/apps/extension/src/tools/__tests__/dispatcher.test.ts b/apps/extension/src/tools/__tests__/dispatcher.test.ts index 8241e61..a8fe0cf 100644 --- a/apps/extension/src/tools/__tests__/dispatcher.test.ts +++ b/apps/extension/src/tools/__tests__/dispatcher.test.ts @@ -334,23 +334,31 @@ describe("ToolDispatcher", () => { await flushMicrotasks(); expect(ac?.signal.aborted).toBe(true); - // Cancel ack arrived synchronously; the slow tool replies with - // `cancelled` once the dispatcher's race observes the abort. + // Cancel ack arrives synchronously, but the original RPC must not reply + // until the in-progress window creation has completed and been rolled back. const ack = sent.find( (m) => typeof (m as { id?: string }).id === "string" && (m as { id: string }).id === "cancel-1", ); expect(ack).toEqual({ id: "cancel-1", result: { cancelled: true } }); + expect( + sent.find( + (m) => typeof (m as { id?: string }).id === "string" && (m as { id: string }).id === "r-1", + ), + ).toBeUndefined(); + expect(dispatcher.inflightAbortControllers.has("r-1")).toBe(true); + + resolveCreate(9999); + await flushMicrotasks(); + const slow = sent.find( (m) => typeof (m as { id?: string }).id === "string" && (m as { id: string }).id === "r-1", ); expect(slow).toMatchObject({ id: "r-1", error: { code: "cancelled" } }); expect(dispatcher.inflightAbortControllers.has("r-1")).toBe(false); - - // Drain the dangling create promise so vitest does not warn. - resolveCreate(9999); - await flushMicrotasks(); + expect(sessions.has("aa44")).toBe(false); + expect(sessions.list()).toEqual([]); }); it("cancel for an unknown rpc_id replies with cancelled=false", async () => { diff --git a/apps/extension/src/tools/__tests__/observation.test.ts b/apps/extension/src/tools/__tests__/observation.test.ts index a009e19..59c091b 100644 --- a/apps/extension/src/tools/__tests__/observation.test.ts +++ b/apps/extension/src/tools/__tests__/observation.test.ts @@ -524,6 +524,41 @@ describe("handleSnapshot", () => { expect(ctx.refStore.resolve("e2")).toBe(200); }); + it("does not overwrite refs when cancellation arrives during CDP capture", async () => { + const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); + const ctx = await sm.start("aa11"); + ctx.refStore.set("e1", 999, { tabId: 4 }); + const controller = new AbortController(); + let resolveTree: (value: { nodes: CdpAxNode[] }) => void = () => {}; + const deps = makeDeps([]); + deps.send.mockImplementation(async (_tabId: number, method: string) => { + if (method === "Accessibility.enable") return {}; + if (method === "Accessibility.getFullAXTree") { + return new Promise((resolve) => { + resolveTree = resolve; + }); + } + throw new Error(`unexpected CDP method ${method}`); + }); + + const pending = handleSnapshot(sm, { session_id: "aa11" }, deps, controller.signal); + await vi.waitFor(() => expect(deps.send).toHaveBeenCalledTimes(2)); + controller.abort(); + resolveTree({ + nodes: [ + { + nodeId: "new", + role: { type: "role", value: "button" }, + name: { type: "computedString", value: "New" }, + backendDOMNodeId: 123, + }, + ], + }); + + await expect(pending).resolves.toMatchObject({ code: "cancelled" }); + expect(ctx.refStore.resolve("e1")).toBe(999); + }); + it("resets the RefStore on every fresh snapshot", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); diff --git a/apps/extension/src/tools/console.ts b/apps/extension/src/tools/console.ts index efd4784..94f30ff 100644 --- a/apps/extension/src/tools/console.ts +++ b/apps/extension/src/tools/console.ts @@ -31,7 +31,9 @@ export async function handleConsole( manager: SessionManager, params: ConsoleParams, deps: ConsoleDeps = defaultConsoleDeps(), + signal?: AbortSignal, ): Promise { + if (signal?.aborted) return { code: "cancelled", message: "console aborted" }; const ctxOrErr = lookupSession(manager, params, "console"); if (isRpcError(ctxOrErr)) return ctxOrErr; const bounds = parseConsoleBounds(params); @@ -44,6 +46,7 @@ export async function handleConsole( try { await deps.cdp.ensureConsoleCapture(target.tabId); + if (signal?.aborted) return { code: "cancelled", message: "console aborted" }; deps.cdp.trackSessionTab?.(ctxOrErr.sessionId, target.tabId); return deps.cdp.consoleEntriesSince( target.tabId, diff --git a/apps/extension/src/tools/dispatcher.ts b/apps/extension/src/tools/dispatcher.ts index ca4c053..4355867 100644 --- a/apps/extension/src/tools/dispatcher.ts +++ b/apps/extension/src/tools/dispatcher.ts @@ -186,7 +186,10 @@ export class ToolDispatcher { let body: ResponseFrame; let startedSession: string | null = null; try { - const result = await Promise.race([this.invoke(req, ac.signal), abortPromise(ac.signal)]); + // Await the handler itself. A cancel trips its AbortSignal immediately, + // but the RPC response is not sent until the handler has stopped or + // completed any required compensation for an already-started side effect. + const result = await this.invoke(req, ac.signal); if (isRpcError(result)) { body = { id: req.id, error: result }; } else { @@ -251,7 +254,7 @@ export class ToolDispatcher { private async invoke(req: RequestFrame, signal: AbortSignal): Promise { switch (req.method) { case "tool.session_start": - return handleSessionStart(this.sessions, req.params as SessionStartParams); + return handleSessionStart(this.sessions, req.params as SessionStartParams, { signal }); case "tool.session_stop": return handleSessionStop(this.sessions, req.params as SessionStopParams, { cdp: this.cdp, @@ -259,18 +262,18 @@ export class ToolDispatcher { case "tool.tab_list": return handleTabList(this.sessions, req.params as TabListParams); case "tool.tab_create": - return handleTabCreate(this.sessions, req.params as TabCreateParams); + return handleTabCreate(this.sessions, req.params as TabCreateParams, { signal }); case "tool.tab_close": - return handleTabClose(this.sessions, req.params as TabCloseParams); + return handleTabClose(this.sessions, req.params as TabCloseParams, { signal }); case "tool.tab_select": - return handleTabSelect(this.sessions, req.params as TabSelectParams); + return handleTabSelect(this.sessions, req.params as TabSelectParams, { signal }); case "tool.tab_borrow": return handleTabBorrow(this.sessions, req.params as TabBorrowParams, { signal, approveBorrow: this.approveBorrow, }); case "tool.tab_return": - return handleTabReturn(this.sessions, req.params as TabReturnParams); + return handleTabReturn(this.sessions, req.params as TabReturnParams, { signal }); case "tool.screenshot": return handleScreenshot( this.sessions, @@ -278,24 +281,28 @@ export class ToolDispatcher { this.cdp ? { cdp: this.cdp, tabsApi: chromeTabsCaptureApi, captureApi: chromeTabsCaptureApi } : undefined, + signal, ); case "tool.console": return handleConsole( this.sessions, req.params as ConsoleParams, this.cdp ? { cdp: this.cdp, tabsApi: chromeTabsApi } : undefined, + signal, ); case "tool.snapshot": return handleSnapshot( this.sessions, req.params as SnapshotParams, this.cdp ? { cdp: this.cdp, tabsApi: chromeTabsCaptureApi } : undefined, + signal, ); case "tool.get_html": return handleGetHtml( this.sessions, req.params as GetHtmlParams, this.cdp ? { cdp: this.cdp, tabsApi: chromeTabsCaptureApi } : undefined, + signal, ); case "tool.navigate": return handleNavigate( @@ -406,41 +413,7 @@ function isRpcError(v: unknown): v is RpcError { ); } -/** - * Resolves never; rejects with `AbortLikeError` as soon as the signal - * fires (or immediately if it is already aborted). Used by the - * dispatcher to race the tool invocation so handlers without explicit - * signal plumbing still surface a `cancelled` reply promptly. - */ -function abortPromise(signal: AbortSignal): Promise { - return new Promise((_, reject) => { - if (signal.aborted) { - reject(new AbortLikeError()); - return; - } - signal.addEventListener( - "abort", - () => { - reject(new AbortLikeError()); - }, - { once: true }, - ); - }); -} - -/** - * Sentinel error class so [`isAbortLikeError`] can recognise our own - * race-rejection without confusing it with a real CDP failure. - */ -class AbortLikeError extends Error { - constructor() { - super("rpc aborted by daemon cancel"); - this.name = "BhAbortError"; - } -} - function isAbortLikeError(err: unknown): boolean { - if (err instanceof AbortLikeError) return true; if (err instanceof DOMException && err.name === "AbortError") return true; if (typeof err === "object" && err !== null && (err as { name?: string }).name === "AbortError") { return true; diff --git a/apps/extension/src/tools/observation.ts b/apps/extension/src/tools/observation.ts index 96b54d0..dacddda 100644 --- a/apps/extension/src/tools/observation.ts +++ b/apps/extension/src/tools/observation.ts @@ -150,7 +150,9 @@ export async function handleScreenshot( manager: SessionManager, params: ScreenshotParams, deps: ScreenshotDeps = defaultScreenshotDeps(), + signal?: AbortSignal, ): Promise { + if (signal?.aborted) return { code: "cancelled", message: "screenshot aborted" }; const ctxOrErr = lookupSession(manager, params, "screenshot"); if (isRpcError(ctxOrErr)) return ctxOrErr; const ctx = ctxOrErr; @@ -170,6 +172,7 @@ export async function handleScreenshot( deps.cdp.trackSessionTab?.(ctx.sessionId, target.tabId); const captured = await captureElementScreenshot(deps.cdp, target.tabId, node.backendNodeId); if (isRpcError(captured)) return captured; + if (signal?.aborted) return { code: "cancelled", message: "screenshot aborted" }; return withShotDialogs({ image_base64: captured.image_base64, width: captured.width, @@ -189,6 +192,7 @@ export async function handleScreenshot( try { const dataUrl = await deps.captureApi.captureVisibleTab(target.windowId, { format: "png" }); + if (signal?.aborted) return { code: "cancelled", message: "screenshot aborted" }; const image_base64 = stripDataUrlPrefix(dataUrl); const dims = parsePngDimensions(image_base64) ?? { width: 0, height: 0 }; return withShotDialogs({ @@ -459,7 +463,9 @@ export async function handleGetHtml( manager: SessionManager, params: GetHtmlParams, deps: SnapshotDeps = getDefaultDeps(), + signal?: AbortSignal, ): Promise { + if (signal?.aborted) return { code: "cancelled", message: "get_html aborted" }; const ctxOrErr = lookupSession(manager, params, "get_html"); if (isRpcError(ctxOrErr)) return ctxOrErr; const ctx = ctxOrErr; @@ -498,6 +504,7 @@ export async function handleGetHtml( }); html = resp.outerHTML ?? ""; } + if (signal?.aborted) return { code: "cancelled", message: "get_html aborted" }; const originalBytes = utf8ByteLength(html); const { out, truncated } = truncateBytes(html, maxBytes); return attachDialogs(deps.cdp, target.tabId, dialogCursor, { @@ -518,7 +525,9 @@ export async function handleSnapshot( manager: SessionManager, params: SnapshotParams, deps: SnapshotDeps = getDefaultDeps(), + signal?: AbortSignal, ): Promise { + if (signal?.aborted) return { code: "cancelled", message: "snapshot aborted" }; const ctxOrErr = lookupSession(manager, params, "snapshot"); if (isRpcError(ctxOrErr)) return ctxOrErr; const ctx = ctxOrErr; @@ -534,6 +543,7 @@ export async function handleSnapshot( "Accessibility.getFullAXTree", {}, ); + if (signal?.aborted) return { code: "cancelled", message: "snapshot aborted" }; const rendered = renderAxTree(result.nodes ?? [], { maxDepth: params.max_depth, maxTokens: params.max_tokens, diff --git a/apps/extension/src/tools/session.ts b/apps/extension/src/tools/session.ts index 6c14d7e..a51733f 100644 --- a/apps/extension/src/tools/session.ts +++ b/apps/extension/src/tools/session.ts @@ -11,6 +11,10 @@ export interface SessionStartResult { agent_window_id?: number; } +export interface SessionStartDeps { + signal?: AbortSignal; +} + export interface SessionStopParams { session_id: string; } @@ -44,6 +48,7 @@ export interface SessionStopDeps { export async function handleSessionStart( manager: SessionManager, params: SessionStartParams, + deps: SessionStartDeps = {}, ): Promise { if (!params?.session_id) { return { @@ -52,9 +57,12 @@ export async function handleSessionStart( }; } try { - const ctx = await manager.start(params.session_id); + const ctx = await manager.start(params.session_id, { signal: deps.signal }); return { agent_window_id: ctx.agentWindowId }; } catch (err) { + if (typeof err === "object" && err !== null && (err as { name?: string }).name === "AbortError") { + return { code: "cancelled", message: "session_start aborted" }; + } // chrome.windows.create / SessionManager failures are not CDP // failures (ยง4.5 reserves cdp_failed for raw CDP errors). Surface // them as protocol_error so the CLI maps to the right exit code From 840a803e2a07e527ccf9981aa1aaed890b9663d2 Mon Sep 17 00:00:00 2001 From: NianJiuZst <3235467914@qq.com> Date: Fri, 10 Jul 2026 22:40:16 +0800 Subject: [PATCH 2/5] chore(ci): match Biome 2.4 formatting --- apps/extension/vitest.config.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/extension/vitest.config.ts b/apps/extension/vitest.config.ts index 2e1c157..00060bf 100644 --- a/apps/extension/vitest.config.ts +++ b/apps/extension/vitest.config.ts @@ -21,9 +21,7 @@ export default defineConfig({ }, define: { __BSK_EXT_VERSION__: JSON.stringify(pkg.version), - __BSK_DAEMON_WS_URL__: JSON.stringify( - process.env.BSK_DAEMON_WS_URL ?? "ws://127.0.0.1:52800", - ), + __BSK_DAEMON_WS_URL__: JSON.stringify(process.env.BSK_DAEMON_WS_URL ?? "ws://127.0.0.1:52800"), }, resolve: { alias: { From cd07383b2fa0a152fe268d8e5f5667468ddd4743 Mon Sep 17 00:00:00 2001 From: NianJiuZst <3235467914@qq.com> Date: Fri, 10 Jul 2026 22:42:35 +0800 Subject: [PATCH 3/5] style(extension): format cancellation branch --- apps/extension/src/tools/session.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/extension/src/tools/session.ts b/apps/extension/src/tools/session.ts index a51733f..aac0f1c 100644 --- a/apps/extension/src/tools/session.ts +++ b/apps/extension/src/tools/session.ts @@ -60,7 +60,11 @@ export async function handleSessionStart( const ctx = await manager.start(params.session_id, { signal: deps.signal }); return { agent_window_id: ctx.agentWindowId }; } catch (err) { - if (typeof err === "object" && err !== null && (err as { name?: string }).name === "AbortError") { + if ( + typeof err === "object" && + err !== null && + (err as { name?: string }).name === "AbortError" + ) { return { code: "cancelled", message: "session_start aborted" }; } // chrome.windows.create / SessionManager failures are not CDP From 061231c2738bde9861e5f0b4e37bc7fa5db705c0 Mon Sep 17 00:00:00 2001 From: NianJiuZst <3235467914@qq.com> Date: Fri, 10 Jul 2026 23:06:02 +0800 Subject: [PATCH 4/5] test: bind daemon ports without probe races --- crates/bsk-cli/tests/browser_list_ordering.rs | 5 +---- crates/bsk-cli/tests/browser_wait.rs | 5 +---- crates/bsk-cli/tests/cancel_forwarding.rs | 5 +---- crates/bsk-cli/tests/handshake_compat.rs | 5 +---- crates/bsk-cli/tests/per_session_queue.rs | 5 +---- crates/bsk-cli/tests/session_user_interrupt.rs | 5 +---- crates/bsk-cli/tests/sessions_ipc.rs | 5 +---- crates/bsk-cli/tests/tools_ipc.rs | 5 +---- crates/bsk-cli/tests/tools_m7_ipc.rs | 5 +---- crates/bsk-cli/tests/tools_m8_ipc.rs | 5 +---- crates/bsk-cli/tests/tools_m9_ipc.rs | 5 +---- crates/bsk-cli/tests/ws_handshake.rs | 5 +---- 12 files changed, 12 insertions(+), 48 deletions(-) diff --git a/crates/bsk-cli/tests/browser_list_ordering.rs b/crates/bsk-cli/tests/browser_list_ordering.rs index 1c5199b..f19f320 100644 --- a/crates/bsk-cli/tests/browser_list_ordering.rs +++ b/crates/bsk-cli/tests/browser_list_ordering.rs @@ -16,7 +16,6 @@ use bsk_protocol::system::BrowserStatusEntry; use bsk_protocol::{ErrorCode, Method}; use rand::Rng; use serde::Deserialize; -use tokio::net::TcpListener; use tokio::sync::mpsc; fn tempfile_path(prefix: &str) -> PathBuf { @@ -30,9 +29,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let config = DaemonConfig::new(port); let sock = tempfile_path("bsk-test-browser-list"); diff --git a/crates/bsk-cli/tests/browser_wait.rs b/crates/bsk-cli/tests/browser_wait.rs index 632a7e8..16513e0 100644 --- a/crates/bsk-cli/tests/browser_wait.rs +++ b/crates/bsk-cli/tests/browser_wait.rs @@ -15,7 +15,6 @@ use bsk_protocol::system::{BrowserListParams, HandshakeParams, HandshakeResult, use bsk_protocol::{BrowserPeerInfo, Method, RequestFrame, ResponseBody, ResponseFrame}; use futures_util::{SinkExt, StreamExt}; use rand::Rng; -use tokio::net::TcpListener; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; use tokio_tungstenite::tungstenite::protocol::Message; @@ -77,9 +76,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let config = DaemonConfig::new(port); let sock = tempfile_path("bsk-test-browser-wait"); diff --git a/crates/bsk-cli/tests/cancel_forwarding.rs b/crates/bsk-cli/tests/cancel_forwarding.rs index b9913ca..e8a0aaf 100644 --- a/crates/bsk-cli/tests/cancel_forwarding.rs +++ b/crates/bsk-cli/tests/cancel_forwarding.rs @@ -20,7 +20,6 @@ use bsk_protocol::{ use futures_util::{SinkExt, StreamExt}; use rand::Rng; use serde_json::json; -use tokio::net::TcpListener; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; use tokio_tungstenite::tungstenite::protocol::Message; @@ -40,9 +39,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let config = DaemonConfig::new(port); let sock = tempfile_path("bsk-test-cancel"); diff --git a/crates/bsk-cli/tests/handshake_compat.rs b/crates/bsk-cli/tests/handshake_compat.rs index 07e14e4..596d35e 100644 --- a/crates/bsk-cli/tests/handshake_compat.rs +++ b/crates/bsk-cli/tests/handshake_compat.rs @@ -10,7 +10,6 @@ use bsk_protocol::system::{HandshakeParams, HandshakeResult, StatusResult}; use bsk_protocol::{BrowserPeerInfo, ErrorCode, Method, RequestFrame, ResponseBody, ResponseFrame}; use futures_util::{SinkExt, StreamExt}; use rand::Rng; -use tokio::net::TcpListener; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; use tokio_tungstenite::tungstenite::protocol::Message; @@ -28,9 +27,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let config = DaemonConfig::new(port); let sock = tempfile_path("bsk-test-handshake"); diff --git a/crates/bsk-cli/tests/per_session_queue.rs b/crates/bsk-cli/tests/per_session_queue.rs index f548ee6..79b5355 100644 --- a/crates/bsk-cli/tests/per_session_queue.rs +++ b/crates/bsk-cli/tests/per_session_queue.rs @@ -27,7 +27,6 @@ use futures_util::{SinkExt, StreamExt}; use rand::Rng; use serde::Deserialize; use serde_json::json; -use tokio::net::TcpListener; use tokio::sync::{Mutex, mpsc}; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; @@ -46,9 +45,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let sock = tempfile_path("bsk-test-queue"); let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone())) .await diff --git a/crates/bsk-cli/tests/session_user_interrupt.rs b/crates/bsk-cli/tests/session_user_interrupt.rs index 2960814..b7d6e61 100644 --- a/crates/bsk-cli/tests/session_user_interrupt.rs +++ b/crates/bsk-cli/tests/session_user_interrupt.rs @@ -26,7 +26,6 @@ use bsk_protocol::{ use futures_util::{SinkExt, StreamExt}; use rand::Rng; use serde_json::json; -use tokio::net::TcpListener; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; use tokio_tungstenite::tungstenite::protocol::Message; @@ -46,9 +45,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let config = DaemonConfig::new(port); let sock = tempfile_path("bsk-test-user-interrupt"); diff --git a/crates/bsk-cli/tests/sessions_ipc.rs b/crates/bsk-cli/tests/sessions_ipc.rs index a686520..99b1535 100644 --- a/crates/bsk-cli/tests/sessions_ipc.rs +++ b/crates/bsk-cli/tests/sessions_ipc.rs @@ -15,7 +15,6 @@ use bsk_protocol::tools::{SessionStartParams, SessionStartResult, SessionStopPar use bsk_protocol::{BrowserPeerInfo, Frame, Method, RequestFrame, ResponseBody, ResponseFrame}; use futures_util::{SinkExt, StreamExt}; use rand::Rng; -use tokio::net::TcpListener; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; use tokio_tungstenite::tungstenite::protocol::Message; @@ -39,9 +38,7 @@ async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { } async fn spawn_daemon_with_connect_wait(connect_wait: Duration) -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let config = DaemonConfig::new(port).with_extension_connect_wait(connect_wait); let sock = tempfile_path("bsk-test-ipc"); diff --git a/crates/bsk-cli/tests/tools_ipc.rs b/crates/bsk-cli/tests/tools_ipc.rs index 32b7732..2857c62 100644 --- a/crates/bsk-cli/tests/tools_ipc.rs +++ b/crates/bsk-cli/tests/tools_ipc.rs @@ -21,7 +21,6 @@ use futures_util::stream::{SplitSink, SplitStream}; use futures_util::{SinkExt, StreamExt}; use rand::Rng; use serde_json::{Value, json}; -use tokio::net::TcpListener; use tokio::sync::Mutex; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; @@ -40,9 +39,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let sock = tempfile_path("bsk-test-tools"); let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone())) .await diff --git a/crates/bsk-cli/tests/tools_m7_ipc.rs b/crates/bsk-cli/tests/tools_m7_ipc.rs index acb9481..ee74b5e 100644 --- a/crates/bsk-cli/tests/tools_m7_ipc.rs +++ b/crates/bsk-cli/tests/tools_m7_ipc.rs @@ -25,7 +25,6 @@ use futures_util::stream::{SplitSink, SplitStream}; use futures_util::{SinkExt, StreamExt}; use rand::Rng; use serde_json::{Value, json}; -use tokio::net::TcpListener; use tokio::sync::Mutex; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; @@ -44,9 +43,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let sock = tempfile_path("bsk-test-tools-m7"); let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone())) .await diff --git a/crates/bsk-cli/tests/tools_m8_ipc.rs b/crates/bsk-cli/tests/tools_m8_ipc.rs index 5308f7b..14e7c72 100644 --- a/crates/bsk-cli/tests/tools_m8_ipc.rs +++ b/crates/bsk-cli/tests/tools_m8_ipc.rs @@ -24,7 +24,6 @@ use futures_util::stream::{SplitSink, SplitStream}; use futures_util::{SinkExt, StreamExt}; use rand::Rng; use serde_json::Value; -use tokio::net::TcpListener; use tokio::sync::Mutex; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; @@ -43,9 +42,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let sock = tempfile_path("bsk-test-tools-m8"); let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone())) .await diff --git a/crates/bsk-cli/tests/tools_m9_ipc.rs b/crates/bsk-cli/tests/tools_m9_ipc.rs index 6dd5d33..2f8b46d 100644 --- a/crates/bsk-cli/tests/tools_m9_ipc.rs +++ b/crates/bsk-cli/tests/tools_m9_ipc.rs @@ -30,7 +30,6 @@ use rand::Rng; use serde_json::{Value, json}; #[cfg(unix)] use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::net::TcpListener; #[cfg(unix)] use tokio::net::UnixStream; use tokio::sync::Mutex; @@ -53,9 +52,7 @@ fn tempfile_path(prefix: &str) -> PathBuf { } async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) { - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let sock = tempfile_path("bsk-test-tools-m9"); let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone())) .await diff --git a/crates/bsk-cli/tests/ws_handshake.rs b/crates/bsk-cli/tests/ws_handshake.rs index 146ef33..c6e2da1 100644 --- a/crates/bsk-cli/tests/ws_handshake.rs +++ b/crates/bsk-cli/tests/ws_handshake.rs @@ -8,7 +8,6 @@ use bsk::daemon::{self, DaemonConfig}; use bsk_protocol::system::{HandshakeParams, HandshakeResult}; use bsk_protocol::{BrowserPeerInfo, Method, RequestFrame, ResponseBody, ResponseFrame}; use futures_util::{SinkExt, StreamExt}; -use tokio::net::TcpListener; use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; use tokio_tungstenite::tungstenite::protocol::Message; @@ -17,9 +16,7 @@ pub const TEST_EXT_ID: &str = "abcdefghijklmnopabcdefghijklmnop"; // 32 chars in pub async fn spawn_daemon() -> daemon::DaemonHandle { // Bind to any free TCP port. - let probe = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = probe.local_addr().unwrap().port(); - drop(probe); + let port = 0; let config = DaemonConfig::new(port); daemon::run(config, None).await.unwrap() From 823ff452a531a08a9d1801718732209dfe9bdb5f Mon Sep 17 00:00:00 2001 From: NianJiuZst <3235467914@qq.com> Date: Fri, 10 Jul 2026 23:10:57 +0800 Subject: [PATCH 5/5] ci: retry incomplete CodeCC scan