blob: 4739b36a44ec7ca64268201ac60e30dc9b0c5529 [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/feature_list.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "chrome/browser/actor/actor_features.h"
#include "chrome/browser/actor/actor_keyed_service.h"
#include "chrome/browser/actor/actor_task.h"
#include "chrome/browser/actor/actor_test_util.h"
#include "chrome/browser/actor/tools/script_tool_request.h"
#include "chrome/browser/actor/tools/tool_request.h"
#include "chrome/browser/actor/tools/tools_test_util.h"
#include "chrome/common/actor.mojom.h"
#include "components/optimization_guide/content/browser/page_content_proto_provider.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/devtools_agent_host_client.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_throttle.h"
#include "content/public/test/test_navigation_throttle_inserter.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
using base::test::TestFuture;
namespace actor {
namespace {
class TestDevToolsClient : public ::content::DevToolsAgentHostClient {
public:
TestDevToolsClient() = default;
~TestDevToolsClient() override { Detach(); }
void AttachToAndEnableWebMCP(
scoped_refptr<content::DevToolsAgentHost> agent_host) {
agent_host_ = agent_host;
agent_host_->AttachClient(this);
const std::string enable_message =
R"JSON({"id": 1, "method": "WebMCP.enable"})JSON";
agent_host_->DispatchProtocolMessage(this,
base::as_byte_span(enable_message));
}
void Detach() {
if (agent_host_) {
agent_host_->DetachClient(this);
agent_host_ = nullptr;
}
}
void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host,
base::span<const uint8_t> message) override {
std::string_view message_str(reinterpret_cast<const char*>(message.data()),
message.size());
std::optional<base::Value> parsed = base::test::ParseJson(message_str);
if (!parsed || !parsed->is_dict()) {
return;
}
const base::DictValue& dict = parsed->GetDict();
const std::string* method = dict.FindString("method");
if (!method) {
return;
}
if (*method == "WebMCP.toolInvoked") {
invoked_events_.push_back(std::move(*parsed));
} else if (*method == "WebMCP.toolResponded") {
responded_events_.push_back(std::move(*parsed));
}
}
void AgentHostClosed(content::DevToolsAgentHost* agent_host) override {}
const std::vector<base::Value>& invoked_events() const {
return invoked_events_;
}
const std::vector<base::Value>& responded_events() const {
return responded_events_;
}
private:
scoped_refptr<content::DevToolsAgentHost> agent_host_;
std::vector<base::Value> invoked_events_;
std::vector<base::Value> responded_events_;
};
class ActorToolsTestScriptTool : public ActorToolsTest {
public:
ActorToolsTestScriptTool() {
features_.InitWithFeatures(
{blink::features::kWebMCP, blink::features::kDevToolsWebMCPSupport,
actor::kGlicActorEnableScriptTools},
{});
}
void SetUpOnMainThread() override {
ActorToolsTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(embedded_https_test_server().Start());
}
struct ToolResult {
mojom::ActionResultPtr action_result;
mojom::ScriptToolResponsePtr response;
};
ToolResult RunScriptTool(std::unique_ptr<ToolRequest> action) {
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectOkResult(result);
const auto& action_results = result.Get();
EXPECT_EQ(action_results.size(), 1u);
EXPECT_TRUE(action_results.at(0).result);
mojom::ActionResultPtr action_result = action_results.at(0).result->Clone();
actor::mojom::ScriptToolResponsePtr response =
std::move(action_results.at(0).result->script_tool_response);
EXPECT_TRUE(response);
return {std::move(action_result), std::move(response)};
}
private:
base::test::ScopedFeatureList features_;
};
class ActorToolsTestScriptToolWithStability : public ActorToolsTestScriptTool {
public:
ActorToolsTestScriptToolWithStability() {
features_.InitAndEnableFeatureWithParameters(
actor::kActorScriptToolDelayObservation,
{{"script_tool_delay_observation_ms", "1000"}});
}
private:
base::test::ScopedFeatureList features_;
};
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptToolWithStability,
PageStabilityDelay) {
const GURL url = embedded_test_server()->GetURL("/actor/script_tool.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const std::string input_arguments = R"JSON({"text": "test"})JSON";
auto action = MakeScriptToolRequest(*main_frame(), "echo", input_arguments);
base::TimeTicks start = base::TimeTicks::Now();
auto [action_result, response] = RunScriptTool(std::move(action));
base::TimeDelta duration = base::TimeTicks::Now() - start;
EXPECT_EQ(response->result, "test");
// The delay should be at least 1000ms.
EXPECT_GE(duration, base::Milliseconds(1000));
EXPECT_TRUE(action_result->requires_page_stabilization);
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, Basic) {
const GURL url = embedded_test_server()->GetURL("/actor/script_tool.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const std::string input_arguments =
R"JSON(
{ "text": "This is an example sentence." }
)JSON";
auto action = MakeScriptToolRequest(*main_frame(), "echo", input_arguments);
auto [action_result, response] = RunScriptTool(std::move(action));
EXPECT_EQ(response->result, "This is an example sentence.");
EXPECT_EQ(response->input_arguments, input_arguments);
EXPECT_EQ(response->tool->name, "echo");
EXPECT_EQ(response->tool->description, "echo input");
EXPECT_EQ(response->tool->annotations->read_only, true);
EXPECT_FALSE(action_result->requires_page_stabilization);
const std::string expected_input_schema =
R"JSON({"type":"object","properties":{"text":{"description":)JSON"
R"JSON("Value to echo","type":"string"}},"required":["text"]})JSON";
EXPECT_EQ(response->tool->input_schema, expected_input_schema);
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, EmitsCdpEvents) {
const GURL url = embedded_test_server()->GetURL("/actor/script_tool.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
TestDevToolsClient client;
client.AttachToAndEnableWebMCP(
content::DevToolsAgentHost::GetOrCreateFor(web_contents()));
const std::string input_arguments = R"JSON({"text": "test_input"})JSON";
auto action = MakeScriptToolRequest(*main_frame(), "echo", input_arguments);
auto [action_result, response] = RunScriptTool(std::move(action));
EXPECT_EQ(response->result, "test_input");
ASSERT_EQ(client.invoked_events().size(), 1u);
const base::DictValue& invoked_event = client.invoked_events()[0].GetDict();
const base::DictValue* invoked_params = invoked_event.FindDict("params");
ASSERT_TRUE(invoked_params);
const std::string* tool_name = invoked_params->FindString("toolName");
ASSERT_TRUE(tool_name);
EXPECT_EQ(*tool_name, "echo");
const std::string* input = invoked_params->FindString("input");
ASSERT_TRUE(input);
EXPECT_EQ(*input, input_arguments);
const std::string* invocation_id = invoked_params->FindString("invocationId");
ASSERT_TRUE(invocation_id);
ASSERT_EQ(client.responded_events().size(), 1u);
const base::DictValue& responded_event =
client.responded_events()[0].GetDict();
const base::DictValue* responded_params = responded_event.FindDict("params");
ASSERT_TRUE(responded_params);
const std::string* responded_invocation_id =
responded_params->FindString("invocationId");
ASSERT_TRUE(responded_invocation_id);
EXPECT_EQ(*responded_invocation_id, *invocation_id);
const std::string* status = responded_params->FindString("status");
ASSERT_TRUE(status);
EXPECT_EQ(*status, "Success");
client.Detach();
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, BadToolName) {
const GURL url = embedded_test_server()->GetURL("/actor/script_tool.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const std::string input_arguments =
R"JSON(
{ "text": "This is an example sentence." }
)JSON";
auto action =
MakeScriptToolRequest(*main_frame(), "invalid", input_arguments);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectErrorResult(result, mojom::ActionResultCode::kScriptToolInvalidName);
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, EmitsCdpEventsOnFailure) {
const GURL url = embedded_test_server()->GetURL("/actor/script_tool.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
TestDevToolsClient client;
client.AttachToAndEnableWebMCP(
content::DevToolsAgentHost::GetOrCreateFor(web_contents()));
const std::string input_arguments = R"JSON({"text": "test_input"})JSON";
auto action =
MakeScriptToolRequest(*main_frame(), "invalid", input_arguments);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectErrorResult(result, mojom::ActionResultCode::kScriptToolInvalidName);
ASSERT_EQ(client.invoked_events().size(), 1u);
const base::DictValue& invoked_event = client.invoked_events()[0].GetDict();
const base::DictValue* invoked_params = invoked_event.FindDict("params");
ASSERT_TRUE(invoked_params);
const std::string* tool_name = invoked_params->FindString("toolName");
ASSERT_TRUE(tool_name);
EXPECT_EQ(*tool_name, "invalid");
const std::string* input = invoked_params->FindString("input");
ASSERT_TRUE(input);
EXPECT_EQ(*input, input_arguments);
const std::string* invocation_id = invoked_params->FindString("invocationId");
ASSERT_TRUE(invocation_id);
ASSERT_EQ(client.responded_events().size(), 1u);
const base::DictValue& responded_event =
client.responded_events()[0].GetDict();
const base::DictValue* responded_params = responded_event.FindDict("params");
ASSERT_TRUE(responded_params);
const std::string* responded_invocation_id =
responded_params->FindString("invocationId");
ASSERT_TRUE(responded_invocation_id);
EXPECT_EQ(*responded_invocation_id, *invocation_id);
const std::string* status = responded_params->FindString("status");
ASSERT_TRUE(status);
EXPECT_EQ(*status, "Error");
const std::string* error_text = responded_params->FindString("errorText");
ASSERT_TRUE(error_text);
EXPECT_EQ(*error_text, "Tool not found: invalid");
client.Detach();
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, DeclarativeTool) {
const GURL url =
embedded_test_server()->GetURL("/actor/declarative_script_tool.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const std::string declarative_input =
R"JSON(
{
"text": "text #1",
"text2": "text #2",
"select": "Option 2"
}
)JSON";
auto action = MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
auto [action_result, response] = RunScriptTool(std::move(action));
EXPECT_EQ(response->tool->name, "declarative_tool");
EXPECT_EQ(response->input_arguments, declarative_input);
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool,
EmitsCdpEventsDeclarativeTool) {
const GURL url =
embedded_test_server()->GetURL("/actor/declarative_script_tool.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
TestDevToolsClient client;
client.AttachToAndEnableWebMCP(
content::DevToolsAgentHost::GetOrCreateFor(web_contents()));
const std::string declarative_input =
R"JSON(
{
"text": "text #1",
"text2": "text #2",
"select": "Option 2"
}
)JSON";
auto action = MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
auto [action_result, response] = RunScriptTool(std::move(action));
ASSERT_EQ(client.invoked_events().size(), 1u);
const base::DictValue& invoked_event = client.invoked_events()[0].GetDict();
const base::DictValue* invoked_params = invoked_event.FindDict("params");
ASSERT_TRUE(invoked_params);
const std::string* tool_name = invoked_params->FindString("toolName");
ASSERT_TRUE(tool_name);
EXPECT_EQ(*tool_name, "declarative_tool");
const std::string* input = invoked_params->FindString("input");
ASSERT_TRUE(input);
// Compare parsed JSON to ignore whitespace differences
std::optional<base::Value> parsed_input = base::test::ParseJson(*input);
std::optional<base::Value> parsed_expected_input =
base::test::ParseJson(declarative_input);
ASSERT_TRUE(parsed_input);
ASSERT_TRUE(parsed_expected_input);
EXPECT_EQ(*parsed_input, *parsed_expected_input);
const std::string* invocation_id = invoked_params->FindString("invocationId");
ASSERT_TRUE(invocation_id);
ASSERT_EQ(client.responded_events().size(), 1u);
const base::DictValue& responded_event =
client.responded_events()[0].GetDict();
const base::DictValue* responded_params = responded_event.FindDict("params");
ASSERT_TRUE(responded_params);
const std::string* responded_invocation_id =
responded_params->FindString("invocationId");
ASSERT_TRUE(responded_invocation_id);
EXPECT_EQ(*responded_invocation_id, *invocation_id);
const std::string* status = responded_params->FindString("status");
ASSERT_TRUE(status);
EXPECT_EQ(*status, "Success");
client.Detach();
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, NavigateAfterResponse) {
const GURL url = embedded_test_server()->GetURL(
"/actor/script_tool_navigate_after_response.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const std::string input_arguments =
R"JSON(
{ "text": "This is an example sentence." }
)JSON";
auto action = MakeScriptToolRequest(*main_frame(), "echo", input_arguments);
auto [action_result, response] = RunScriptTool(std::move(action));
EXPECT_EQ(response->result, "This is an example sentence.");
}
// TODO(crbug.com/492477322): Re-enable this test.
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, DISABLED_DeclarativeToolCrossDocument) {
const GURL url = embedded_test_server()->GetURL(
"/actor/declarative_script_tool_cross_document.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const std::string declarative_input =
R"JSON(
{
"echo": "hello world"
}
)JSON";
auto action = MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
auto [action_result, response] = RunScriptTool(std::move(action));
EXPECT_EQ(response->input_arguments, declarative_input);
EXPECT_EQ(response->tool->name, "declarative_tool");
EXPECT_EQ(response->tool->description, "A declarative WebMCP tool");
EXPECT_FALSE(response->tool->annotations);
EXPECT_EQ(response->tool->input_schema, "{}");
base::Value actual_json = base::test::ParseJson(*response->result);
base::Value expected_json = base::test::ParseJson(R"JSON(
[
{
"@context": "https://schema.org",
"@type": "Message",
"text": "echoed value"
},
{
"@context": "https://schema.org",
"@type": "Message",
"text": "extra stuff"
}
]
)JSON");
EXPECT_EQ(actual_json, expected_json);
}
// TODO(crbug.com/496357393): Re-enable this test.
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool,
DISABLED_EmitsCdpEventsDeclarativeToolCrossDocument) {
const GURL url = embedded_test_server()->GetURL(
"/actor/declarative_script_tool_cross_document.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
TestDevToolsClient client;
client.AttachToAndEnableWebMCP(
content::DevToolsAgentHost::GetOrCreateFor(web_contents()));
const std::string declarative_input =
R"JSON(
{
"echo": "hello world"
}
)JSON";
auto action = MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
auto [action_result, response] = RunScriptTool(std::move(action));
EXPECT_EQ(response->input_arguments, declarative_input);
EXPECT_EQ(response->tool->name, "declarative_tool");
ASSERT_EQ(client.invoked_events().size(), 1u);
const base::DictValue& invoked_event = client.invoked_events()[0].GetDict();
const base::DictValue* invoked_params = invoked_event.FindDict("params");
ASSERT_TRUE(invoked_params);
const std::string* tool_name = invoked_params->FindString("toolName");
ASSERT_TRUE(tool_name);
EXPECT_EQ(*tool_name, "declarative_tool");
const std::string* input = invoked_params->FindString("input");
ASSERT_TRUE(input);
EXPECT_EQ(*input, declarative_input);
const std::string* invocation_id = invoked_params->FindString("invocationId");
ASSERT_TRUE(invocation_id);
// Cross document tools do not send a WebMCP.toolResponded event.
EXPECT_EQ(client.responded_events().size(), 0u);
client.Detach();
}
// TODO(crbug.com/496357393): Re-enable this test.
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool,
DISABLED_DeclarativeToolCrossDocument_No_Autosubmit) {
const GURL url = embedded_test_server()->GetURL(
"/actor/declarative_script_tool_pause.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const std::string declarative_input =
R"JSON(
{
"echo": "hello world"
}
)JSON";
auto action = MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
// Wait for the task to be paused. The Act() call has not returned yet.
ASSERT_TRUE(base::test::RunUntil([&]() {
return actor_task().GetState() == ActorTask::State::kPausedByActor;
}));
// Trigger the submission manually.
ASSERT_TRUE(content::ExecJs(web_contents(),
"document.querySelector('button').click()"));
// The Act() call should now complete successfully with the result of the
// navigation.
ExpectOkResult(result);
EXPECT_EQ(actor_task().GetState(), ActorTask::State::kReflecting);
const auto& action_results = result.Get();
ASSERT_EQ(action_results.size(), 1u);
ASSERT_TRUE(action_results.at(0).result->script_tool_response);
base::Value actual_json = base::test::ParseJson(
*action_results.at(0).result->script_tool_response->result);
// The result should match the static content of the page.
base::Value expected_json = base::test::ParseJson(R"JSON(
[
{
"@context": "https://schema.org",
"@type": "Message",
"text": "echoed value"
},
{
"@context": "https://schema.org",
"@type": "Message",
"text": "extra stuff"
}
]
)JSON");
EXPECT_EQ(actual_json, expected_json);
// Verify that the task can be stopped cleanly.
actor_task().Stop(ActorTask::StoppedReason::kTaskComplete);
EXPECT_EQ(actor_keyed_service().GetTask(task_id_), nullptr);
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, Histograms) {
base::HistogramTester histogram_tester;
const GURL url = embedded_test_server()->GetURL("/actor/script_tool.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const std::string valid_input_arguments = R"JSON({"text": "test"})JSON";
auto action =
MakeScriptToolRequest(*main_frame(), "echo", valid_input_arguments);
auto [action_result, response] = RunScriptTool(std::move(action));
histogram_tester.ExpectUniqueSample("Actor.Tools.ScriptTool.InputSizeBytes",
valid_input_arguments.size(), 1);
histogram_tester.ExpectUniqueSample("Actor.Tools.ScriptTool.InvocationResult",
true, 1);
histogram_tester.ExpectUniqueSample("Actor.Tools.ScriptTool.ActionResultCode",
mojom::ActionResultCode::kOk, 1);
histogram_tester.ExpectUniqueSample("Actor.Tools.ScriptTool.OutputSizeBytes",
std::string("test").size(), 1);
// Test a failure case.
const std::string input_arguments = R"JSON({})JSON";
auto bad_action =
MakeScriptToolRequest(*main_frame(), "invalid", input_arguments);
ActResultFuture result;
actor_task().Act(ToRequestList(bad_action), result.GetCallback());
ExpectErrorResult(result, mojom::ActionResultCode::kScriptToolInvalidName);
histogram_tester.ExpectBucketCount("Actor.Tools.ScriptTool.InputSizeBytes",
input_arguments.size(), 1);
histogram_tester.ExpectBucketCount("Actor.Tools.ScriptTool.InvocationResult",
false, 1);
histogram_tester.ExpectBucketCount(
"Actor.Tools.ScriptTool.ActionResultCode",
mojom::ActionResultCode::kScriptToolInvalidName, 1);
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, NavigationFailed) {
const GURL url = embedded_test_server()->GetURL(
"/actor/declarative_script_tool_cross_document.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Insert a throttle to cancel the navigation.
content::TestNavigationThrottleInserter throttle_inserter(
web_contents(),
base::BindLambdaForTesting(
[&](content::NavigationThrottleRegistry& registry) -> void {
auto throttle =
std::make_unique<content::TestNavigationThrottle>(registry);
throttle->SetResponse(
content::TestNavigationThrottle::WILL_START_REQUEST,
content::TestNavigationThrottle::SYNCHRONOUS,
content::NavigationThrottle::CANCEL);
registry.AddThrottle(std::move(throttle));
}));
const std::string declarative_input = R"JSON({"echo": "hello world"})JSON";
auto action = MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectErrorResult(result,
mojom::ActionResultCode::kScriptToolNavigationDidNotCommit);
}
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, NavigationCommittedErrorPage) {
const GURL url = embedded_test_server()->GetURL(
"/actor/declarative_script_tool_cross_document.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Change form action to a non-existent path on the same server, which should
// result in an error page (404).
const GURL error_url = embedded_test_server()->GetURL("/non-existent");
ASSERT_TRUE(content::ExecJs(
web_contents(),
content::JsReplace("document.querySelector('form').action = $1",
error_url)));
const std::string declarative_input = R"JSON({"echo": "hello world"})JSON";
auto action = MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectErrorResult(
result, mojom::ActionResultCode::kScriptToolNavigationCommittedErrorPage);
}
// TODO(crbug.com/492477322): Enable for bfcache.
IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, NavigationFailedLoad) {
if (!base::FeatureList::IsEnabled(features::kBackForwardCache)) {
GTEST_SKIP() << "Skipping when bfcache is disabled; crbug.com/492477322";
}
const GURL url = embedded_test_server()->GetURL(
"/actor/declarative_script_tool_cross_document.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
const GURL fail_url = embedded_test_server()->GetURL(
"/actor/declarative_script_tool_cross_document_fail.html");
ASSERT_TRUE(content::ExecJs(
web_contents(),
content::JsReplace("document.querySelector('form').action = $1",
fail_url)));
const std::string declarative_input = R"JSON({"echo": "hello world"})JSON";
auto action = MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
ActResultFuture result;
actor_task().Act(ToRequestList(action), result.GetCallback());
ExpectErrorResult(result,
mojom::ActionResultCode::kScriptToolNavigationFailedLoad);
}
} // namespace
} // namespace actor