Run before tagging a release. Each line is one minute or less.
-
bun --cwd packages/bridge run dev:cli install --devsucceeds -
byob doctorall green after extension reload + Chrome restart -
curl --unix-socket ~/.byob/bridges/<id>.sock http://x/status→{"connected":true} - Kill bridge → confirm extension auto-reconnects within ~3s (extension's exponential backoff)
-
browser_read https://news.ycombinator.comreturns story headlines -
browser_read https://x.com/anthropicreturns logged-in tweets (cookie reuse proof) -
browser_screenshot https://github.comwrites file to~/.byob/screenshots/<ts>.png -
browser_click textarea[name=q]on Google home (after navigate) -
browser_type "mcp protocol" pressEnter:trueon Google -
browser_get_cookies github.comreturnsuser_session(or your authenticated cookie) -
browser_navigatebetween two domains, reuses tab -
browser_wait_for #searchafter a Google query →{found:true, elapsedMs} -
browser_list_tabsreturns all open tabs -
browser_switch_tab <id>activates a chosen tab (window comes to foreground) -
BYOB_ALLOW_EVAL=1 browser_eval document.titlereturns title + emits notification + audit log line
-
browser_read file:///etc/passwd→url_forbiddenenvelope - DevTools open on tab →
browser_clickreturnscdp_attach_failedwith hint -
BYOB_ALLOW_EVALunset →browser_evalnot intools/list(LLM doesn't see it) - Tab closed mid-op →
tab_closed(or graceful failure — depends on which step) - Chrome quit mid-call →
bridge_not_running - Attach to an allowed page, navigate that same attached tab to a host in
BYOB_DENIED_DOMAINS, then call a CDP tool again →url_forbidden(the cached CDP session must not bypass the new URL policy)
- Claude Code:
"use byob to ..."triggers correct tool calls -
claude plugin validate . --strictandclaude plugin validate plugins/byob --strictboth pass - Install through the local marketplace, run
/reload-plugins, and verify/mcpshowsplugin:byob:byobwithout a separateclaude mcp add -
/byob:control-chromeappears and a natural-language Chrome request automatically loads the Skill - Cursor: same (config in Cursor settings)
- Cancel mid-call (Ctrl+C) → eventually frees the bridge slot (Phase 6 will do this cleanly)
-
browser_new_tab {}returns a backgroundabout:blanktab; navigating the returnedtabIdtohttps://example.comsucceeds -
browser_reloadwaits for the load event and returns the new URL/title -
browser_clickwith viewportx+ydispatches a real click; withclickCount:2it dispatches a double click - Click an input, then call
browser_typewithoutselector→ text lands in the currently focused field -
browser_hoverwith viewportx+ytriggers a hover state -
browser_scrollwithx,y, andscrollYdispatches a wheel gesture -
browser_screenshotwithclip:{x,y,width,height}writes an image with the requested dimensions;clip+fullPage:trueis rejected - Trigger
alert()/confirm()/prompt()and verifybrowser_get_js_dialogreturns type/message without closing it -
browser_handle_js_dialogaccepts or dismisses only after an explicit call; accepting a prompt forwardstext - A confirm dialog remains open if no handle call is made (regression: byob must never auto-accept it)
-
browser_historywith a unique search term returns matching visits and honorsfrom,to, andlimit; protected URLs are omitted -
browser_clipboard_write_textfollowed bybrowser_clipboard_read_textround-trips Unicode text; both reject a forbiddentabId
-
byob bridgeslists 1 process per active Chrome profile -
byob logs -fstreams new entries -
byob uninstallremoves launcher + all manifests; subsequentbyob doctorshows✗ - Re-
byob install --dev→ all green again
- On
https://example.com, runbrowser_eval "console.error('test'); console.warn('w'); throw new Error('boom')"thenbrowser_get_console_logs(same tab) — expect 3 entries: 1× error, 1× warn, 1× exception with stackTrace - Same tab, call
browser_get_console_logs level: ['error']— exception still appears (independent flag), warn does not - Same tab, call
browser_get_console_logs level: ['error'] includeExceptions: false— only the error entry; no exception - On
chrome://settings— expecturl_forbiddenenvelope with hint
- On a BBC News article — markdown contains the headline, lengthChars > 500, byline non-empty, no
<nav>/ footer text - On
https://x.com/anthropic(heavy SPA) — expect either a usable result ORreadability_no_articleenvelope with htmlLength field; never crash - Same BBC article with
includeImages: false— markdown contains zero![substrings - Same BBC article with
maxLength: 200— markdown ends with\n\n[truncated]\n,truncated: true
- On
https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)with defaultselector: 'table'andformat: 'objects'— at least one returned table; first row has key names matching first column header (e.g. "Country / Dependency"); rowCount >= 50 - Same URL with
selector: 'table.sortable'andformat: 'rows'— rows arestring[][]notRecord<string,string>[] - On a page with no tables (e.g.
https://example.com) —tables: [], no error
Pre-req: Chrome restarted with the rebuilt extension. bun run typecheck green.
- Basic XHR capture — Start
browser_start_record_networkwithurl=https://news.ycombinator.com, browse for ~30 s, then stop. ExpectrecordCount >= 5and at least one entry withresourceType ∈ {xhr, fetch}and a populatedresponseBody. - urlPattern glob filter — Start with
urlPattern='*api*'on a SPA-heavy page (e.g.https://github.com/anthropics/anthropic-sdk-typescript), browse, stop. Verify every record'surlcontainsapi. - resourceTypes=['*'] — Start with
resourceTypes=['*']onhttps://github.com, stop after 10 s. Verify records of typedocument,script,image,stylesheetare all present. - maxRecords autoStop — Start with
maxRecords=5, navigate to a page that fires more than 10 requests, stop. VerifyrecordCount=5,truncated=true,endedReason='max_records'. - tab_closed autoStop — Start a recording, then close the tab manually before calling stop. Call stop. Verify
endedReason='tab_closed'and the records captured before close are returned. - timeout autoStop — Start with
timeoutMs=5000, wait 10 s, then stop. VerifyendedReason='timeout'. - WebSocket frames — Start on
https://www.websocket.org/echo.html(or open the dev console on a page and connect towss://echo.websocket.events). Send a few text messages. Stop. Verify exactly one record withresourceType='websocket'andwebSocketFramescontaining bothdirection='sent'anddirection='received'entries. - HAR format — Stop with
format='har'. Verifyhar.log.version='1.2',har.log.entries.length===recordCount, every entry hasrequest.method,response.status,timings.dns/connect/send/wait/receivekeys. - eval-then-stop — Start a recording. Use
BYOB_ALLOW_EVAL=1 browser_evalto runfetch('/robots.txt').then(r=>r.text()). Stop. Verify a record exists for/robots.txtwithresponseBodypopulated. - recording_not_found — Call stop with a random UUID that was never started. Expect MCP error envelope with
error='recording_not_found'.
Pre-req: BYOB_ALLOW_FILE=1 for the local nested fixture; standard env for the others.
- In Chrome, open
https://www.w3schools.com/html/html_iframe.asp. - Run
browser_read framePath:['iframe[name="iframe_a"]']. Expected: responsetextcontains "W3Schools" placeholder text from the embedded frame, NOT the surrounding tutorial chrome. - Run
browser_eval code:'document.title' framePath:['iframe[name="iframe_a"]'](withBYOB_ALLOW_EVAL=1). Expected:resultequals the inner-frame document title (different from the outer tutorial's title).
- Open
file://<repo-root>/assets/fixtures/iframe-nested.htmlin Chrome (replace<repo-root>withpwdfrom your byob clone, e.g./home/me/byob). RequiresBYOB_ALLOW_FILE=1. - Run
browser_read framePath:['iframe.outer', 'iframe.inner']. Expected: responsetextcontains "inner". - Run
browser_click framePath:['iframe.outer', 'iframe.inner'] selector:'#go'. - Run
browser_read framePath:['iframe.outer', 'iframe.inner']again. Expected: responsetextnow contains "clicked".
- Open any page that embeds Stripe Elements. Confirm DevTools shows the Stripe iframe is cross-origin.
- Run
browser_eval code:'location.host' framePath:['iframe[src*="stripe"]']. Expected:resultends with "stripe.com".
- On the local fixture, run
browser_click framePath:['iframe.outer', 'iframe.inner'] selector:'#go'. - Then
browser_eval code:'document.getElementById("mark").textContent' framePath:['iframe.outer', 'iframe.inner']. Expected:result === "clicked".
-
browser_click framePath:['#nonexistent'] selector:'button'. Expected: response envelopeerror: 'frame_not_found',framePathIndex: 0. -
browser_click framePath:['div.foo'] selector:'button'against a page with a<div class="foo">. Expected: responseerror: 'frame_not_found',framePathIndex: 0,reason: 'not_an_iframe'. -
browser_eval framePath:['iframe[sandbox=""]'] code:'1+1'against a page with<iframe sandbox="">. Expected: responseerror: 'frame_eval_blocked'with hint mentioningallow-scripts.
-
browser_read https://news.ycombinator.com(noframePath). Expected: same v0.1 output. -
browser_click selector:'#search'on Google with noframePath. Expected: same v0.1 click behavior.
-
Long screenshot cancel
- In Claude Code (or any MCP client), call:
byob:browser_screenshot { url: "https://www.binance.com", fullPage: true } - After ≥ 1 s but before completion, send Ctrl-C / cancel.
- Verify all of:
- MCP client sees an isError envelope with
error: "aborted"andaborted: true. tail -50 ~/.byob/bridge.logshows acancel: aborting requestId=...line.- Extension service-worker console (chrome://extensions → byob → service worker → Inspect) shows the handler dispatch ended without an unhandled error.
- Immediately after, a fresh
byob:browser_list_tabscall succeeds in < 200 ms (no zombie state).
- MCP client sees an isError envelope with
- In Claude Code (or any MCP client), call:
-
Mid-navigation cancel
- Call
byob:browser_navigate { url: "https://example.com", waitUntil: "load" }. - Cancel before completion.
- Verify the tab in Chrome is in a usable state (not stuck loading).
- Call
-
Cancel of an already-finished request is a no-op
- Run any quick tool to completion (e.g.
browser_list_tabs). - Manually POST
/cancelwith that requestId via curl:curl -s --unix-socket ~/.byob/bridges/<id>.sock -X POST -H 'Content-Type: application/json' -d '{"requestId":"00000000-0000-0000-0000-000000000000"}' http://localhost/cancel - Verify response is
{ "ok": true }and bridge log showscancel: unknown requestId ... (no-op).
- Run any quick tool to completion (e.g.
-
Eval works immediately after extension reload (DevTools-held simulation)
- Reload the extension at chrome://extensions.
- Within 1 s of the reload, call
byob:browser_eval { code: "1+1", tabId: <some open tab> }(withBYOB_ALLOW_EVAL=1). - Expected: response succeeds, value === 2, MCP
_meta.fallbackUsed === true.
-
Steady-state eval uses CDP (no fallback)
- Wait 5 s after the previous test.
- Call
byob:browser_eval { code: "1+1", tabId: <same tab> }. - Expected: response succeeds, value === 2,
_meta.fallbackUsed === false.
-
Fallback path on a special page returns url_forbidden, not fallbackUsed:true
byob:browser_eval { code: "1+1", tabId: <chrome://settings tab> }.- Expected: isError envelope
{ error: "url_forbidden" }. (Fallback should NOT activate on chrome:// pages.)
-
macOS lid-close-and-open with no in-flight call
- Close laptop lid for ≥ 5 minutes, reopen, unlock.
- Within 10 s of unlock, run
byob:browser_read https://news.ycombinator.com. - Expected: success. Service-worker log shows
[byob/wake-watch] triggered by alarmoridle, plusaborting in-flight + detachAll.
-
Lid-close mid-call surfaces aborted_due_to_wake
- Start a long-running call (e.g.
browser_readof a 50-screen page withscreens: 50). - Within 5 s, close the lid for ≥ 2 minutes, reopen.
- Expected: the original MCP call returns isError
{ error: "aborted_due_to_wake", aborted: true }shortly after wake.
- Start a long-running call (e.g.
-
Wake recovery is idempotent
- Sleep+wake the laptop twice in quick succession (e.g. lid close-open-close-open).
- Expected: no errors; subsequent tool calls succeed.
在测试前先重启 Chrome(让新 manifest 生效)+ 重启 MCP server(让 24 个工具被注册)。
- 滚到底部:
browser_scroll(url='https://example.com', to='bottom')→ 期望scrollY > 0、pageHeight > 0 - 滚到 selector:在 byob GitHub README 上调
browser_scroll(tabId=<某个 tabId>, selector='#footer')→ 期望页面滚到 footer,scrollY接近pageHeight - viewport - 绝对坐标:
browser_scroll(tabId=..., y=500)→ 期望scrollY === 500 - selector 不存在:
browser_scroll(tabId=..., selector='#does-not-exist')→ 期望error: 'selector_not_found'
- 回车提交:先
browser_navigate('https://www.google.com')→browser_click(selector='textarea')→browser_type(selector='textarea', text='mcp protocol spec')→browser_press_key(tabId=..., key='Enter')→ 验证导航到搜索结果页 - GitHub 搜索快捷键:在 GitHub 任意页面上
browser_press_key(tabId=..., key='/')→ 期望搜索框 focus(用browser_eval或browser_get_html验证document.activeElement是搜索框)
- w3schools select:在
https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_select演示页 →browser_select(tabId=..., selector='select', label='Banana')→ 期望selectedLabel === 'Banana' - value 模式:同页面
browser_select(tabId=..., selector='select', value='audi')→ 期望selectedValue === 'audi' - option 不存在:
browser_select(tabId=..., selector='select', value='not-real')→ 期望error: 'option_not_found'
- 正常关闭:
browser_list_tabs拿到 ID →browser_close_tab(tabId=<id>)→ 期望closed: true+ 后续browser_list_tabs不再列出该 tab - 不存在的 tabId:
browser_close_tab(tabId=99999999)→ 期望error: 'tab_closed'
- A→B→back:
browser_navigate('https://example.com')拿到 tabId →browser_navigate(url='https://example.org', tabId=<id>)→browser_go_back(tabId=<id>)→ 期望url是example.com - 再 forward:紧接着
browser_go_forward(tabId=<id>)→ 期望url是example.org - 没有可后退:开新 tab 后立刻
browser_go_back(tabId=<id>)→ 期望error: 'no_history'
- 下拉菜单展开:在 GitHub 任一页面 →
browser_hover(tabId=..., selector='button[aria-label*="Open user navigation menu"]')→ 接browser_wait_for(tabId=..., selector='[data-testid="signed-in-user-menu"]')→ 期望菜单出现
- 整页:
browser_get_html(url='https://example.com')→ 期望html包含<h1>Example Domain</h1>、truncated: false - selector:同上 +
selector='h1'→ 期望html === '<h1>Example Domain</h1>' - innerHtml:同上 +
selector='h1'、outerHtml=false→ 期望html === 'Example Domain' - 截断:在大页面(比如
https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations))上browser_get_html(url=..., maxBytes=1024)→ 期望truncated: true、byteLength <= 1024
在测试前先重启 Chrome(让新 manifest 生效)+ 重启 MCP server(让 29 个工具被注册)。
- set 一条
name='byob-test',value='1',url='https://example.com',再get_cookies({domain:'example.com'}),验证读到 byob-test=1 - set 一条带
expirationDate=Math.floor(Date.now()/1000)+3600,验证 1 小时后过期 - set 一条
sameSite:'lax'(小写)验证写入;试sameSite:'Lax'(大写)验证 schema 拒绝
- 打 example.com → PDF,验证
~/.byob/pdfs/<ts>.pdf存在且 byteLength > 0;用open打开能正确显示 - 同一页
landscape:true,验证 PDF 是横向 - 长页面(GitHub 某 README)
pageRanges:'1-2',验证只 2 页 - 自定义
savePath:'/tmp/byob-pdf-test.pdf'验证按指定位置写盘
- 在 example.com 上先
eval写一些 localStorage(localStorage.setItem('a','1')),再get_storagekind:'local',验证读到{ a: '1' } -
kind:'both',验证返localStorage和sessionStorage两个字段 - 写 >1MB 的 localStorage(用 eval 写一个大 string),get 时验证
truncated:true
- 在 example.com 调,验证 navigation 字段都有值,FCP 有值
- 在 SPA(如 google.com)上点几下后再调,验证 INP 有值
- 在新 tab 直接调,验证 INP=null,CLS=null(无累积)
-
waitMs:0调一次(瞬读),验证仍能返回,且 LCP 可能 null
- 准备一个本机文件
/tmp/byob-test.txt,在 https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_input_type_file 之类的演示页上传,验证input.files.length === 1 - 多文件:
paths:['/tmp/a.txt','/tmp/b.txt'],input 的multiple属性下验证读到 2 个 - 文件不存在:
paths:['/tmp/nonexistent'],验证file_not_found - 非绝对路径:
paths:['./relative.txt'],验证file_not_found(错误信息说 "not absolute") - selector 不是 file input:传
<input type="text">,验证not_a_file_input
- start
[{ urlPattern: 'https://example.com/*', action: 'block' }]on a tab;导航到 example.com,验证页面加载失败(DevTools network panel 显示 BlockedByClient);stop 后再访问 example.com 正常加载;stop 输出totalRequests > 0且hitsByRule[0].count > 0
- start 一条 rule mock
https://api.github.com/users/octocat:action: 'fulfill', fulfill: { status: 200, headers: { 'content-type': 'application/json' }, body: '{"login":"fakeuser"}' };导航到一个会调用 octocat 的页面(或直接 console fetch 该 URL),验证返回 fakeuser;stop 验证hitsByRule[0].count >= 1
- start 一条 rule
action: 'modify', modify: { requestHeaders: { 'x-byob-test': '1' } }forhttps://httpbin.org/headers;console fetch 该 URL,验证响应里 echo 出X-Byob-Test: 1
- start rule on
https://api.github.com/users/octocat:action: 'modifyResponse', modifyResponse: { bodyRegex: { pattern: '"login":"[^"]+"', replacement: '"login":"hacked"' } };fetch 该 API,验证 body 里 login=hacked;stop 验证 hits=1
- stop 一个不存在的 interceptId,验证
intercept_not_found - start 后关闭 tab,再 stop,验证
endedReason: 'tab_closed'(而非 not_found)
- 在 https://www.w3schools.com/html/tryit.asp?filename=tryhtml5_draganddrop drop demo 上 drag
from: '#drag1'toto: '#div2',验证图片被拖动(视觉验证)
- 在一个有 canvas 的 demo 上 drag
from: { x: 100, y: 100 }to{ x: 300, y: 200 },验证 canvas 有对应轨迹
- drag 同一对端点两次:第一次默认 500ms/30 步,第二次 durationMs:2000/steps:120,验证后者明显慢
- 错误:
from: '#nonexistent'验证selector_not_found
- emulate
device: 'iphone-17-pro-max'在 https://m.taobao.com 或 https://m.zhihu.com,验证页面切到移动版 layout - eval
navigator.userAgent验证含 iPhone 字样 - eval
window.innerWidth验证为 PRESETS[iphone-17-pro-max].width
- 上一步之后调
device: 'desktop';eval UA / innerWidth 回到原 desktop 值
- custom
{ width: 320, height: 568, deviceScaleFactor: 2, mobile: true }(仿 iPhone SE 第一代尺寸),验证 innerWidth=320
- emulate
device: 'iphone-17-pro-max'后 console eval 一段绑定 touchstart 的代码 + 用browser_click点击,验证 touchstart fired