From 640839f3606f65e4dd492aec2d1653997ef1a4a4 Mon Sep 17 00:00:00 2001
From: SGavrl <277843151+SGavrl@users.noreply.github.com>
Date: Tue, 4 Aug 2026 12:24:27 +0000
Subject: [PATCH] Make MCP navigation tests deterministic
---
crates/obscura-cli/tests/mcp_client.rs | 99 +++++++++++++++++++++++---
1 file changed, 89 insertions(+), 10 deletions(-)
diff --git a/crates/obscura-cli/tests/mcp_client.rs b/crates/obscura-cli/tests/mcp_client.rs
index ff1a8d9..c8ffc30 100644
--- a/crates/obscura-cli/tests/mcp_client.rs
+++ b/crates/obscura-cli/tests/mcp_client.rs
@@ -1,7 +1,78 @@
-use std::io::{BufRead, BufReader, Write};
+use std::io::{BufRead, BufReader, Read, Write};
+use std::net::{SocketAddr, TcpListener, TcpStream};
use std::process::{Child, ChildStdin, Command, Stdio};
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+use std::thread::JoinHandle;
+use std::time::Duration;
const OBSCURA: &str = env!("CARGO_BIN_EXE_obscura");
+const TEST_PAGE: &str = r#"
Example Domain
+Example Domain
Deterministic local MCP fixture.
+More information..."#;
+
+/// A loopback fixture keeps protocol tests independent of public-site bot
+/// policy, DNS, and content changes. The previous example.com dependency can
+/// return a Cloudflare block page, which tests the runner's egress reputation
+/// rather than Obscura navigation, snapshots, or evaluation.
+struct TestPageServer {
+ addr: SocketAddr,
+ stop: Arc,
+ thread: Option>,
+}
+
+impl TestPageServer {
+ fn spawn() -> Self {
+ let listener = TcpListener::bind("127.0.0.1:0").expect("bind local fixture");
+ let addr = listener.local_addr().expect("local fixture address");
+ listener
+ .set_nonblocking(true)
+ .expect("nonblocking local fixture");
+ let stop = Arc::new(AtomicBool::new(false));
+ let thread_stop = Arc::clone(&stop);
+ let thread = std::thread::spawn(move || {
+ while !thread_stop.load(Ordering::Acquire) {
+ match listener.accept() {
+ Ok((mut stream, _)) => {
+ let _ = stream.set_read_timeout(Some(Duration::from_secs(1)));
+ let mut request = [0u8; 2048];
+ let _ = stream.read(&mut request);
+ let response = format!(
+ "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+ TEST_PAGE.len(),
+ TEST_PAGE
+ );
+ let _ = stream.write_all(response.as_bytes());
+ let _ = stream.flush();
+ }
+ Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
+ std::thread::sleep(Duration::from_millis(2));
+ }
+ Err(_) => break,
+ }
+ }
+ });
+ Self {
+ addr,
+ stop,
+ thread: Some(thread),
+ }
+ }
+
+ fn url(&self) -> String {
+ format!("http://{}/", self.addr)
+ }
+}
+
+impl Drop for TestPageServer {
+ fn drop(&mut self) {
+ self.stop.store(true, Ordering::Release);
+ let _ = TcpStream::connect(self.addr);
+ if let Some(thread) = self.thread.take() {
+ let _ = thread.join();
+ }
+ }
+}
// ── minimal MCP client ────────────────────────────────────────────────────────
@@ -16,6 +87,7 @@ impl McpClient {
fn spawn() -> Self {
let mut child = Command::new(OBSCURA)
.args(["mcp"])
+ .env("OBSCURA_ALLOW_PRIVATE_NETWORK", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
@@ -194,12 +266,13 @@ fn test_notifications_are_silent() {
#[test]
fn test_navigate_and_snapshot() {
+ let server = TestPageServer::spawn();
let mut c = McpClient::spawn();
- let nav = c.tool("browser_navigate", serde_json::json!({"url": "https://example.com"}));
+ let nav = c.tool("browser_navigate", serde_json::json!({"url": server.url()}));
assert!(nav["result"]["isError"].is_null(), "navigate failed: {nav}");
let text = content_text(&nav);
- assert!(text.contains("example.com"), "unexpected nav text: {text}");
+ assert!(text.contains("127.0.0.1"), "unexpected nav text: {text}");
let snap = c.tool("browser_snapshot", serde_json::json!({}));
assert!(snap["result"]["isError"].is_null(), "snapshot failed: {snap}");
@@ -210,8 +283,9 @@ fn test_navigate_and_snapshot() {
#[test]
fn test_evaluate() {
+ let server = TestPageServer::spawn();
let mut c = McpClient::spawn();
- c.tool("browser_navigate", serde_json::json!({"url": "https://example.com"}));
+ c.tool("browser_navigate", serde_json::json!({"url": server.url()}));
let resp = c.tool(
"browser_evaluate",
@@ -223,8 +297,9 @@ fn test_evaluate() {
#[test]
fn test_evaluate_math() {
+ let server = TestPageServer::spawn();
let mut c = McpClient::spawn();
- c.tool("browser_navigate", serde_json::json!({"url": "https://example.com"}));
+ c.tool("browser_navigate", serde_json::json!({"url": server.url()}));
let resp = c.tool(
"browser_evaluate",
@@ -237,8 +312,9 @@ fn test_evaluate_math() {
#[test]
fn test_wait_for_selector() {
+ let server = TestPageServer::spawn();
let mut c = McpClient::spawn();
- c.tool("browser_navigate", serde_json::json!({"url": "https://example.com"}));
+ c.tool("browser_navigate", serde_json::json!({"url": server.url()}));
let resp = c.tool(
"browser_wait_for",
@@ -250,8 +326,9 @@ fn test_wait_for_selector() {
#[test]
fn test_wait_for_timeout() {
+ let server = TestPageServer::spawn();
let mut c = McpClient::spawn();
- c.tool("browser_navigate", serde_json::json!({"url": "https://example.com"}));
+ c.tool("browser_navigate", serde_json::json!({"url": server.url()}));
let resp = c.tool(
"browser_wait_for",
@@ -277,21 +354,23 @@ fn test_unknown_tool_returns_error() {
#[test]
fn test_network_requests() {
+ let server = TestPageServer::spawn();
let mut c = McpClient::spawn();
- c.tool("browser_navigate", serde_json::json!({"url": "https://example.com"}));
+ c.tool("browser_navigate", serde_json::json!({"url": server.url()}));
let resp = c.tool("browser_network_requests", serde_json::json!({}));
let text = content_text(&resp);
assert!(
- text.contains("example.com") || text.contains("No network"),
+ text.contains("127.0.0.1") || text.contains("No network"),
"unexpected: {text}"
);
}
#[test]
fn test_close_resets_state() {
+ let server = TestPageServer::spawn();
let mut c = McpClient::spawn();
- c.tool("browser_navigate", serde_json::json!({"url": "https://example.com"}));
+ c.tool("browser_navigate", serde_json::json!({"url": server.url()}));
let close = c.tool("browser_close", serde_json::json!({}));
assert!(close["result"]["isError"].is_null());