From ce119741cea274d587ee43d8bca24689bb49b3e3 Mon Sep 17 00:00:00 2001 From: Lhy099 Date: Sat, 25 Apr 2026 16:31:45 +0800 Subject: [PATCH 1/2] fix: support stdin scripts and preserve JS IIFE returns --- helpers.py | 9 +++++++-- run.py | 31 +++++++++++++++++++++++++------ test_js.py | 38 +++++++++++++++++++++++++++++++++----- test_run.py | 12 ++++++++++++ 4 files changed, 77 insertions(+), 13 deletions(-) diff --git a/helpers.py b/helpers.py index fd81b806..e965932a 100644 --- a/helpers.py +++ b/helpers.py @@ -197,6 +197,11 @@ def wait_for_load(timeout=15.0): time.sleep(0.3) return False +def _exception_text(r): + d = r.get("exceptionDetails") or {} + e = d.get("exception") or {} + return "\n".join(str(x) for x in (d.get("text"), e.get("description"), e.get("value")) if x) + def js(expression, target_id=None): """Run JS in the attached tab (default) or inside an iframe target (via iframe_target()). @@ -204,9 +209,9 @@ def js(expression, target_id=None): `document.title` and `const x = 1; return x` are valid inputs. """ sid = cdp("Target.attachToTarget", targetId=target_id, flatten=True)["sessionId"] if target_id else None - if "return " in expression and not expression.strip().startswith("("): - expression = f"(function(){{{expression}}})()" r = cdp("Runtime.evaluate", session_id=sid, expression=expression, returnByValue=True, awaitPromise=True) + if "Illegal return statement" in _exception_text(r): + r = cdp("Runtime.evaluate", session_id=sid, expression=f"(function(){{{expression}}})()", returnByValue=True, awaitPromise=True) return r.get("result", {}).get("value") diff --git a/run.py b/run.py index 5e763e9e..f70c12d9 100644 --- a/run.py +++ b/run.py @@ -21,11 +21,13 @@ Read SKILL.md for the default workflow and examples. Typical usage: - uv run bh <<'PY' + browser-harness <<'PY' ensure_real_tab() print(page_info()) PY + browser-harness -c "print(page_info())" + Helpers are pre-imported. The daemon auto-starts and connects to the running browser. Commands: @@ -37,6 +39,15 @@ """ +USAGE = 'Usage: browser-harness [-c "print(page_info())"]' + + +def _exec_code(code): + print_update_banner() + ensure_daemon() + exec(code, globals()) + + def main(): args = sys.argv[1:] if args and args[0] in {"-h", "--help"}: @@ -59,11 +70,19 @@ def main(): if args and args[0] == "--debug-clicks": os.environ["BH_DEBUG_CLICKS"] = "1" args = args[1:] - if not args or args[0] != "-c": - sys.exit("Usage: browser-harness -c \"print(page_info())\"") - print_update_banner() - ensure_daemon() - exec(args[1], globals()) + if args and args[0] == "-c": + if len(args) < 2: + sys.exit(USAGE) + _exec_code(args[1]) + return + if args: + sys.exit(USAGE) + if sys.stdin.isatty(): + sys.exit(USAGE) + code = sys.stdin.read() + if not code.strip(): + sys.exit(USAGE) + _exec_code(code) if __name__ == "__main__": diff --git a/test_js.py b/test_js.py index 02d68ae9..ad718274 100644 --- a/test_js.py +++ b/test_js.py @@ -2,11 +2,16 @@ import helpers -def _capture_cdp(): +def _capture_cdp(runtime_results=None): captured = [] + runtime_results = list(runtime_results or [{"result": {"value": None}}]) + def fake_cdp(method, **kwargs): captured.append((method, kwargs)) + if method == "Runtime.evaluate" and runtime_results: + return runtime_results.pop(0) return {"result": {"value": None}} + return fake_cdp, captured @@ -14,6 +19,10 @@ def _evaluated_expression(captured): return next(kw["expression"] for m, kw in captured if m == "Runtime.evaluate") +def _evaluated_expressions(captured): + return [kw["expression"] for m, kw in captured if m == "Runtime.evaluate"] + + def test_simple_expression_passes_through(): fake_cdp, captured = _capture_cdp() with patch("helpers.cdp", side_effect=fake_cdp): @@ -21,11 +30,22 @@ def test_simple_expression_passes_through(): assert _evaluated_expression(captured) == "document.title" -def test_return_statement_gets_wrapped(): - fake_cdp, captured = _capture_cdp() +def test_top_level_return_retries_wrapped(): + fake_cdp, captured = _capture_cdp([ + { + "exceptionDetails": { + "text": "Uncaught SyntaxError: Illegal return statement", + "exception": {"description": "SyntaxError: Illegal return statement"}, + } + }, + {"result": {"value": 1}}, + ]) with patch("helpers.cdp", side_effect=fake_cdp): - helpers.js("const x = 1; return x") - assert _evaluated_expression(captured) == "(function(){const x = 1; return x})()" + assert helpers.js("const x = 1; return x") == 1 + assert _evaluated_expressions(captured) == [ + "const x = 1; return x", + "(function(){const x = 1; return x})()", + ] def test_iife_with_internal_return_is_not_double_wrapped(): @@ -33,3 +53,11 @@ def test_iife_with_internal_return_is_not_double_wrapped(): with patch("helpers.cdp", side_effect=fake_cdp): helpers.js("(function(){ return document.title; })()") assert _evaluated_expression(captured) == "(function(){ return document.title; })()" + + +def test_iife_return_is_not_wrapped(): + fake_cdp, captured = _capture_cdp([{"result": {"value": 1}}]) + expression = "(() => { return 1 })()" + with patch("helpers.cdp", side_effect=fake_cdp): + assert helpers.js(expression) == 1 + assert _evaluated_expressions(captured) == [expression] diff --git a/test_run.py b/test_run.py index 83c46480..f893d22e 100644 --- a/test_run.py +++ b/test_run.py @@ -26,3 +26,15 @@ def test_c_flag_does_not_read_stdin(): run.main() assert not stdin_read, "stdin should not be read when -c is passed" + + +def test_no_args_executes_stdin(): + stdout = StringIO() + with patch.object(sys, "argv", ["browser-harness"]), \ + patch("run.ensure_daemon"), \ + patch("run.print_update_banner"), \ + patch("sys.stdin", StringIO("print('hello from stdin')")), \ + patch("sys.stdout", stdout): + run.main() + + assert stdout.getvalue().strip() == "hello from stdin" From 9b9fb482a39e428d988b3420f1fba6aa52507fda Mon Sep 17 00:00:00 2001 From: Lhy099 Date: Sat, 25 Apr 2026 17:50:28 +0800 Subject: [PATCH 2/2] fix: restrict illegal return retry to syntax errors --- helpers.py | 13 ++++++++++++- test_js.py | 23 ++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/helpers.py b/helpers.py index e965932a..b3f4915d 100644 --- a/helpers.py +++ b/helpers.py @@ -202,6 +202,17 @@ def _exception_text(r): e = d.get("exception") or {} return "\n".join(str(x) for x in (d.get("text"), e.get("description"), e.get("value")) if x) +def _is_illegal_return_syntax_error(r): + d = r.get("exceptionDetails") or {} + e = d.get("exception") or {} + text = str(d.get("text") or "") + description = str(e.get("description") or "") + class_name = e.get("className") + return ( + "Illegal return statement" in (text + "\n" + description) + and (class_name == "SyntaxError" or "SyntaxError:" in text or "SyntaxError:" in description) + ) + def js(expression, target_id=None): """Run JS in the attached tab (default) or inside an iframe target (via iframe_target()). @@ -210,7 +221,7 @@ def js(expression, target_id=None): """ sid = cdp("Target.attachToTarget", targetId=target_id, flatten=True)["sessionId"] if target_id else None r = cdp("Runtime.evaluate", session_id=sid, expression=expression, returnByValue=True, awaitPromise=True) - if "Illegal return statement" in _exception_text(r): + if _is_illegal_return_syntax_error(r): r = cdp("Runtime.evaluate", session_id=sid, expression=f"(function(){{{expression}}})()", returnByValue=True, awaitPromise=True) return r.get("result", {}).get("value") diff --git a/test_js.py b/test_js.py index ad718274..f6bc9082 100644 --- a/test_js.py +++ b/test_js.py @@ -35,7 +35,10 @@ def test_top_level_return_retries_wrapped(): { "exceptionDetails": { "text": "Uncaught SyntaxError: Illegal return statement", - "exception": {"description": "SyntaxError: Illegal return statement"}, + "exception": { + "className": "SyntaxError", + "description": "SyntaxError: Illegal return statement", + }, } }, {"result": {"value": 1}}, @@ -55,6 +58,24 @@ def test_iife_with_internal_return_is_not_double_wrapped(): assert _evaluated_expression(captured) == "(function(){ return document.title; })()" +def test_runtime_error_message_does_not_retry_wrapped(): + fake_cdp, captured = _capture_cdp([ + { + "exceptionDetails": { + "text": "Uncaught Error: Illegal return statement", + "exception": { + "className": "Error", + "description": "Error: Illegal return statement", + }, + } + } + ]) + expression = "throw new Error('Illegal return statement')" + with patch("helpers.cdp", side_effect=fake_cdp): + assert helpers.js(expression) is None + assert _evaluated_expressions(captured) == [expression] + + def test_iife_return_is_not_wrapped(): fake_cdp, captured = _capture_cdp([{"result": {"value": 1}}]) expression = "(() => { return 1 })()"