#![allow(clippy::zombie_processes)]
use std::time::Duration;
use flow_wm::ipc::message::{SocketMessage, SocketResponse};
use windows::Win32::Foundation::{HWND, RECT};
use windows::Win32::UI::WindowsAndMessaging::GetWindowRect;
use super::common::unique_pipe_name;
use super::test_desktop::{
DaemonGuard, TestDesktop, TestWindow, active_window_ids, query_layout_virtual, send_ipc_retry,
start_test_daemon, unique_title, wait_until_windows_tiled,
};
const HOOK_SETTLE: Duration = Duration::from_millis(1500);
const WORKSPACE_SETTLE: Duration = Duration::from_millis(500);
fn active_window_count(json: &serde_json::Value) -> i64 {
json["window_count"].as_i64().unwrap_or(0)
}
fn hwnd_id(hwnd: windows::Win32::Foundation::HWND) -> i64 {
hwnd.0 as i64
}
fn win32_window_rect(hwnd: HWND) -> (i32, i32, i32, i32) {
let mut rect = RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
unsafe { GetWindowRect(hwnd, &mut rect) }
.expect("GetWindowRect must succeed for a live test window");
(rect.left, rect.top, rect.right, rect.bottom)
}
#[ignore = "non-deterministic on isolated test desktop: daemon hooks may not register/tile windows before the assertion (startup hook race)"]
#[test]
fn move_to_active_workspace_is_noop() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let title = unique_title("MoveToSelf");
let _w = TestWindow::create(&title).expect("create window");
let before = wait_until_windows_tiled(&pipe, 1).expect("window tiled before move");
assert_eq!(
active_window_count(&before),
1,
"expected exactly 1 window on workspace 1 before move, got {before:?}",
);
let ids_before = active_window_ids(&before);
let moved_id = ids_before[0];
let resp = send_ipc_retry(
&pipe,
&SocketMessage::MoveWindowToWorkspace { workspace_id: 1 },
)
.expect("IPC send: move-to-self");
assert_eq!(
resp,
SocketResponse::Ok,
"move-to-self must succeed without mutating any state",
);
std::thread::sleep(WORKSPACE_SETTLE);
let after = query_layout_virtual(&pipe).expect("query layout after");
assert_eq!(
before["columns"], after["columns"],
"move-to-self must not mutate the layout",
);
assert_eq!(
active_window_count(&after),
1,
"move-to-self must not change the window count, got {after:?}",
);
assert_eq!(
active_window_ids(&after),
vec![moved_id],
"move-to-self must keep the same window id in place",
);
}
#[test]
fn move_to_other_workspace_switches_active() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let title = unique_title("MoveToOther");
let w = TestWindow::create(&title).expect("create window");
let moved_id = hwnd_id(w.hwnd);
std::thread::sleep(HOOK_SETTLE);
let initial = query_layout_virtual(&pipe).expect("query layout initial");
assert_eq!(
active_window_count(&initial),
1,
"expected exactly 1 window on workspace 1 initially, got {initial:?}",
);
assert!(
active_window_ids(&initial).contains(&moved_id),
"expected the created window ({moved_id}) on the initial active workspace, got {:?}",
active_window_ids(&initial),
);
let resp = send_ipc_retry(
&pipe,
&SocketMessage::MoveWindowToWorkspace { workspace_id: 2 },
)
.expect("IPC send: move to ws 2");
assert_eq!(
resp,
SocketResponse::Ok,
"move to a valid workspace must succeed",
);
std::thread::sleep(WORKSPACE_SETTLE);
let after = query_layout_virtual(&pipe).expect("query layout after move");
assert_eq!(
active_window_count(&after),
1,
"expected exactly 1 window on the destination workspace, got {after:?}",
);
assert!(
active_window_ids(&after).contains(&moved_id),
"expected the moved window ({moved_id}) on the now-active workspace 2, got {:?}",
active_window_ids(&after),
);
let resp = send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 1 })
.expect("IPC send: switch back to ws 1");
assert_eq!(
resp,
SocketResponse::Ok,
"switch to a valid workspace must succeed",
);
std::thread::sleep(WORKSPACE_SETTLE);
let ws1 = query_layout_virtual(&pipe).expect("query workspace 1 after move");
assert_eq!(
active_window_count(&ws1),
0,
"source workspace 1 must be empty after the move, got {ws1:?}",
);
assert!(
!active_window_ids(&ws1).contains(&moved_id),
"source workspace 1 must not contain the moved window",
);
}
#[ignore = "non-deterministic on isolated test desktop: daemon hooks may not register/tile windows before the assertion (startup hook race)"]
#[test]
fn move_to_higher_workspace_switches_active() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let title = unique_title("MoveToFive");
let w = TestWindow::create(&title).expect("create window");
let moved_id = hwnd_id(w.hwnd);
std::thread::sleep(HOOK_SETTLE);
let resp = send_ipc_retry(
&pipe,
&SocketMessage::MoveWindowToWorkspace { workspace_id: 5 },
)
.expect("IPC send: move to ws 5");
assert_eq!(
resp,
SocketResponse::Ok,
"move to a valid workspace must succeed",
);
std::thread::sleep(WORKSPACE_SETTLE);
let after = query_layout_virtual(&pipe).expect("query layout after move to 5");
assert_eq!(
active_window_count(&after),
1,
"expected exactly 1 window on workspace 5 after the move, got {after:?}",
);
assert!(
active_window_ids(&after).contains(&moved_id),
"expected the moved window ({moved_id}) on workspace 5, got {:?}",
active_window_ids(&after),
);
send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 1 })
.expect("IPC send: switch back to ws 1");
std::thread::sleep(WORKSPACE_SETTLE);
let ws1 = query_layout_virtual(&pipe).expect("query workspace 1 after move to 5");
assert_eq!(
active_window_count(&ws1),
0,
"source workspace 1 must be empty after moving to workspace 5, got {ws1:?}",
);
}
#[ignore = "non-deterministic on isolated test desktop: daemon hooks may not register/tile windows before the assertion (startup hook race)"]
#[test]
fn move_to_unknown_workspace_returns_error() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let title = unique_title("MoveUnknown");
let _w = TestWindow::create(&title).expect("create window");
std::thread::sleep(HOOK_SETTLE);
let before = query_layout_virtual(&pipe).expect("query layout before");
assert_eq!(
active_window_count(&before),
1,
"expected exactly 1 window on workspace 1 before move, got {before:?}",
);
super::common::flow(&pipe)
.args(["dispatch", "move-to-workspace", "99"])
.assert()
.failure();
std::thread::sleep(WORKSPACE_SETTLE);
let after = query_layout_virtual(&pipe).expect("query layout after");
assert_eq!(
before["columns"], after["columns"],
"move to an unknown workspace must not mutate the layout",
);
assert_eq!(
active_window_count(&after),
1,
"move to an unknown workspace must not change the window count, got {after:?}",
);
}
#[test]
fn switch_to_self_is_noop() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let title = unique_title("SwitchSelf");
let _w = TestWindow::create(&title).expect("create window");
std::thread::sleep(HOOK_SETTLE);
let before = query_layout_virtual(&pipe).expect("query layout before");
assert_eq!(
active_window_count(&before),
1,
"expected exactly 1 window on workspace 1 before switch, got {before:?}",
);
let resp = send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 1 })
.expect("IPC send: switch-to-self");
assert_eq!(
resp,
SocketResponse::Ok,
"switch-to-self must succeed without mutating any state",
);
std::thread::sleep(WORKSPACE_SETTLE);
let after = query_layout_virtual(&pipe).expect("query layout after");
assert_eq!(
before["columns"], after["columns"],
"switch-to-self must not mutate the layout",
);
assert_eq!(
active_window_count(&after),
1,
"switch-to-self must not change the window count, got {after:?}",
);
}
const PARKED_TOLERANCE_PX: i32 = 20;
const RAPID_SWITCH_GAP: Duration = Duration::from_millis(50);
const POST_SWITCH_SETTLE: Duration = Duration::from_millis(700);
#[test]
#[ignore = "isolated desktop makes SetWindowPos an observable no-op (see comment)"]
fn rapid_switch_workspace_does_not_strand_bystander() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let title_a = unique_title("StrandA");
let w_a = TestWindow::create(&title_a).expect("create window A");
std::thread::sleep(HOOK_SETTLE);
let resp = send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 2 })
.expect("IPC: slow switch 1→2");
assert_eq!(
resp,
SocketResponse::Ok,
"slow switch 1→2 must succeed so we can capture A's parked rect",
);
std::thread::sleep(WORKSPACE_SETTLE);
let parked_rect = win32_window_rect(w_a.hwnd);
assert!(
parked_rect.3 <= 0,
"pre-condition: A's bottom edge must be at or above the screen top after \
slow parking, got {:?}; if bottom > 0 the test daemon is likely running \
with animation disabled (the stranding bug requires animation enabled)",
parked_rect,
);
let resp = send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 1 })
.expect("IPC: slow switch 2→1");
assert_eq!(
resp,
SocketResponse::Ok,
"slow switch 2→1 must succeed so A returns on screen",
);
std::thread::sleep(WORKSPACE_SETTLE);
let resp = send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 3 })
.expect("IPC: slow switch 1→3 (to populate ws3)");
assert_eq!(
resp,
SocketResponse::Ok,
"slow switch 1→3 must succeed so window C can be created on ws3",
);
std::thread::sleep(WORKSPACE_SETTLE);
let title_c = unique_title("StrandC");
let _w_c = TestWindow::create(&title_c).expect("create window C on ws3");
std::thread::sleep(HOOK_SETTLE);
let resp = send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 1 })
.expect("IPC: slow switch 3→1");
assert_eq!(
resp,
SocketResponse::Ok,
"slow switch 3→1 must succeed so A is back on screen for the bug scenario",
);
std::thread::sleep(WORKSPACE_SETTLE);
let onscreen_rect = win32_window_rect(w_a.hwnd);
assert!(
onscreen_rect.3 > 0,
"pre-condition: A must be fully on screen at ws1 before the rapid \
switch, got {onscreen_rect:?}",
);
let resp = send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 2 })
.expect("IPC: rapid switch 1→2");
assert_eq!(
resp,
SocketResponse::Ok,
"rapid switch 1→2 must succeed (it kicks off A's parking animation)",
);
std::thread::sleep(RAPID_SWITCH_GAP);
let resp = send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 3 })
.expect("IPC: rapid switch 2→3");
assert_eq!(
resp,
SocketResponse::Ok,
"rapid switch 2→3 must succeed (it interrupts A's animation mid-flight)",
);
std::thread::sleep(POST_SWITCH_SETTLE);
let after_rect = win32_window_rect(w_a.hwnd);
let top_diff = (after_rect.1 - parked_rect.1).abs();
assert!(
top_diff <= PARKED_TOLERANCE_PX,
"regression: ws1's window is stranded after rapid 1→2→3 switch. \
Expected parked top={parked_top} (±{tol}px); got top={after_top} \
(off by {top_diff}px). Pre-fix this reproduces the user's \
'window remains partially visible in the work area' bug. \
after_rect={after_rect:?}, parked_rect={parked_rect:?}",
parked_top = parked_rect.1,
after_top = after_rect.1,
tol = PARKED_TOLERANCE_PX,
);
}
#[test]
#[ignore = "isolated desktop makes SetWindowPos an observable no-op (passes vacuously; see comment)"]
fn slow_switch_workspace_settles_bystander() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let title_a = unique_title("SlowStrandA");
let w_a = TestWindow::create(&title_a).expect("create window A");
std::thread::sleep(HOOK_SETTLE);
send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 2 })
.expect("IPC: slow switch 1→2 (capture parked rect)");
std::thread::sleep(WORKSPACE_SETTLE);
let parked_rect = win32_window_rect(w_a.hwnd);
send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 1 })
.expect("IPC: slow switch 2→1");
std::thread::sleep(WORKSPACE_SETTLE);
send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 3 })
.expect("IPC: slow switch 1→3");
std::thread::sleep(WORKSPACE_SETTLE);
let title_c = unique_title("SlowStrandC");
let _w_c = TestWindow::create(&title_c).expect("create window C on ws3");
std::thread::sleep(HOOK_SETTLE);
send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 1 })
.expect("IPC: slow switch 3→1");
std::thread::sleep(WORKSPACE_SETTLE);
send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 2 })
.expect("IPC: slow switch 1→2");
std::thread::sleep(WORKSPACE_SETTLE);
send_ipc_retry(&pipe, &SocketMessage::SwitchWorkspace { workspace_id: 3 })
.expect("IPC: slow switch 2→3");
std::thread::sleep(WORKSPACE_SETTLE);
let after_rect = win32_window_rect(w_a.hwnd);
let top_diff = (after_rect.1 - parked_rect.1).abs();
assert!(
top_diff <= PARKED_TOLERANCE_PX,
"slow 1→2→3 must park A correctly even without mid-flight interrupt. \
Expected parked top={parked_top} (±{tol}px); got top={after_top} \
(off by {top_diff}px). after_rect={after_rect:?}, \
parked_rect={parked_rect:?}",
parked_top = parked_rect.1,
after_top = after_rect.1,
tol = PARKED_TOLERANCE_PX,
);
}