Skip to content

Commit f808220

Browse files
committed
test(cli): cover the signed-out chatgpt and --models paths
1 parent 95bc185 commit f808220

1 file changed

Lines changed: 68 additions & 0 deletions

File tree

crates/llm_stream/tests/cli.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//! Offline coverage of the CLI's surface.
2+
//!
3+
//! Nothing here touches the network or needs credentials — every path asserted
4+
//! below fails (or answers) before a socket is opened — so these run on any
5+
//! machine, in CI, for free. Contrast `live_chatgpt.rs`, which is opt-in.
6+
7+
use std::process::{Command, Stdio};
8+
9+
/// The one sentence a signed-out operator should ever see. Duplicated from
10+
/// `crate::auth::flow::NOT_SIGNED_IN` on purpose: an integration test cannot
11+
/// import from a binary-only crate, and a test that reads the constant it is
12+
/// checking would pass no matter what the constant said.
13+
///
14+
/// The dash is U+2014.
15+
const NOT_SIGNED_IN: &str = "not signed in — run: llm-stream --login";
16+
17+
/// Runs the CLI against an empty, throwaway config directory, so there are no
18+
/// credentials to find. Returns `(exit_ok, stdout, stderr)`.
19+
fn run_without_credentials(args: &[&str]) -> (bool, String, String) {
20+
let dir = match tempfile::tempdir() {
21+
Ok(dir) => dir,
22+
Err(e) => panic!("could not create a temporary config directory: {e}"),
23+
};
24+
let mut command = Command::new(env!("CARGO_BIN_EXE_llm-stream"));
25+
command.arg("--config-dir").arg(dir.path());
26+
command.args(args);
27+
// Without this the child inherits the harness's stdin, and `parse_args`
28+
// reads stdin whenever it is not a terminal — the test would hang.
29+
let output = match command.stdin(Stdio::null()).output() {
30+
Ok(output) => output,
31+
Err(e) => panic!("could not run the llm-stream binary: {e}"),
32+
};
33+
(
34+
output.status.success(),
35+
String::from_utf8_lossy(&output.stdout).into_owned(),
36+
String::from_utf8_lossy(&output.stderr).into_owned(),
37+
)
38+
}
39+
40+
#[test]
41+
fn a_prompt_without_credentials_names_the_login_command() {
42+
let (ok, stdout, stderr) = run_without_credentials(&["--api", "chatgpt", "hi"]);
43+
assert!(
44+
!ok,
45+
"expected a failure exit\nstdout: {stdout}\nstderr: {stderr}"
46+
);
47+
assert!(stderr.contains(NOT_SIGNED_IN), "stderr: {stderr}");
48+
// A caller piping stdout must get an empty pipe, not half an error.
49+
assert!(stdout.is_empty(), "nothing should reach stdout: {stdout}");
50+
}
51+
52+
#[test]
53+
fn models_without_credentials_names_the_login_command() {
54+
let (ok, stdout, stderr) = run_without_credentials(&["--models"]);
55+
assert!(
56+
!ok,
57+
"expected a failure exit\nstdout: {stdout}\nstderr: {stderr}"
58+
);
59+
assert!(stderr.contains(NOT_SIGNED_IN), "stderr: {stderr}");
60+
// The quota warning must come after the credential check. Warning someone
61+
// about a cost they were never going to pay is noise that trains them to
62+
// ignore the warning when it matters.
63+
assert!(
64+
!stderr.contains("quota"),
65+
"no quota warning before the guard: {stderr}"
66+
);
67+
assert!(stdout.is_empty(), "nothing should reach stdout: {stdout}");
68+
}

0 commit comments

Comments
 (0)