blob: 3ffa1e7fe1c7d875fd7b5bf69921a32cfc9c9493 [file]
// Copyright 2026 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/test/run_until.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "chrome/browser/actor/actor_test_util.h"
#include "chrome/browser/actor/tools/tools_test_util.h"
#include "components/actor/core/actor_features.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 "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#if (!BUILDFLAG(IS_ANDROID))
namespace {
class TestDevToolsClient : public ::content::DevToolsAgentHostClient {
public:
TestDevToolsClient() = default;
~TestDevToolsClient() override { Detach(); }
void WaitForInvokedEvent() {
ASSERT_TRUE(
base::test::RunUntil([this]() { return !invoked_events_.empty(); }));
}
void WaitForRespondedEvent() {
ASSERT_TRUE(
base::test::RunUntil([this]() { return !responded_events_.empty(); }));
}
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 DevToolsScriptToolTest : public actor::ActorToolsTest,
public testing::WithParamInterface<bool> {
public:
DevToolsScriptToolTest() {
std::vector<base::test::FeatureRef> enabled_features = {
blink::features::kWebMCP, blink::features::kDevToolsWebMCPSupport,
actor::kGlicActorEnableScriptTools};
std::vector<base::test::FeatureRef> disabled_features;
if (GetParam()) {
enabled_features.push_back(::features::kBackForwardCache);
} else {
disabled_features.push_back(::features::kBackForwardCache);
}
features_.InitWithFeatures(enabled_features, disabled_features);
}
void SetUpOnMainThread() override {
actor::ActorToolsTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(embedded_https_test_server().Start());
}
struct ToolResult {
actor::mojom::ActionResultPtr action_result;
actor::mojom::ScriptToolResponsePtr response;
};
ToolResult RunScriptTool(std::unique_ptr<actor::ToolRequest> action) {
actor::ActResultFuture result;
actor_task().Act(actor::ToRequestList(std::move(action)),
result.GetCallback());
ExpectOkResult(result);
const auto& action_results = result.Get();
EXPECT_EQ(action_results.size(), 1u);
EXPECT_TRUE(action_results.at(0).result);
actor::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_;
};
} // namespace
IN_PROC_BROWSER_TEST_P(DevToolsScriptToolTest, 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 =
actor::MakeScriptToolRequest(*main_frame(), "echo", input_arguments);
auto [action_result, response] = RunScriptTool(std::move(action));
EXPECT_EQ(response->result, "test_input");
client.WaitForInvokedEvent();
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);
client.WaitForRespondedEvent();
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, "Completed");
client.Detach();
}
IN_PROC_BROWSER_TEST_P(DevToolsScriptToolTest, 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 =
actor::MakeScriptToolRequest(*main_frame(), "invalid", input_arguments);
actor::ActResultFuture result;
actor_task().Act(actor::ToRequestList(std::move(action)),
result.GetCallback());
ExpectErrorResult(result,
actor::mojom::ActionResultCode::kScriptToolInvalidName);
client.WaitForInvokedEvent();
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);
client.WaitForRespondedEvent();
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_P(DevToolsScriptToolTest, 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 = actor::MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
auto [action_result, response] = RunScriptTool(std::move(action));
client.WaitForInvokedEvent();
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);
client.WaitForRespondedEvent();
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, "Completed");
client.Detach();
}
IN_PROC_BROWSER_TEST_P(DevToolsScriptToolTest,
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 = actor::MakeScriptToolRequest(*main_frame(), "declarative_tool",
declarative_input);
auto [action_result, response] = RunScriptTool(std::move(action));
ASSERT_TRUE(response);
EXPECT_EQ(response->input_arguments, declarative_input);
EXPECT_EQ(response->tool->name, "declarative_tool");
client.WaitForInvokedEvent();
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);
// Wait for the toolResponded event instead of asserting it doesn't exist.
client.WaitForRespondedEvent();
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, "Completed");
client.Detach();
}
INSTANTIATE_TEST_SUITE_P(All,
DevToolsScriptToolTest,
testing::Bool(),
[](const testing::TestParamInfo<bool>& info) {
return info.param ? "BFCacheEnabled"
: "BFCacheDisabled";
});
#endif // !BUILDFLAG(IS_ANDROID)