Raise on evaluate_js failure instead of returning a null DataSpace - #905
Raise on evaluate_js failure instead of returning a null DataSpace#905giordano-lucas wants to merge 4 commits into
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review. WalkthroughThe default Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: ⚪ Minimal · up to The PR makes evaluate_js failures raise descriptive errors while preserving successful results and opt-out behavior; no actionable merge-blocking risk remains, so it is merge-ready after normal checks. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This comment has been minimized.
This comment has been minimized.
|
| Filename | Overview |
|---|---|
| packages/notte-browser/src/notte_browser/session.py | Adds explicit evaluate-JavaScript exceptions and consistently applies the configured failure-raising policy. |
| packages/notte-sdk/src/notte_sdk/endpoints/sessions.py | Raises remote unsuccessful execution results and recovers detailed messages from generic serialized action errors. |
| tests/test_session.py | Covers local timeout, Playwright error, successful JavaScript values, and non-exception failure behavior. |
| tests/sdk/test_execute_raise_on_failure.py | Covers serialized remote failures across error modes and both raising and non-raising configurations. |
| docs/src/features/sessions/configuration.mdx | Corrects the documented raise_on_failure default to true. |
Reviews (1): Last reviewed commit: "Document raise_on_failure's actual defau..." | Re-trigger Greptile
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
`execute(type="evaluate_js")` caught `asyncio.TimeoutError` and `PlaywrightError`, set `success=False` and a descriptive `message`, but left `exception=None`. Both raise gates key off `exception`, not `success`, so `raise_on_session_execution_failure = true` (the shipped default) never fired: callers got `success=False, data=None, exception=None` and then crashed on `result.data.markdown` with `'NoneType' object has no attribute 'markdown'`, while the real reason sat unread in `.message`. `scrape()` in the same file already gets this right: it records the failure with `exception=e` and re-raises unless `raise_on_failure=False`, in which case it returns a value that says it failed. Make the eval-js path behave the same way by attaching an `ActionExecutionError` carrying the message as its reason, the way the controller already does for "Element is disabled". On the remote path the exception is serialised with the user-facing message, which drops the action-specific reason. Extend the existing generic-message fallback in the SDK so `ActionExecutionError`'s user message is recognised too, and the caller is raised the actual reason rather than "Sorry, this action cannot be executed at the moment.". The success path is untouched: a JS `null` still yields `data.markdown == "null"` and `success=True`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Separate from the evaluate_js fix, and droppable on its own. Both raise gates asked "did something throw" (`exception is not None`) rather than "did the action fail" (`not success`). Anything that reports failure by returning is therefore invisible to `raise_on_failure`: a tool whose `ExecutionResult` carries `success=False`, and `controller.execute` returning `False`. evaluate_js was the loudest instance of this class, but it is not the only one, so gate on `success` and synthesise an `ActionExecutionError` from `.message` when no exception is available. This is a real behaviour change for callers who currently receive a silent `success=False`. Every in-repo consumer that wants quiet failures already opts out explicitly (`notte_agent/agent.py`, `notte_agent/agent_fallback.py`, and the MCP server's session), and the documented pattern for optional actions is `raise_on_failure=False`. The blast radius is narrow in practice: `controller.execute` raises on interaction failures rather than returning `False` (the only `False` it returns is for `HelpAction`), and no in-repo tool returns `success=False`. Third-party tools that do will now raise by default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`raise_on_session_execution_failure = true` in `notte-core/config.toml`, and both `NotteSession` and `RemoteSession` default `raise_on_failure` to it. The session configuration page said the default was false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The unit tests cover both failure branches; this covers the pattern the
builder generated, which is what the branches are for. Reduced from the
tapology.com, pokerdb.thehendonmob.com and carrefour.be sources: a script
that reads a property off an element it expects, run against a page that does
not have it - a block page, an interstitial, a redesign.
Against the code before this branch the first test reports
Tapology bout search request failed: 'NoneType' object has no attribute 'markdown'
which is verbatim what the catalogue's ledger recorded for that Function.
The third case pins the opt-out path rather than the fix: raise_on_failure=
False returned .message before this branch too. It is here because that is
what the builder prompt now teaches callers to read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Found 1 test failure on Blacksmith runners: Failure
|
18ab81d to
f80671f
Compare
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light.png)
Summary
execute(type="evaluate_js")failed silently, whilescrape()— the same contract, in the same file — got it right.Both eval-js failure branches in
NotteSession._aexecute_implsetsuccess = Falseand a descriptivemessage, but leftexception = None. Both raise gates key offexception, notsuccess:notte-browser/src/notte_browser/session.py:if _raise_on_failure and exception is not Nonenotte-sdk/src/notte_sdk/endpoints/sessions.py:if _raise_on_failure and result.exception is not Noneraise_on_session_execution_failure = trueis the shipped default, so it never fired. The caller gotsuccess=False, data=None, exception=Noneand crashed two lines later onresult.data.markdownwith'NoneType' object has no attribute 'markdown', with the real reason ("JavaScript evaluation timed out after 45000ms") sitting unread in.message. Five deployed marketplace Functions failed this way; one turned "the page never loaded" into a reported JSON parse error.scrape()already does the right thing: it records the failure to the trajectory withexception=eand re-raises unlessraise_on_failure=False, in which case it returns a value that says it failed rather thanNone. This makes eval-js behave the same way.What changed
1.
Raise on evaluate_js failure instead of returning a null DataSpaceActionExecutionErrorcarryingmessageas itsreasonon both theasyncio.TimeoutErrorandPlaywrightErrorbranches, the waycontroller.pyalready does for "Element is disabled"ActionExecutionError's user-facing message, so a remote caller is raised the actual reason instead of"Sorry, this action cannot be executed at the moment."The success path is untouched. A JS
nullis still a successful evaluation returningdata.markdown == "null", which is what makesresult.data is Noneafter an eval an unambiguous failure signal.2.⚠️ behaviour change, droppable
Gate raise_on_failure on failure, not on an exception having been thrown—Kept as a separate commit so it can be dropped in review without losing the eval-js fix.
Both gates now read
not successinstead ofexception is not None, synthesising anActionExecutionErrorfrom.messagewhen no exception is available. This is a real behaviour change for external callers who today receive a silentsuccess=False— most relevantly anyone with a customBaseToolwhoseExecutionResultcarriessuccess=False, which will now raise under the default.Evidence it is safe in-repo — every consumer that wants quiet failures already opts out explicitly:
notte-agent/src/notte_agent/agent.py:251—raise_on_failure=Falsenotte-agent/src/notte_agent/agent_fallback.py:127—raise_on_failure=False(and it rejectsraise_on_failure=Trueoutright at line 104)notte-mcp/src/notte_mcp/server.py:138— builds its session withraise_on_failure=Falseraise_on_failure=False(docs/src/guides/web_automation_tips.mdx,browser-controls/{conditional_actions,error_handling}.mdx,guides/handle_optional_popup.mdx)And the practical blast radius is narrower than it looks:
controller.execute()returns a bool, but it raises on interaction failures (ActionExecutionError,InvalidActionError,FailedToUploadFileError, …) rather than returningFalse— the onlyFalseit returns is forHelpAction, which is agent-only. So a failed click already raises today. No in-repo tool returnssuccess=False. The paths this commit actually newly covers are third-party tools andHelpAction.3.
Document raise_on_failure's actual default—docs/src/features/sessions/configuration.mdxclaimeddefault={false};notte-core/src/notte_core/config.tomlsets it totrue.The remote path
Catalog Functions run against the API, so the action executes server-side and the failure has to survive serialisation. Verified by round-tripping a real
ExecutionResultthroughmodel_dump_json()/model_validate_json():ExecutionResult.exceptionserialises viajson_encoderstostr(e), and thefield_validatorrebuilds it as aNotteBaseError. The concrete type is not preserved, so a remote caller can never receiveActionExecutionErroritself — only aNotteBaseError.str(e)is the message captured at construction time, i.e. whateverErrorConfigmode the API is in. The existing_GENERIC_UNEXPECTED_MESSAGESentries are verbatimuser_messagestrings fromnotte_browser/errors.py, which is good evidence the API serialises in user mode. In that modeActionExecutionErrorreduces to"Sorry, this action cannot be executed at the moment. …"and the reason is lost — hence the prefix check added in commit 1.tests/sdk/test_execute_raise_on_failure.pycovers bothdeveloperanduserserver modes and fails on theusercase without it.success=False, exception=None, and the client raises the reason from.messageanyway.Tests
tests/test_session.py(local) andtests/sdk/test_execute_raise_on_failure.py(remote, new file):raise_on_failure=Falsestill returns a result that says it failed —success=False,messageintact,exceptionset — and does not regress to returningNonenull→data.markdown == "null",success=True,exception is NoneFalseraises under the default and stays quiet withraise_on_failure=Falsedeveloperanduserserver error modes, and when the server attaches no exception at allRan:
uv run pytest tests/test_session.py -q— 33 passed, 1 skipped, 1 failed:test_step_should_return_valid_timed_span, which calls Gemini and fails withkey=Nonein a worktree with no.env. It passes in the main checkout, and it fails identically with these commits stashed.uv run pytest tests/sdk/test_execute_raise_on_failure.py -q— 5 passeduv run pytest tests/browser tests/test_trajectory.py tests/actions tests/mcp tests/code tests/config -q— 173 passed, 7 skipped. The 2 failures + 2 errors are all missing-credentials or network flakiness (test_tools.pyneeds a realNOTTE_API_KEY;test_screenshot_types.pydepends on google.com's live DOM and fails identically with these commits stashed).pre-commiton every commit:ruff check,ruff format,basedpyright(0 errors, 0 warnings), detect-secrets, forbidden/playwright import checks, docs link checks — all pass. Thedocs-sdk-generatehook was skipped: it re-fetcheshttps://api.notte.cc/openapi.jsonand rewritesdocs/src/llms.txtwith unrelated live-API drift (mailboxes, profile-duplicate, …). No public signature or docstring changed, so no reference doc regeneration is warranted.Not run: integration suites requiring API credentials (
tests/integration/**), and no marketplace sweeps.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit
New Features
Configuration
raise_on_failurenow defaults to enabled.Bug Fixes
Added: the generated code, as a test
The unit tests above cover the two failure branches. This covers the thing the branches exist for — the pattern the anything-api builder actually emitted, reduced from the real
tapology.com,pokerdb.thehendonmob.comandcarrefour.besources:tests/integration/test_generated_function_patterns.py, which the CI suite already picks up (onlytest_webvoyager_resolutionandtest_e2eare excluded). Runs in ~9s againstexample.com, the page the other integration tests here use.The failure is provoked the way it happened in production rather than synthetically: a script that reads a property off an element it expects, run against a page that does not have it. That is what a block page, an interstitial or a redesign looks like from inside
evaluate_js, and it is how at least three of the five actually failed.Checked against the code before this branch, the first test reports:
which is verbatim what the catalogue ledger recorded for that Function in production. With the branch, the same call reports
JavaScript evaluation failed: ...and names the page.Three cases:
or ""variant no longer reports bad JSONraise_on_failure=Falsereturned.messagebefore this branch tooThe third is a characterisation test, not a regression test, and is included because it pins the path the builder prompt now teaches callers to take. Calling that out so nobody reads three green ticks as three guarantees.
Suite: 36 passed. One unrelated pre-existing failure,
test_step_should_return_valid_timed_span, which makes a live LLM call and fails on credentials in a worktree — it fails identically without these commits.