mirror of
https://github.com/actionbook/actionbook.git
synced 2026-09-19 03:28:06 +08:00
[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:
@@ -119,3 +119,4 @@ store-assets/
|
||||
# Agent prompt files
|
||||
.agent-prompt-*.md
|
||||
|
||||
.gstack/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user