[packages/cli-v2]fix: misc fixes from cookies, logs, wait, and CDP session

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
mcfn.feng
2026-03-30 11:35:46 +08:00
parent b2da35b417
commit 7a1cc2207a
12 changed files with 144 additions and 31 deletions
+1
View File
@@ -119,3 +119,4 @@ store-assets/
# Agent prompt files
.agent-prompt-*.md
.gstack/
+5 -1
View File
@@ -107,7 +107,11 @@ pub async fn execute(cmd: &Cmd, registry: &SharedRegistry) -> ActionResult {
let domain = cookie.get("domain").and_then(|v| v.as_str()).unwrap_or("");
let path = cookie.get("path").and_then(|v| v.as_str()).unwrap_or("/");
let params = json!({ "name": name, "domain": domain, "path": path });
if cdp.execute_on_tab(&target_id, "Network.deleteCookies", params).await.is_ok() {
if cdp
.execute_on_tab(&target_id, "Network.deleteCookies", params)
.await
.is_ok()
{
cleared += 1;
}
}
@@ -96,7 +96,11 @@ pub async fn execute(cmd: &Cmd, registry: &SharedRegistry) -> ActionResult {
let domain = cookie.get("domain").and_then(|v| v.as_str()).unwrap_or("");
let path = cookie.get("path").and_then(|v| v.as_str()).unwrap_or("/");
let params = json!({ "name": cmd.name, "domain": domain, "path": path });
if cdp.execute_on_tab(&target_id, "Network.deleteCookies", params).await.is_ok() {
if cdp
.execute_on_tab(&target_id, "Network.deleteCookies", params)
.await
.is_ok()
{
deleted += 1;
}
}
+4 -1
View File
@@ -147,7 +147,10 @@ pub async fn execute(cmd: &Cmd, registry: &SharedRegistry) -> ActionResult {
params["expires"] = json!(exp);
}
match cdp.execute_on_tab(&target_id, "Network.setCookie", params).await {
match cdp
.execute_on_tab(&target_id, "Network.setCookie", params)
.await
{
Ok(_) => {}
Err(e) => return ActionResult::fatal("CDP_ERROR", e.to_string()),
};
@@ -161,8 +161,7 @@ pub async fn execute(cmd: &Cmd, registry: &SharedRegistry) -> ActionResult {
// Build since filter JS (filter items with id seq > since id seq)
let since_filter = match &cmd.since {
Some(id) => {
let id_json =
serde_json::to_string(id).unwrap_or_else(|_| format!("\"{}\"", id));
let id_json = serde_json::to_string(id).unwrap_or_else(|_| format!("\"{}\"", id));
format!(
".filter(function(l) {{ return parseInt((l.id||'').split('-')[1]||'0') > parseInt(({id_json}).split('-')[1]||'0'); }})"
)
@@ -93,11 +93,8 @@ pub async fn execute(cmd: &Cmd, registry: &SharedRegistry) -> ActionResult {
// Build source filter JS
let source_filter = match &cmd.source {
Some(src) => {
let src_json =
serde_json::to_string(src).unwrap_or_else(|_| format!("\"{}\"", src));
format!(
".filter(function(e) {{ return e.source === {src_json}; }})"
)
let src_json = serde_json::to_string(src).unwrap_or_else(|_| format!("\"{}\"", src));
format!(".filter(function(e) {{ return e.source === {src_json}; }})")
}
None => String::new(),
};
@@ -105,8 +102,7 @@ pub async fn execute(cmd: &Cmd, registry: &SharedRegistry) -> ActionResult {
// Build since filter JS (filter items with id seq > since id seq)
let since_filter = match &cmd.since {
Some(id) => {
let id_json =
serde_json::to_string(id).unwrap_or_else(|_| format!("\"{}\"", id));
let id_json = serde_json::to_string(id).unwrap_or_else(|_| format!("\"{}\"", id));
format!(
".filter(function(e) {{ return parseInt((e.id||'').split('-')[1]||'0') > parseInt(({id_json}).split('-')[1]||'0'); }})"
)
@@ -138,6 +138,7 @@ pub async fn execute(cmd: &Cmd, registry: &SharedRegistry) -> ActionResult {
let mut reg = registry.lock().await;
if cdp_endpoint.is_none()
&& cmd.set_session_id.is_none()
&& mode == Mode::Local
&& let Some(existing) = reg.find_local_session_by_profile(profile_name, mode)
{
@@ -94,9 +94,7 @@ pub async fn execute(cmd: &Cmd, registry: &SharedRegistry) -> ActionResult {
.map(|rv| match rv {
serde_json::Value::Bool(b) => *b,
serde_json::Value::Null => false,
serde_json::Value::Number(n) => {
n.as_f64().map(|f| f != 0.0).unwrap_or(false)
}
serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
serde_json::Value::String(s) => !s.is_empty(),
// Arrays and objects are always truthy in JS, even when empty.
serde_json::Value::Array(_) | serde_json::Value::Object(_) => true,
@@ -99,10 +99,7 @@ pub async fn execute(cmd: &Cmd, registry: &SharedRegistry) -> ActionResult {
let result_val = v.pointer("/result/result/value");
if let Some(rv) = result_val {
let current_url = rv.get("url").and_then(|v| v.as_str()).unwrap_or("");
let ready_state = rv
.get("ready_state")
.and_then(|v| v.as_str())
.unwrap_or("");
let ready_state = rv.get("ready_state").and_then(|v| v.as_str()).unwrap_or("");
let url_changed = current_url != initial_url.as_str();
+10 -6
View File
@@ -230,7 +230,12 @@ impl CdpSession {
) -> mpsc::Receiver<Value> {
let key = format!("{cdp_session_id}:{method}");
let (tx, rx) = mpsc::channel(256);
self.event_subs.lock().await.entry(key).or_default().push(tx);
self.event_subs
.lock()
.await
.entry(key)
.or_default()
.push(tx);
rx
}
@@ -826,11 +831,10 @@ mod tests {
)
.await;
let event =
tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
.await
.expect("timed out waiting for event")
.expect("channel closed");
let event = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
.await
.expect("timed out waiting for event")
.expect("channel closed");
assert_eq!(event["method"], "Network.requestWillBeSent");
assert_eq!(event["params"]["requestId"], "req-42");
+2 -6
View File
@@ -41,9 +41,7 @@ pub async fn route(action: &Action, registry: &SharedRegistry) -> ActionResult {
Action::LogsConsole(cmd) => {
browser::observation::logs_console::execute(cmd, registry).await
}
Action::LogsErrors(cmd) => {
browser::observation::logs_errors::execute(cmd, registry).await
}
Action::LogsErrors(cmd) => browser::observation::logs_errors::execute(cmd, registry).await,
Action::CookiesList(cmd) => browser::cookies::list::execute(cmd, registry).await,
Action::CookiesGet(cmd) => browser::cookies::get::execute(cmd, registry).await,
Action::CookiesSet(cmd) => browser::cookies::set::execute(cmd, registry).await,
@@ -51,9 +49,7 @@ pub async fn route(action: &Action, registry: &SharedRegistry) -> ActionResult {
Action::CookiesClear(cmd) => browser::cookies::clear::execute(cmd, registry).await,
Action::WaitElement(cmd) => browser::wait::element::execute(cmd, registry).await,
Action::WaitNavigation(cmd) => browser::wait::navigation::execute(cmd, registry).await,
Action::WaitNetworkIdle(cmd) => {
browser::wait::network_idle::execute(cmd, registry).await
}
Action::WaitNetworkIdle(cmd) => browser::wait::network_idle::execute(cmd, registry).await,
Action::WaitCondition(cmd) => browser::wait::condition::execute(cmd, registry).await,
Action::Eval(cmd) => browser::interaction::eval::execute(cmd, registry).await,
Action::Click(cmd) => browser::interaction::click::execute(cmd, registry).await,
@@ -980,3 +980,113 @@ fn lifecycle_start_concurrent_same_profile_rejects_second_json() {
env.headless(&["browser", "close", "--session", &session_id], 30);
}
// ===========================================================================
// set-session-id must not silently reuse an existing session
// ===========================================================================
/// When a session with profile P already exists and the user requests
/// `--set-session-id NEW_ID --profile P`, the command must NOT silently
/// reuse the existing session. It should fail with SESSION_ALREADY_EXISTS
/// because profile P is already occupied.
#[test]
fn lifecycle_set_session_id_rejects_reuse_of_occupied_profile() {
if skip() {
return;
}
let (sid1, prof1) = unique_session("reuse");
let (sid2, _) = unique_session("reuse2");
// Start first session with profile
let out = headless_json(
&[
"browser",
"start",
"--mode",
"local",
"--headless",
"--profile",
&prof1,
"--set-session-id",
&sid1,
],
30,
);
assert_success(&out, "start first session");
let _guard = SessionGuard::new(&sid1);
// Try to start second session with SAME profile but DIFFERENT session ID.
// Must NOT silently return the first session.
let out = headless_json(
&[
"browser",
"start",
"--mode",
"local",
"--headless",
"--profile",
&prof1,
"--set-session-id",
&sid2,
],
30,
);
assert_failure(&out, "second start with set-session-id must fail");
let v = parse_json(&out);
assert_eq!(v["ok"], false);
assert_eq!(
v["error"]["code"], "SESSION_ALREADY_EXISTS",
"must return SESSION_ALREADY_EXISTS, not silently reuse"
);
}
/// When --set-session-id matches an already-running session's ID,
/// the command must fail with SESSION_ALREADY_EXISTS, not silently reuse.
#[test]
fn lifecycle_set_session_id_rejects_duplicate_id() {
if skip() {
return;
}
let (sid, prof) = unique_session("dupid");
let out = headless_json(
&[
"browser",
"start",
"--mode",
"local",
"--headless",
"--profile",
&prof,
"--set-session-id",
&sid,
],
30,
);
assert_success(&out, "start session");
let _guard = SessionGuard::new(&sid);
// Try to start again with the SAME session ID (different profile).
let (_, prof2) = unique_session("dupid2");
let out = headless_json(
&[
"browser",
"start",
"--mode",
"local",
"--headless",
"--profile",
&prof2,
"--set-session-id",
&sid,
],
30,
);
assert_failure(&out, "duplicate set-session-id must fail");
let v = parse_json(&out);
assert_eq!(v["ok"], false);
assert_eq!(
v["error"]["code"], "SESSION_ALREADY_EXISTS",
"must return SESSION_ALREADY_EXISTS for duplicate ID"
);
}