Compare commits

...
Author SHA1 Message Date
“haonan”andCursor c48a53269e style: rustfmt doctor protocol-version test asserts
cargo fmt --check failed on two long assert lines in PR CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 22:50:01 +08:00
“haonan”andCursor 3bbd6ab19f fix: treat protocol version differences as upgrade reminders
Agents were stopping on popup and status copy that looked like a hard incompatibility. Keep the connection usable, reframe exit 5 as an unknown method, and ask users to upgrade instead of aborting.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 22:22:30 +08:00
8 changed files with 71 additions and 28 deletions
@@ -247,6 +247,41 @@ describe("App", () => {
expect(copyButton.getAttribute("disabled")).not.toBeNull(); expect(copyButton.getAttribute("disabled")).not.toBeNull();
expect(screen.getByText("连接后可用")).toBeTruthy(); expect(screen.getByText("连接后可用")).toBeTruthy();
}); });
it("treats protocol drift as still connected and prompts an upgrade", async () => {
mockUseConnectionState.mockReturnValue({
snapshot: {
...baseSnapshot,
state: "version_skew",
instanceId: "03c3e47f",
handshake: {
server: "bh",
version: mockDaemonVersion,
protocol_version: "1.0",
},
},
statusState: "version_skew",
setLabel,
setConnectionEnabled,
});
render(<App />);
expect(screen.getByText("已连接")).toBeTruthy();
expect(screen.getByText("可升级")).toBeTruthy();
expect(screen.queryByText("协议不一致")).toBeNull();
expect(screen.queryByText("Action needed")).toBeNull();
expect(screen.queryByText("兼容")).toBeNull();
const warning = screen.getByText(/协议版本不同,请及时升级/);
expect(warning.textContent).toContain("CLI 协议");
expect(warning.textContent).toContain("扩展协议");
openRecordView();
const copyButton = screen.getByRole("button", { name: "复制录制指令" });
expect(copyButton.getAttribute("disabled")).toBeNull();
fireEvent.click(copyButton);
expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
});
}); });
describe("control hints toggle", () => { describe("control hints toggle", () => {
+4 -3
View File
@@ -79,6 +79,7 @@ export function App() {
}, [copiedTick]); }, [copiedTick]);
const isSkewed = statusState === "version_skew"; const isSkewed = statusState === "version_skew";
const connectionLive = statusState === "connected" || isSkewed;
const daemonVersion = snapshot.handshake?.version ?? "—"; const daemonVersion = snapshot.handshake?.version ?? "—";
const daemonProtocol = snapshot.handshake?.protocol_version ?? "—"; const daemonProtocol = snapshot.handshake?.protocol_version ?? "—";
const extensionVersion = snapshot.extensionVersion || "—"; const extensionVersion = snapshot.extensionVersion || "—";
@@ -90,7 +91,7 @@ export function App() {
setCopiedInstanceId(true); setCopiedInstanceId(true);
}; };
const recordReady = statusState === "connected" && Boolean(snapshot.instanceId); const recordReady = connectionLive && Boolean(snapshot.instanceId);
const recordPurpose = purposeDraft.trim(); const recordPurpose = purposeDraft.trim();
const recordStartUrl = startUrlDraft.trim(); const recordStartUrl = startUrlDraft.trim();
const recordCommand = snapshot.instanceId const recordCommand = snapshot.instanceId
@@ -206,12 +207,12 @@ export function App() {
</div> </div>
{isSkewed && ( {isSkewed && (
<p <p
className="mt-2 text-xs leading-snug text-amber-600 dark:text-amber-400" className="mt-2 text-xs leading-snug text-muted-foreground"
data-slot="popup-version-skew-warning" data-slot="popup-version-skew-warning"
> >
{t("popup.versionSkewWarning", { {t("popup.versionSkewWarning", {
extensionProtocol: PROTOCOL_VERSION, extensionProtocol: PROTOCOL_VERSION,
daemonProtocol, cliProtocol: daemonProtocol,
})} })}
</p> </p>
)} )}
+2 -2
View File
@@ -29,7 +29,7 @@ Drive the user's **real Chromium browser** (with their logins and cookies) throu
## Prerequisites ## Prerequisites
1. `bsk` on `PATH` (Rust CLI from browser-skill) 1. `bsk` on `PATH` (Rust CLI from browser-skill)
2. browser-skill **extension** loaded in Chromium and connected (popup shows green) 2. browser-skill **extension** loaded in Chromium and connected (popup shows green, or an upgrade reminder — still connected; continue)
3. Any `bsk` command auto-starts background services as needed; use `bsk doctor` if anything fails 3. Any `bsk` command auto-starts background services as needed; use `bsk doctor` if anything fails
## Mandatory workflow ## Mandatory workflow
@@ -299,7 +299,7 @@ bsk record stop [--output trace] # terminal fallback if the browser panel is u
| `2` | Protocol / transport — service unreachable, IPC failure | `bsk doctor`; check extension connected; retry the command | | `2` | Protocol / transport — service unreachable, IPC failure | `bsk doctor`; check extension connected; retry the command |
| `3` | Browser / CDP execution failed | Retry; simplify selector; check tab still open | | `3` | Browser / CDP execution failed | Retry; simplify selector; check tab still open |
| `4` | Timeout | Increase `--timeout`; try `--wait-until domcontentloaded` | | `4` | Timeout | Increase `--timeout`; try `--wait-until domcontentloaded` |
| `5` | Version skew (CLI vs extension) | Upgrade/reinstall matching versions | | `5` | Unknown RPC method | This command is not implemented in the current build. Continue the task with other commands. If a newly added command is missing, suggest `bsk update`. |
Human errors print `error:` + `hint:` on stderr; `--json` includes `code`, `message`, `hint`, `exit_code`. Human errors print `error:` + `hint:` on stderr; `--json` includes `code`, `message`, `hint`, `exit_code`.
+18 -11
View File
@@ -351,10 +351,10 @@ fn check_version_compatible(status: Option<&StatusResult>) -> CheckResult {
} }
} }
/// `bsk doctor` check: every connected browser should use the same /// `bsk doctor` check: connected browsers should speak a protocol the
/// protocol version as the daemon. Minor protocol drift is accepted by /// daemon accepts. A different protocol string is still a live
/// the daemon but flagged here so the user can see who needs updating /// connection — report Ok so agents keep working. The detail is an
/// (M10.4). /// upgrade reminder, not a blocker.
/// ///
/// Review M2 (round-1 minor): when no browsers are connected, the /// Review M2 (round-1 minor): when no browsers are connected, the
/// check has nothing to compare against, so it now reports /// check has nothing to compare against, so it now reports
@@ -396,13 +396,12 @@ fn check_browsers_protocol_compatible(status: Option<&StatusResult>) -> CheckRes
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "); .join(", ");
CheckResult::fail( CheckResult::ok(
name, name,
format!( format!(
"{} browser(s) have protocol minor drift from the daemon: {stale}", "{} browser(s) report a different protocol version (still usable — continue, and upgrade soon): {stale}",
status.version_skew_browsers.len() status.version_skew_browsers.len()
), ),
"upgrade the browser-skill extension or bsk CLI so both sides use the same protocol version",
) )
} }
@@ -581,7 +580,7 @@ mod m2_tests {
} }
#[test] #[test]
fn browsers_check_reports_fail_when_skew_present() { fn browsers_check_reports_ok_when_skew_present() {
let status = fake_status( let status = fake_status(
vec![BrowserStatusEntry { vec![BrowserStatusEntry {
instance_id: "alpha".into(), instance_id: "alpha".into(),
@@ -605,9 +604,17 @@ mod m2_tests {
}], }],
); );
let check = check_browsers_protocol_compatible(Some(&status)); let check = check_browsers_protocol_compatible(Some(&status));
assert_eq!(check.status, CheckStatus::Fail); assert_eq!(check.status, CheckStatus::Ok);
assert!(check.detail.contains("alpha")); assert!(check.detail.contains("alpha"));
assert!(check.hint.is_some()); assert!(
check
.detail
.contains("still usable — continue, and upgrade soon")
);
assert!(
check.hint.is_none(),
"a protocol-version note must not hint to stop"
);
} }
#[test] #[test]
@@ -635,7 +642,7 @@ mod m2_tests {
}], }],
); );
let check = check_browsers_protocol_compatible(Some(&status)); let check = check_browsers_protocol_compatible(Some(&status));
assert_eq!(check.status, CheckStatus::Fail); assert_eq!(check.status, CheckStatus::Ok);
assert!( assert!(
check check
.detail .detail
+2 -2
View File
@@ -65,7 +65,7 @@ fn render_human(s: &StatusResult) {
let bold = "\x1b[1m"; let bold = "\x1b[1m";
let reset = "\x1b[0m"; let reset = "\x1b[0m";
eprintln!( eprintln!(
"{bold}{yellow}warning:{reset} {} browser(s) have protocol version drift from the daemon{reset}", "{bold}{yellow}note:{reset} {} browser(s) report a different protocol version; still usable — continue, and run `bsk update` soon{reset}",
s.version_skew_browsers.len() s.version_skew_browsers.len()
); );
for skew in &s.version_skew_browsers { for skew in &s.version_skew_browsers {
@@ -75,7 +75,7 @@ fn render_human(s: &StatusResult) {
skew.label.clone() skew.label.clone()
}; };
eprintln!( eprintln!(
" {} ({}) — protocol ext {} vs daemon {} (app ext v{}, daemon v{}) — please align protocol versions", " {} ({}) — protocol ext {} vs CLI {} (app ext v{}, CLI v{}) — still usable; please upgrade",
skew.instance_id, skew.instance_id,
label, label,
display_protocol(&skew.client_protocol_version), display_protocol(&skew.client_protocol_version),
@@ -6,27 +6,27 @@
"disconnected": "Disconnected", "disconnected": "Disconnected",
"connecting": "Connecting…", "connecting": "Connecting…",
"connected": "Connected", "connected": "Connected",
"version_skew": "Protocol mismatch", "version_skew": "Connected",
"disabled": "Connection off" "disabled": "Connection off"
}, },
"stateDetail": { "stateDetail": {
"disconnected": "Open BrowserSkill to connect.", "disconnected": "Open BrowserSkill to connect.",
"connecting": "Connecting to BrowserSkill…", "connecting": "Connecting to BrowserSkill…",
"connected": "Ready for automation tasks.", "connected": "Ready for automation tasks.",
"version_skew": "Some features may be unavailable." "version_skew": "Protocol versions differ. Please upgrade."
}, },
"stateBadge": { "stateBadge": {
"disconnected": "Standby", "disconnected": "Standby",
"connecting": "Igniting", "connecting": "Igniting",
"connected": "Ready", "connected": "Ready",
"version_skew": "Action needed", "version_skew": "Upgradable",
"disabled": "Off" "disabled": "Off"
}, },
"connectionToggleTitle": "BrowserSkill connection", "connectionToggleTitle": "BrowserSkill connection",
"controlHintsToggleTitle": "Control hints", "controlHintsToggleTitle": "Control hints",
"controlHintsToggleHint": "Show the status pill and orange glow while the Agent controls a page.", "controlHintsToggleHint": "Show the status pill and orange glow while the Agent controls a page.",
"controlHintsInfoLabel": "About control hints", "controlHintsInfoLabel": "About control hints",
"versionSkewWarning": "Extension protocol v{{extensionProtocol}}, daemon v{{daemonProtocol}}.", "versionSkewWarning": "Extension protocol v{{extensionProtocol}}, CLI protocol v{{cliProtocol}}. Protocol versions differ — please upgrade.",
"upgradeAvailable": "Upgradable", "upgradeAvailable": "Upgradable",
"launcher": { "launcher": {
"title": "Quick actions" "title": "Quick actions"
@@ -6,27 +6,27 @@
"disconnected": "未连接", "disconnected": "未连接",
"connecting": "连接中…", "connecting": "连接中…",
"connected": "已连接", "connected": "已连接",
"version_skew": "协议不一致", "version_skew": "已连接",
"disabled": "连接已关闭" "disabled": "连接已关闭"
}, },
"stateDetail": { "stateDetail": {
"disconnected": "请先打开 BrowserSkill。", "disconnected": "请先打开 BrowserSkill。",
"connecting": "正在连接 BrowserSkill…", "connecting": "正在连接 BrowserSkill…",
"connected": "可接收自动化任务。", "connected": "可接收自动化任务。",
"version_skew": "部分能力可能不可用。" "version_skew": "协议版本不同,请及时升级。"
}, },
"stateBadge": { "stateBadge": {
"disconnected": "Standby", "disconnected": "Standby",
"connecting": "Igniting", "connecting": "Igniting",
"connected": "Ready", "connected": "Ready",
"version_skew": "Action needed", "version_skew": "可升级",
"disabled": "Off" "disabled": "Off"
}, },
"connectionToggleTitle": "BrowserSkill 连接", "connectionToggleTitle": "BrowserSkill 连接",
"controlHintsToggleTitle": "控制提示", "controlHintsToggleTitle": "控制提示",
"controlHintsToggleHint": "Agent 控制页面时显示提示条和橙色闪光。", "controlHintsToggleHint": "Agent 控制页面时显示提示条和橙色闪光。",
"controlHintsInfoLabel": "控制提示说明", "controlHintsInfoLabel": "控制提示说明",
"versionSkewWarning": "扩展协议 v{{extensionProtocol}},daemon 协议 v{{daemonProtocol}}。", "versionSkewWarning": "扩展协议 v{{extensionProtocol}},CLI 协议 v{{cliProtocol}}。协议版本不同,请及时升级。",
"upgradeAvailable": "可升级", "upgradeAvailable": "可升级",
"launcher": { "launcher": {
"title": "快捷功能" "title": "快捷功能"
+2 -2
View File
@@ -29,7 +29,7 @@ Drive the user's **real Chromium browser** (with their logins and cookies) throu
## Prerequisites ## Prerequisites
1. `bsk` on `PATH` (Rust CLI from browser-skill) 1. `bsk` on `PATH` (Rust CLI from browser-skill)
2. browser-skill **extension** loaded in Chromium and connected (popup shows green) 2. browser-skill **extension** loaded in Chromium and connected (popup shows green, or an upgrade reminder — still connected; continue)
3. Any `bsk` command auto-starts background services as needed; use `bsk doctor` if anything fails 3. Any `bsk` command auto-starts background services as needed; use `bsk doctor` if anything fails
## Mandatory workflow ## Mandatory workflow
@@ -299,7 +299,7 @@ bsk record stop [--output trace] # terminal fallback if the browser panel is u
| `2` | Protocol / transport — service unreachable, IPC failure | `bsk doctor`; check extension connected; retry the command | | `2` | Protocol / transport — service unreachable, IPC failure | `bsk doctor`; check extension connected; retry the command |
| `3` | Browser / CDP execution failed | Retry; simplify selector; check tab still open | | `3` | Browser / CDP execution failed | Retry; simplify selector; check tab still open |
| `4` | Timeout | Increase `--timeout`; try `--wait-until domcontentloaded` | | `4` | Timeout | Increase `--timeout`; try `--wait-until domcontentloaded` |
| `5` | Version skew (CLI vs extension) | Upgrade/reinstall matching versions | | `5` | Unknown RPC method | This command is not implemented in the current build. Continue the task with other commands. If a newly added command is missing, suggest `bsk update`. |
Human errors print `error:` + `hint:` on stderr; `--json` includes `code`, `message`, `hint`, `exit_code`. Human errors print `error:` + `hint:` on stderr; `--json` includes `code`, `message`, `hint`, `exit_code`.