use super::harness::*; use serde_json::{json, Value}; struct Capture { output: std::process::Output, requests: Vec, } fn capture(args: &[&str], response: Value) -> Capture { let base = unique_test_dir(); fs::create_dir_all(&base).unwrap(); let socket = base.join("contract.sock"); let listener = UnixListener::bind(&socket).unwrap(); listener.set_nonblocking(true).unwrap(); let mut child = Command::new(env!("CARGO_BIN_EXE_herdr")) .args(args) .env("HERDR_SOCKET_PATH", &socket) .env("XDG_CONFIG_HOME", &base) .env("XDG_RUNTIME_DIR", &base) .env_remove("HERDR_CLIENT_SOCKET_PATH") .env_remove("HERDR_ENV") .env_remove("HERDR_PANE_ID") .env_remove("HERDR_SESSION") .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let deadline = Instant::now() + Duration::from_secs(10); let mut requests = Vec::new(); loop { if Instant::now() >= deadline { child.kill().unwrap(); let output = child.wait_with_output().unwrap(); drop(listener); cleanup_test_base(&base); panic!("CLI timed out: {args:?}; output: {output:?}; requests: {requests:?}"); } match listener.accept() { Ok((mut stream, _)) => { // BSD-derived platforms inherit the listener's non-blocking flag. stream.set_nonblocking(false).unwrap(); stream .set_read_timeout(Some(Duration::from_secs(1))) .unwrap(); stream .set_write_timeout(Some(Duration::from_secs(1))) .unwrap(); let mut line = String::new(); BufReader::new(stream.try_clone().unwrap()) .read_line(&mut line) .unwrap(); let request: Value = serde_json::from_str(&line).unwrap(); if request["method"] == "ping" { write_fake_pong(&mut stream, &request, "contract-baseline", CURRENT_PROTOCOL); } else { writeln!(stream, "{response}").unwrap(); stream.flush().unwrap(); } requests.push(request); } Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { if child.try_wait().unwrap().is_some() { break; } thread::sleep(Duration::from_millis(5)); } Err(error) => panic!("contract listener failed: {error}"), } } let output = child.wait_with_output().unwrap(); drop(listener); cleanup_test_base(&base); Capture { output, requests } } fn assert_request(args: &[&str], expected: Value) { let response = json!({"id": expected["id"], "result": {"type": "ok"}}); let captured = capture(args, response.clone()); assert_eq!( captured.output.status.code(), Some(0), "{args:?}: {:?}", captured.output ); assert!( captured.output.stderr.is_empty(), "{args:?}: {:?}", captured.output ); assert_eq!( captured.requests.len(), 2, "{args:?}: {:?}", captured.requests ); assert_eq!(captured.requests[0]["method"], "ping", "{args:?}"); assert_eq!(captured.requests[1], expected, "{args:?}"); assert_eq!( captured.output.stdout, format!("{response}\n").as_bytes(), "{args:?}" ); } #[test] fn workspace_cli_request_contract_matrix() { let cases: Vec<(Vec<&str>, Value)> = vec![ ( vec!["workspace", "list"], json!({"id":"cli:workspace:list","method":"workspace.list","params":{}}), ), ( vec!["workspace", "get", "opaque:w-7:p.2"], json!({"id":"cli:workspace:get","method":"workspace.get","params":{"workspace_id":"opaque:w-7:p.2"}}), ), ( vec!["workspace", "focus", "007"], json!({"id":"cli:workspace:focus","method":"workspace.focus","params":{"workspace_id":"007"}}), ), ( vec!["workspace", "rename", "w:7", "two", "words", "--literal"], json!({"id":"cli:workspace:rename","method":"workspace.rename","params":{"workspace_id":"w:7","label":"two words --literal"}}), ), ( vec!["workspace", "rename", "w:7", ""], json!({"id":"cli:workspace:rename","method":"workspace.rename","params":{"workspace_id":"w:7","label":""}}), ), ( vec!["workspace", "create"], json!({"id":"cli:workspace:create","method":"workspace.create","params":{"focus":false}}), ), ( vec!["workspace", "create", "--focus", "--no-focus", "--focus"], json!({"id":"cli:workspace:create","method":"workspace.create","params":{"focus":true}}), ), ( vec!["workspace", "create", "--no-focus", "--focus", "--no-focus"], json!({"id":"cli:workspace:create","method":"workspace.create","params":{"focus":false}}), ), ( vec![ "workspace", "create", "--cwd", "relative/first", "--cwd", "../last", "--label", "first", "--label", "--dash-value", "--env", "KEY=first", "--env", "KEY=last=value", "--env", "EMPTY=", ], json!({"id":"cli:workspace:create","method":"workspace.create","params":{"cwd":"../last","label":"--dash-value","focus":false,"env":{"KEY":"last=value","EMPTY":""}}}), ), ( vec!["workspace", "close", "w:7"], json!({"id":"cli:workspace:close","method":"workspace.close","params":{"workspace_id":"w:7"}}), ), ( vec!["workspace", "close", "w:7", "--group"], json!({"id":"cli:workspace:close","method":"workspace.close","params":{"workspace_id":"w:7","close_group":true}}), ), ( vec!["workspace", "get", "--literal"], json!({"id":"cli:workspace:get","method":"workspace.get","params":{"workspace_id":"--literal"}}), ), ]; for (args, request) in cases { assert_request(&args, request); } } #[test] fn workspace_metadata_presence_and_order_contract() { let cases: Vec<(Vec<&str>, Value)> = vec![ ( vec![ "workspace", "report-metadata", "w:7", "--source", "first", "--source", "user:final", "--token", "set=a=b", "--token", "empty=", "--clear-token", "cleared", "--seq", "1", "--seq", "0", "--ttl-ms", "5", "--ttl-ms", "0", ], json!({"id":"cli:request","method":"workspace.report_metadata","params":{"workspace_id":"w:7","source":"user:final","tokens":{"set":"a=b","empty":"","cleared":null},"seq":0,"ttl_ms":0}}), ), ( vec![ "workspace", "report-metadata", "w:7", "--source", "user", "--token", "key=first", "--clear-token", "key", ], json!({"id":"cli:request","method":"workspace.report_metadata","params":{"workspace_id":"w:7","source":"user","tokens":{"key":null}}}), ), ( vec![ "workspace", "report-metadata", "w:7", "--source", "user", "--clear-token", "key", "--token", "key=", ], json!({"id":"cli:request","method":"workspace.report_metadata","params":{"workspace_id":"w:7","source":"user","tokens":{"key":""}}}), ), ]; for (args, expected) in cases { let captured = capture(&args, json!({"id":"cli:request","result":{"type":"ok"}})); assert_eq!( captured.output.status.code(), Some(0), "{args:?}: {:?}", captured.output ); assert!(captured.output.stdout.is_empty(), "{args:?}"); assert!(captured.output.stderr.is_empty(), "{args:?}"); assert_eq!(captured.requests.len(), 2, "{args:?}"); assert_eq!(captured.requests[0]["method"], "ping"); assert_eq!(captured.requests[1], expected, "{args:?}"); } } #[test] fn workspace_legacy_rejections_do_not_connect() { let cases: Vec<(Vec<&str>, &str)> = vec![ ( vec!["workspace", "list", "extra"], "error: unexpected argument 'extra' found", ), (vec!["workspace", "get"], ""), ( vec!["workspace", "get", "w1", "extra"], "error: unexpected argument 'extra' found", ), (vec!["workspace", "focus"], ""), (vec!["workspace", "rename", "w1"], "