diff --git a/cmux-tui/crates/cmux-tui/src/cli/app_focus.rs b/cmux-tui/crates/cmux-tui/src/cli/app_focus.rs index 5962c688227f..c7e888adcd29 100644 --- a/cmux-tui/crates/cmux-tui/src/cli/app_focus.rs +++ b/cmux-tui/crates/cmux-tui/src/cli/app_focus.rs @@ -129,11 +129,23 @@ impl AppWorkspaces { } } -/// What the app's windows show (`snapshot.get`). +/// What the app's windows show (`snapshot.get`), from the app's last +/// published snapshot. Which workspace a window shows is the app's own +/// state, and no daemon write of this caller changes it (focus goes through +/// the app's focus actions, which answer after the window moved), so this +/// read takes no `after: "sync"` barrier: that barrier waits for a daemon +/// round trip and for the app's main actor to publish past it, and under +/// load it made each `workspace list` wait up to two 2 s app deadlines +/// (CLI-1). `after: 0` answers from any loaded snapshot, off the main actor. pub(super) fn app_workspaces(stream: &mut UnixStream) -> Option { - let reply = super::app::request(stream, "snapshot.get", json!({}), super::app::READ_TIMEOUT) - .ok()? - .ok()?; + let reply = super::app::request( + stream, + "snapshot.get", + json!({ "after": 0 }), + super::app::READ_TIMEOUT, + ) + .ok()? + .ok()?; let shown = reply.pointer("/topology/focus/workspace").and_then(Value::as_str)?.to_owned(); let known = reply .pointer("/topology/workspaces") diff --git a/cmux-tui/crates/cmux-tui/src/cli/app_focus/tests.rs b/cmux-tui/crates/cmux-tui/src/cli/app_focus/tests.rs index c75f92e6da33..499fab772d00 100644 --- a/cmux-tui/crates/cmux-tui/src/cli/app_focus/tests.rs +++ b/cmux-tui/crates/cmux-tui/src/cli/app_focus/tests.rs @@ -173,3 +173,63 @@ fn screen_focus_runs_the_apps_screen_focus() { assert_eq!(received[0]["params"]["action"], "screen.focus"); assert_eq!(received[0]["params"]["target"], "screen_1"); } + +/// An app whose read barrier never settles: a request with `after: "sync"` +/// gets the app's own deadline error (its main actor is busy publishing, +/// TYPING UNDER LOAD L6), any other request the window's snapshot. +fn busy_app(snapshot: Value) -> (PathBuf, std::thread::JoinHandle>) { + let dir = cmux_unix_socket::short_test_dir("cmux-focus-busy"); + let socket = dir.path().join("app.sock"); + let listener = UnixListener::bind(&socket).unwrap(); + let handle = std::thread::spawn(move || { + let (stream, _) = listener.accept().unwrap(); + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut writer = stream; + let mut received = Vec::new(); + let mut line = String::new(); + while reader.read_line(&mut line).unwrap() > 0 { + let request = serde_json::from_str::(&line).unwrap(); + line.clear(); + let response = if request["params"]["after"] == "sync" { + json!({"id": 1, "ok": false, + "error": {"code": "timeout", "message": "snapshot.get timed out after 2 s"}}) + } else { + ok(snapshot.clone()) + }; + received.push(request); + writeln!(writer, "{response}").unwrap(); + } + drop(dir); + received + }); + (socket, handle) +} + +/// CLI-1 (TYPING UNDER LOAD, STATUS 6290): `workspace list` asked the app +/// which workspace its window shows with the read barrier `after: "sync"`, +/// which waits for the app's main actor to publish a snapshot past a daemon +/// round trip. Under load each try hit the app's 2 s deadline and the CLI +/// asked twice: a 4.1 s list. Which workspace a window shows is the app's +/// own state, which no daemon write of the caller changes (focus goes +/// through the app's focus actions), so the overlay reads the published +/// snapshot and never waits for the main actor. +#[test] +fn workspace_list_reads_the_window_without_waiting_for_the_apps_read_barrier() { + let (socket, app) = busy_app(json!({ + "topology": {"focus": {"workspace": "ws_home"}, + "workspaces": [{"id": "ws_home"}, {"id": "ws_new"}]} + })); + let mut stream = UnixStream::connect(&socket).unwrap(); + let mut reply = json!([ + {"id": "ws_home", "name": "Home", "focused": false}, + {"id": "ws_new", "name": "workspace-2", "focused": true} + ]); + after_daemon_with(&mut stream, &follow_for(ResourceOperation::WorkspaceList), &mut reply) + .unwrap(); + drop(stream); + let received = app.join().unwrap(); + assert_eq!(received.len(), 1, "one snapshot read, no retry: {received:?}"); + assert_ne!(received[0]["params"]["after"], "sync", "{received:?}"); + assert_eq!(reply[0]["focused"], true, "{reply}"); + assert_eq!(reply[1]["focused"], false, "{reply}"); +}