hive-agent: test the todo socket server's error replies, bind and a full socket round trip
This commit is contained in:
parent
df02ff89aa
commit
03b9c39498
1 changed files with 160 additions and 0 deletions
|
|
@ -1047,4 +1047,164 @@ mod tests {
|
||||||
);
|
);
|
||||||
done.expect("served");
|
done.expect("served");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_line_that_is_not_a_request_gets_an_error_and_the_server_goes_on() {
|
||||||
|
let (resp, done) = exchange(b"not json\n".to_vec(), REQUEST_DEADLINES).await;
|
||||||
|
assert!(
|
||||||
|
matches!(&resp, Response::Err { message } if message.starts_with("bad request:")),
|
||||||
|
"{resp:?}"
|
||||||
|
);
|
||||||
|
done.expect("served");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_sender_that_closes_without_a_line_gets_no_reply() {
|
||||||
|
let (client, server) = UnixStream::pair().expect("socketpair");
|
||||||
|
drop(client);
|
||||||
|
serve_one(server, REQUEST_DEADLINES, async |_| {
|
||||||
|
panic!("no request was sent")
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("an empty connection is not an error");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop `table` from the database `file` of a test's stores through a
|
||||||
|
/// second connection, so every later call on that store fails.
|
||||||
|
fn break_table(scratch: &Scratch, file: &str, table: &str) {
|
||||||
|
rusqlite::Connection::open(scratch.0.path().join(file))
|
||||||
|
.expect("a second connection")
|
||||||
|
.execute(&format!("DROP TABLE {table}"), [])
|
||||||
|
.expect("the table drops");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_store_failure_is_an_error_response_and_never_a_wake() {
|
||||||
|
let (scratch, todos, reminders) = stores();
|
||||||
|
break_table(&scratch, "todos.sqlite", "todos");
|
||||||
|
break_table(&scratch, "reminders.sqlite", "reminders");
|
||||||
|
let wake = Notify::new();
|
||||||
|
let bus = Bus::new();
|
||||||
|
let requests = [
|
||||||
|
upsert("s", Some("k"), "x"),
|
||||||
|
Request::ClearTodo {
|
||||||
|
subsystem: "s".to_owned(),
|
||||||
|
key: Some("k".to_owned()),
|
||||||
|
all: false,
|
||||||
|
},
|
||||||
|
Request::ClearTodo {
|
||||||
|
subsystem: "s".to_owned(),
|
||||||
|
key: None,
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
Request::ListTodos { subsystem: None },
|
||||||
|
Request::MarkTodoDone { id: 1 },
|
||||||
|
Request::MarkTodosDone { ids: vec![1, 2] },
|
||||||
|
Request::StoreReminder {
|
||||||
|
message: "later".to_owned(),
|
||||||
|
timing: hive_sh4re::inbox::ReminderTiming::InSeconds { seconds: 60 },
|
||||||
|
file_path: None,
|
||||||
|
},
|
||||||
|
Request::ListReminders,
|
||||||
|
Request::CancelReminder { id: 1 },
|
||||||
|
Request::CountPendingReminders,
|
||||||
|
Request::ReminderRollup { since_secs: 0 },
|
||||||
|
];
|
||||||
|
for req in requests {
|
||||||
|
let label = format!("{req:?}");
|
||||||
|
let resp = dispatch(req, &todos, &wake, Some(&reminders), &bus);
|
||||||
|
assert!(matches!(resp, Response::Err { .. }), "{label}: {resp:?}");
|
||||||
|
}
|
||||||
|
assert!(!woke(&wake), "a failed upsert wakes nothing");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_transcribe_request_is_refused_by_the_store_dispatch() {
|
||||||
|
let (_scratch, todos, reminders) = stores();
|
||||||
|
let resp = dispatch(
|
||||||
|
Request::Transcribe {
|
||||||
|
path: "/tmp/memo.ogg".to_owned(),
|
||||||
|
},
|
||||||
|
&todos,
|
||||||
|
&Notify::new(),
|
||||||
|
Some(&reminders),
|
||||||
|
&Bus::new(),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(&resp, Response::Err { message } if message.contains("connection handler")),
|
||||||
|
"{resp:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn binding_makes_the_directory_replaces_a_stale_socket_and_opens_the_mode() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let path = dir.path().join("run/nested/agent.sock");
|
||||||
|
let first = bind(&path).expect("the parent directories are created");
|
||||||
|
drop(first);
|
||||||
|
assert!(path.exists(), "the dropped listener leaves its file behind");
|
||||||
|
let _second = bind(&path).expect("a stale socket does not block the bind");
|
||||||
|
let mode = std::fs::metadata(&path)
|
||||||
|
.expect("metadata")
|
||||||
|
.permissions()
|
||||||
|
.mode();
|
||||||
|
assert_eq!(mode & 0o777, 0o666, "any uid in the container can dial it");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn binding_fails_with_the_path_when_the_directory_cannot_be_made() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let blocker = dir.path().join("file");
|
||||||
|
std::fs::write(&blocker, b"").expect("a plain file");
|
||||||
|
let e = bind(&blocker.join("agent.sock")).expect_err("a file is not a directory");
|
||||||
|
assert!(format!("{e:#}").contains("in-agent socket dir"), "{e:#}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The real path a producer takes: dial the bound socket, send a request
|
||||||
|
/// line, read the answer, with the stores and the wake behind the server.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn requests_over_the_bound_socket_reach_the_stores_and_the_wake() {
|
||||||
|
let (scratch, todos, reminders) = stores();
|
||||||
|
let path = scratch.0.path().join("agent.sock");
|
||||||
|
let listener = bind(&path).expect("bind");
|
||||||
|
let (todos, reminders) = (Arc::new(todos), Arc::new(reminders));
|
||||||
|
let wake = Arc::new(Notify::new());
|
||||||
|
let bus = Bus::new();
|
||||||
|
let server = tokio::spawn({
|
||||||
|
let (todos, reminders, wake) = (todos.clone(), reminders.clone(), wake.clone());
|
||||||
|
accept_loop(listener, 4, move |stream| {
|
||||||
|
let (todos, reminders, wake, bus) =
|
||||||
|
(todos.clone(), reminders.clone(), wake.clone(), bus.clone());
|
||||||
|
async move {
|
||||||
|
handle_conn(stream, &todos, &wake, Some(&reminders), &bus)
|
||||||
|
.await
|
||||||
|
.expect("served");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let ask = |req: Request| {
|
||||||
|
let path = path.clone();
|
||||||
|
async move {
|
||||||
|
hive_sock_client::request::<_, Response>(&path, &req, hive_sock_client::Retry::None)
|
||||||
|
.await
|
||||||
|
.expect("an answer")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = ask(upsert("forge", Some("pr-1"), "review the PR")).await;
|
||||||
|
assert!(matches!(resp, Response::Ok), "{resp:?}");
|
||||||
|
assert!(woke(&wake), "a new todo wakes the serve loop");
|
||||||
|
let resp = ask(Request::ListTodos { subsystem: None }).await;
|
||||||
|
assert!(
|
||||||
|
matches!(&resp, Response::LooseEnds { loose_ends }
|
||||||
|
if matches!(loose_ends.as_slice(), [LooseEnd::Todo { summary, .. }] if summary == "review the PR")),
|
||||||
|
"{resp:?}"
|
||||||
|
);
|
||||||
|
let resp = ask(Request::CountPendingReminders).await;
|
||||||
|
assert!(
|
||||||
|
matches!(resp, Response::PendingRemindersCount { count: 0 }),
|
||||||
|
"{resp:?}"
|
||||||
|
);
|
||||||
|
server.abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue