chore: seed sgclaw rust baseline
This commit is contained in:
84
tests/browser_tool_test.rs
Normal file
84
tests/browser_tool_test.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
mod common;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use common::MockTransport;
|
||||
use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing};
|
||||
use sgclaw::security::MacPolicy;
|
||||
|
||||
fn test_policy() -> MacPolicy {
|
||||
MacPolicy::from_json_str(
|
||||
r#"{
|
||||
"version": "1.0",
|
||||
"domains": { "allowed": ["oa.example.com", "erp.example.com"] },
|
||||
"pipe_actions": {
|
||||
"allowed": ["click", "type", "navigate", "getText"],
|
||||
"blocked": ["eval", "executeJsInPage"]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_tool_signs_and_sends_command_then_waits_for_response() {
|
||||
let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
|
||||
seq: 1,
|
||||
success: true,
|
||||
data: serde_json::json!({"text": "ok"}),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 20,
|
||||
},
|
||||
}]));
|
||||
let tool = BrowserPipeTool::new(
|
||||
transport.clone(),
|
||||
test_policy(),
|
||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||||
)
|
||||
.with_response_timeout(Duration::from_secs(1));
|
||||
|
||||
let result = tool
|
||||
.invoke(
|
||||
Action::Click,
|
||||
serde_json::json!({ "selector": "#submit" }),
|
||||
"oa.example.com",
|
||||
)
|
||||
.unwrap();
|
||||
let sent = transport.sent_messages();
|
||||
|
||||
assert_eq!(result.seq, 1);
|
||||
assert_eq!(result.data, serde_json::json!({"text": "ok"}));
|
||||
assert_eq!(sent.len(), 1);
|
||||
assert!(matches!(
|
||||
&sent[0],
|
||||
AgentMessage::Command {
|
||||
seq,
|
||||
action,
|
||||
params,
|
||||
security
|
||||
} if *seq == 1
|
||||
&& action == &Action::Click
|
||||
&& params == &serde_json::json!({"selector": "#submit"})
|
||||
&& security.expected_domain == "oa.example.com"
|
||||
&& !security.hmac.is_empty()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_tool_rejects_action_when_mac_policy_blocks_it() {
|
||||
let transport = Arc::new(MockTransport::new(vec![]));
|
||||
let tool = BrowserPipeTool::new(transport, test_policy(), vec![1, 2, 3, 4]);
|
||||
|
||||
let err = tool
|
||||
.invoke(
|
||||
Action::GetHtml,
|
||||
serde_json::json!({ "selector": "body" }),
|
||||
"oa.example.com",
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("action is not allowed"));
|
||||
}
|
||||
38
tests/common/mod.rs
Normal file
38
tests/common/mod.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use sgclaw::pipe::{AgentMessage, BrowserMessage, PipeError, Transport};
|
||||
|
||||
pub struct MockTransport {
|
||||
incoming: Mutex<VecDeque<BrowserMessage>>,
|
||||
sent: Mutex<Vec<AgentMessage>>,
|
||||
}
|
||||
|
||||
impl MockTransport {
|
||||
pub fn new(messages: Vec<BrowserMessage>) -> Self {
|
||||
Self {
|
||||
incoming: Mutex::new(VecDeque::from(messages)),
|
||||
sent: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sent_messages(&self) -> Vec<AgentMessage> {
|
||||
self.sent.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for MockTransport {
|
||||
fn send(&self, message: &AgentMessage) -> Result<(), PipeError> {
|
||||
self.sent.lock().unwrap().push(message.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv_timeout(&self, _timeout: Duration) -> Result<BrowserMessage, PipeError> {
|
||||
self.incoming
|
||||
.lock()
|
||||
.unwrap()
|
||||
.pop_front()
|
||||
.ok_or(PipeError::Timeout)
|
||||
}
|
||||
}
|
||||
2
tests/handshake_flow_test.rs
Normal file
2
tests/handshake_flow_test.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[path = "integration/handshake_flow_test.rs"]
|
||||
mod handshake_flow_test;
|
||||
89
tests/integration/handshake_flow_test.rs
Normal file
89
tests/integration/handshake_flow_test.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::io::{Cursor, Result as IoResult, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use sgclaw::pipe::{
|
||||
perform_handshake, Action, AgentMessage, BrowserPipeTool, StdioTransport, Timing,
|
||||
};
|
||||
use sgclaw::security::MacPolicy;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct SharedBuffer {
|
||||
inner: Arc<Mutex<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl SharedBuffer {
|
||||
fn snapshot(&self) -> String {
|
||||
String::from_utf8(self.inner.lock().unwrap().clone()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for SharedBuffer {
|
||||
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
|
||||
self.inner.lock().unwrap().extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> IoResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_and_command_flow_work_over_json_line_transport() {
|
||||
let reader = Cursor::new(
|
||||
concat!(
|
||||
r#"{"type":"init","version":"1.0","hmac_seed":"0123456789abcdef","capabilities":["browser_action"]}"#,
|
||||
"\n",
|
||||
r#"{"type":"response","seq":1,"success":true,"data":{"text":"提交成功"},"aom_snapshot":[],"timing":{"queue_ms":2,"exec_ms":38}}"#,
|
||||
"\n"
|
||||
)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
);
|
||||
let writer = SharedBuffer::default();
|
||||
let captured = writer.clone();
|
||||
let transport = Arc::new(StdioTransport::new(reader, writer));
|
||||
|
||||
let handshake = perform_handshake(transport.as_ref(), Duration::from_secs(1)).unwrap();
|
||||
let policy = MacPolicy::from_json_str(
|
||||
r#"{
|
||||
"version": "1.0",
|
||||
"domains": { "allowed": ["oa.example.com"] },
|
||||
"pipe_actions": {
|
||||
"allowed": ["click", "type", "navigate", "getText"],
|
||||
"blocked": []
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let tool = BrowserPipeTool::new(transport, policy, handshake.session_key)
|
||||
.with_response_timeout(Duration::from_secs(1));
|
||||
|
||||
let result = tool
|
||||
.invoke(
|
||||
Action::Click,
|
||||
serde_json::json!({ "selector": "#submit" }),
|
||||
"oa.example.com",
|
||||
)
|
||||
.unwrap();
|
||||
let written = captured.snapshot();
|
||||
|
||||
assert_eq!(
|
||||
result.timing,
|
||||
Timing {
|
||||
queue_ms: 2,
|
||||
exec_ms: 38
|
||||
}
|
||||
);
|
||||
assert!(written.contains(r#""type":"init_ack""#));
|
||||
assert!(written.contains(r#""type":"command""#));
|
||||
assert!(written.contains(r#""action":"click""#));
|
||||
|
||||
let lines: Vec<&str> = written.lines().collect();
|
||||
let command: AgentMessage = serde_json::from_str(lines[1]).unwrap();
|
||||
assert!(matches!(
|
||||
command,
|
||||
AgentMessage::Command { seq, .. } if seq == 1
|
||||
));
|
||||
}
|
||||
41
tests/pipe_handshake_test.rs
Normal file
41
tests/pipe_handshake_test.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
mod common;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use common::MockTransport;
|
||||
use sgclaw::pipe::{perform_handshake, AgentMessage, BrowserMessage};
|
||||
|
||||
#[test]
|
||||
fn handshake_reads_init_and_writes_init_ack() {
|
||||
let transport = MockTransport::new(vec![BrowserMessage::Init {
|
||||
version: "1.0".to_string(),
|
||||
hmac_seed: "0123456789abcdef".to_string(),
|
||||
capabilities: vec!["browser_action".to_string()],
|
||||
}]);
|
||||
|
||||
let result = perform_handshake(&transport, Duration::from_secs(5)).unwrap();
|
||||
let sent = transport.sent_messages();
|
||||
|
||||
assert_eq!(result.capabilities, vec!["browser_action"]);
|
||||
assert_eq!(sent.len(), 1);
|
||||
assert!(matches!(
|
||||
&sent[0],
|
||||
AgentMessage::InitAck {
|
||||
version,
|
||||
agent_id,
|
||||
supported_actions
|
||||
} if version == "1.0" && !agent_id.is_empty() && supported_actions.len() >= 4
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_rejects_version_mismatch() {
|
||||
let transport = MockTransport::new(vec![BrowserMessage::Init {
|
||||
version: "9.9".to_string(),
|
||||
hmac_seed: "0123456789abcdef".to_string(),
|
||||
capabilities: vec![],
|
||||
}]);
|
||||
|
||||
let err = perform_handshake(&transport, Duration::from_secs(5)).unwrap_err();
|
||||
assert!(err.to_string().contains("unsupported protocol version"));
|
||||
}
|
||||
59
tests/pipe_protocol_test.rs
Normal file
59
tests/pipe_protocol_test.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, SecurityFields, Timing};
|
||||
|
||||
#[test]
|
||||
fn browser_init_round_trip_uses_frozen_wire_format() {
|
||||
let raw = r#"{"type":"init","version":"1.0","hmac_seed":"0123456789abcdef","capabilities":["browser_action"]}"#;
|
||||
let message: BrowserMessage = serde_json::from_str(raw).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
message,
|
||||
BrowserMessage::Init {
|
||||
ref version,
|
||||
ref hmac_seed,
|
||||
ref capabilities
|
||||
} if version == "1.0"
|
||||
&& hmac_seed == "0123456789abcdef"
|
||||
&& *capabilities == vec!["browser_action".to_string()]
|
||||
));
|
||||
|
||||
assert_eq!(serde_json::to_string(&message).unwrap(), raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_serializes_action_and_security_fields() {
|
||||
let message = AgentMessage::Command {
|
||||
seq: 1,
|
||||
action: Action::GetText,
|
||||
params: serde_json::json!({ "selector": "#submit" }),
|
||||
security: SecurityFields {
|
||||
expected_domain: "oa.example.com".to_string(),
|
||||
hmac: "abc123".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let raw = serde_json::to_string(&message).unwrap();
|
||||
|
||||
assert!(raw.contains(r#""type":"command""#));
|
||||
assert!(raw.contains(r#""action":"getText""#));
|
||||
assert!(raw.contains(r#""expected_domain":"oa.example.com""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_deserializes_timing_and_payload() {
|
||||
let raw = r#"{"type":"response","seq":7,"success":true,"data":{"text":"提交成功"},"aom_snapshot":[],"timing":{"queue_ms":2,"exec_ms":38}}"#;
|
||||
let message: BrowserMessage = serde_json::from_str(raw).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
message,
|
||||
BrowserMessage::Response {
|
||||
seq: 7,
|
||||
success: true,
|
||||
data: serde_json::json!({"text": "提交成功"}),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 2,
|
||||
exec_ms: 38,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user