usage-meter-poll: a subprocess test that serve --config starts the poll
Spawns the claude-connector binary on a ready document and reads the usage poll state line from its stderr; the connector must also stay up. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c1cc349b19
commit
e17ef16d48
1 changed files with 163 additions and 0 deletions
163
crates/claude-connector/tests/serve_config.rs
Normal file
163
crates/claude-connector/tests/serve_config.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
//! `claude-connector serve --config` as a process: it binds, stays up, and
|
||||||
|
//! starts the usage poll beside the lease listener. Only a spawned binary
|
||||||
|
//! shows that `serve_config` calls the poll start at all; the poll's own
|
||||||
|
//! behaviour is tested against fake servers in `usage_poll`.
|
||||||
|
//!
|
||||||
|
//! The usage endpoint is a constant of the connector, so this test's poll
|
||||||
|
//! asks the real host with a made-up token and sees an answer or an
|
||||||
|
//! unreachable network; either way it logs a `usage poll:` state line, and
|
||||||
|
//! that line is what is asserted.
|
||||||
|
#![cfg(unix)]
|
||||||
|
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
const TOKEN: &str = "sk-ant-oat01-CANARY-must-never-surface";
|
||||||
|
|
||||||
|
struct Scratch(PathBuf);
|
||||||
|
|
||||||
|
impl Scratch {
|
||||||
|
fn new() -> Self {
|
||||||
|
let dir =
|
||||||
|
std::env::temp_dir().join(format!("claude-connector-serve-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
Self(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self, name: &str) -> PathBuf {
|
||||||
|
self.0.join(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Scratch {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kills the served process however the test ends.
|
||||||
|
struct Served(Child);
|
||||||
|
|
||||||
|
impl Drop for Served {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.0.kill();
|
||||||
|
let _ = self.0.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex(bytes: &[u8]) -> String {
|
||||||
|
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(args: &[&str]) {
|
||||||
|
let made = Command::new(env!("CARGO_BIN_EXE_claude-connector"))
|
||||||
|
.args(args)
|
||||||
|
.env_clear()
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(made.status.success(), "{made:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A document the connector can serve: a pinned stand-in for the claude
|
||||||
|
/// binary, a 0700 prompt directory, and the unit's credentials directory.
|
||||||
|
fn ready_document(scratch: &Scratch) -> (PathBuf, PathBuf) {
|
||||||
|
let path = |name: &str| scratch.path(name);
|
||||||
|
let text = |name: &str| path(name).to_string_lossy().into_owned();
|
||||||
|
std::fs::write(path("claude"), b"not a real claude binary").unwrap();
|
||||||
|
let pin = hex(&Sha256::digest(b"not a real claude binary"));
|
||||||
|
for dir in ["work", "home", "prompts", "credentials"] {
|
||||||
|
std::fs::create_dir(path(dir)).unwrap();
|
||||||
|
}
|
||||||
|
std::fs::set_permissions(path("prompts"), std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||||
|
std::fs::write(path("caller.key"), "k".repeat(48)).unwrap();
|
||||||
|
std::fs::write(path("credentials/claude-token"), TOKEN).unwrap();
|
||||||
|
let config = text("connector.cc");
|
||||||
|
run(&[
|
||||||
|
"config",
|
||||||
|
"init",
|
||||||
|
&config,
|
||||||
|
"--listen",
|
||||||
|
"127.0.0.1:0",
|
||||||
|
"--replay-store",
|
||||||
|
&text("replay.cc"),
|
||||||
|
"--claude-binary",
|
||||||
|
&text("claude"),
|
||||||
|
"--claude-sha256",
|
||||||
|
&pin,
|
||||||
|
"--work-dir",
|
||||||
|
&text("work"),
|
||||||
|
"--config-dir",
|
||||||
|
&text("home"),
|
||||||
|
"--prompt-dir",
|
||||||
|
&text("prompts"),
|
||||||
|
"--instance",
|
||||||
|
"persona-a",
|
||||||
|
"--token-credential",
|
||||||
|
"claude-token",
|
||||||
|
"--handle-secret-credential",
|
||||||
|
"claude-handle-secret",
|
||||||
|
"--handle-secret-file",
|
||||||
|
&text("credentials/claude-handle-secret"),
|
||||||
|
"--caller-id",
|
||||||
|
"persona-a-caller",
|
||||||
|
"--connection-key-file",
|
||||||
|
&text("caller.key"),
|
||||||
|
"--connection-key-epoch",
|
||||||
|
"1",
|
||||||
|
"--allowed-model",
|
||||||
|
"claude-haiku-5-5",
|
||||||
|
]);
|
||||||
|
(path("connector.cc"), path("credentials"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve(config: &Path, credentials: &Path) -> (Served, mpsc::Receiver<String>) {
|
||||||
|
let mut child = Command::new(env!("CARGO_BIN_EXE_claude-connector"))
|
||||||
|
.args(["serve", "--config"])
|
||||||
|
.arg(config)
|
||||||
|
.env_clear()
|
||||||
|
.env("CREDENTIALS_DIRECTORY", credentials)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
let stderr = child.stderr.take().unwrap();
|
||||||
|
let (lines, received) = mpsc::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
for line in BufReader::new(stderr).lines().map_while(Result::ok) {
|
||||||
|
if lines.send(line).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(Served(child), received)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serve_config_starts_the_usage_poll_and_stays_up() {
|
||||||
|
let scratch = Scratch::new();
|
||||||
|
let (config, credentials) = ready_document(&scratch);
|
||||||
|
let (mut served, lines) = serve(&config, &credentials);
|
||||||
|
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(60);
|
||||||
|
let mut seen = Vec::new();
|
||||||
|
let poll_line = loop {
|
||||||
|
let left = deadline.saturating_duration_since(Instant::now());
|
||||||
|
match lines.recv_timeout(left) {
|
||||||
|
Ok(line) if line.contains("usage poll:") => break line,
|
||||||
|
Ok(line) => seen.push(line),
|
||||||
|
Err(_) => panic!("no usage poll state line; the connector said {seen:?}"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
served.0.try_wait().unwrap().is_none(),
|
||||||
|
"the connector exited after starting the poll"
|
||||||
|
);
|
||||||
|
assert!(!poll_line.contains("CANARY"), "{poll_line}");
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue