The OPML file and the profile's ## Interests section become one-time import inputs; the prompt groups by the stored category and the OPML config key is gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
73 lines
3.0 KiB
Rust
73 lines
3.0 KiB
Rust
//! `daily-epub config check` end to end: the built binary, a temp config, no
|
|
//! environment. It must validate like `generate`, print the provider table
|
|
//! with `key MISSING` warnings, exit 0, and exit non-zero on an invalid file —
|
|
//! all without a database or the run lock.
|
|
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
fn run(config_body: &str) -> (i32, String, String) {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
let path = dir.path().join("config.toml");
|
|
std::fs::write(&path, config_body).expect("write config");
|
|
let output = Command::new(env!("CARGO_BIN_EXE_daily-epub"))
|
|
.env_clear()
|
|
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
|
|
.args(["--config"])
|
|
.arg(&path)
|
|
.args(["config", "check"])
|
|
.output()
|
|
.expect("run daily-epub");
|
|
(
|
|
output.status.code().unwrap_or(-1),
|
|
String::from_utf8_lossy(&output.stdout).into_owned(),
|
|
String::from_utf8_lossy(&output.stderr).into_owned(),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn config_check_prints_the_facts_and_exits_zero_without_keys() {
|
|
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("config.example.toml");
|
|
let body = std::fs::read_to_string(example).expect("example config");
|
|
let (code, stdout, stderr) = run(&body);
|
|
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
|
|
for needle in [
|
|
"config: ",
|
|
"database_path: /var/lib/daily-epub/daily-epub.db",
|
|
"profile_path: data/profile.md",
|
|
"llm.bulk: deepseek · openai · deepseek-v4-flash",
|
|
"key MISSING (set DAILY_EPUB_PROVIDERS__DEEPSEEK__API_KEY)",
|
|
"llm.editor: anthropic · anthropic · claude-opus-5 · effort high · max_daily_usd $3.00",
|
|
"key MISSING (set DAILY_EPUB_PROVIDERS__ANTHROPIC__API_KEY)",
|
|
"providers.gemini: unreferenced",
|
|
"voyage: voyage-4-lite · enabled · max_daily_usd $0.50 · key MISSING (set DAILY_EPUB_VOYAGE__API_KEY)",
|
|
"editorial.summary_model: editor",
|
|
"publish.epub_dir: /srv/bookorbit/libraries/daily-epub",
|
|
"publish.xtc_dir: /var/lib/daily-epub/xtc",
|
|
] {
|
|
assert!(stdout.contains(needle), "missing {needle:?} in:\n{stdout}");
|
|
}
|
|
assert!(
|
|
stdout.lines().any(|line| line.starts_with("! ")),
|
|
"missing keys are flagged with a `!` prefix:\n{stdout}"
|
|
);
|
|
// No lock file, no database: the command is read-only.
|
|
assert!(!Path::new("/var/lib/daily-epub/daily-epub.db.lock").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn config_check_exits_non_zero_on_an_invalid_config() {
|
|
let (code, stdout, stderr) = run("[llm]\nbulk = \"nope\"\n");
|
|
assert_ne!(code, 0);
|
|
assert!(stdout.is_empty(), "{stdout}");
|
|
assert!(stderr.contains("providers.nope"), "{stderr}");
|
|
|
|
let (code, _, stderr) = run("[deepseek]\nmodel = \"x\"\n");
|
|
assert_ne!(code, 0);
|
|
assert!(stderr.contains("[providers.deepseek]"), "{stderr}");
|
|
|
|
let (code, _, stderr) = run("interests_opml = \"data/scour-interests.opml\"\n");
|
|
assert_ne!(code, 0);
|
|
assert!(stderr.contains("interests_opml"), "{stderr}");
|
|
}
|