#![cfg(feature = "remote")]
use std::path::{Path, PathBuf};
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_roteiro");
const UNREACHABLE_LOOPBACK: &str = "http://127.0.0.1:1/v1/chat/completions";
const MODEL: &str = "some-vendor/model-2026-05";
fn git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.args([
"-c",
"user.name=Test",
"-c",
"user.email=test@example.com",
"-c",
"commit.gpgsign=false",
"-c",
"init.defaultBranch=main",
])
.args(args)
.current_dir(dir)
.status()
.expect("run git");
assert!(status.success(), "git {args:?} failed");
}
fn roteiro(dir: &Path, args: &[&str]) -> std::process::Output {
Command::new(BIN)
.args(args)
.current_dir(dir)
.env("ROTEIRO_HOME", dir)
.env("HOME", dir)
.env("USERPROFILE", dir)
.stdin(std::process::Stdio::null())
.output()
.expect("run roteiro")
}
fn fresh_repo(label: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"roteiro-remote-surface-{label}-{}",
std::process::id()
));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(dir.join("src")).expect("mkdir");
git(&dir, &["init", "-q"]);
std::fs::write(
dir.join("README.md"),
"# fixture\n\n## Store\n\nThe store holds nodes and edges.\n",
)
.expect("write");
std::fs::write(
dir.join("src/store.rs"),
"/// The store of nodes and edges.\npub struct Store;\n\n pub fn store_nodes(store: &Store) {\n let _ = store;\n}\n",
)
.expect("write");
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
dir
}
fn project_layer(dir: &Path, endpoint: &str, body: &str) {
std::fs::write(
dir.join("roteiro.toml"),
format!("[remote]\nendpoint = \"{endpoint}\"\nmodel = \"{MODEL}\"\n{body}"),
)
.expect("write");
}
fn user_layer(dir: &Path, body: &str) {
std::fs::write(dir.join("config.toml"), body).expect("write");
}
fn ledger(dir: &Path) -> Vec<serde_json::Value> {
let Ok(text) = std::fs::read_to_string(dir.join("remote").join("egress.jsonl")) else {
return Vec::new();
};
text.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| serde_json::from_str(l).expect("a ledger line is JSON"))
.collect()
}
fn stderr(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn stdout(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[test]
fn spec_draft_allow_remote_refused_stops_the_run_rather_than_drafting_locally() {
let dir = fresh_repo("draft-refused");
project_layer(&dir, UNREACHABLE_LOOPBACK, "");
let out = roteiro(&dir, &["spec", "draft", "store", "--allow-remote"]);
assert!(!out.status.success(), "a refused grant must not succeed");
let err = stderr(&out);
assert!(
err.contains("did **not** fall back"),
"says it did not degrade: {err}"
);
assert!(
err.contains("~/.roteiro/config.toml"),
"names the layer that would change it: {err}"
);
assert!(
err.contains("--allow-remote"),
"and the deliberate local alternative: {err}"
);
assert!(
!stdout(&out).contains("_TODO"),
"no scaffold was emitted as a consolation: {}",
stdout(&out)
);
assert!(
ledger(&dir).is_empty(),
"a refusal disclosed nothing, so it records nothing"
);
}
#[test]
fn a_project_denial_refuses_spec_draft_with_the_repository_remedy() {
let dir = fresh_repo("draft-project-denied");
project_layer(&dir, UNREACHABLE_LOOPBACK, "enabled = false\n");
user_layer(&dir, "[remote]\nenabled = true\n");
let out = roteiro(&dir, &["spec", "draft", "store", "--allow-remote"]);
assert!(!out.status.success());
let err = stderr(&out);
assert!(
err.contains("no flag overrides"),
"does not advise editing a file that would not help: {err}"
);
assert!(ledger(&dir).is_empty(), "nothing left the machine");
}
#[test]
fn an_ignored_project_grant_is_reported_on_the_draft_surface_too() {
let dir = fresh_repo("draft-ignored-grant");
project_layer(&dir, UNREACHABLE_LOOPBACK, "enabled = true\n");
let out = roteiro(&dir, &["spec", "draft", "store", "--allow-remote"]);
assert!(!out.status.success(), "a committed grant is not a grant");
let err = stderr(&out);
assert!(err.contains("read and ignored"), "{err}");
assert!(ledger(&dir).is_empty());
}
#[test]
fn a_granting_user_layer_without_the_flag_sends_nothing_and_never_asks() {
let dir = fresh_repo("draft-no-flag");
project_layer(&dir, UNREACHABLE_LOOPBACK, "");
user_layer(&dir, "[remote]\nenabled = true\n");
let out = roteiro(&dir, &["spec", "draft", "store"]);
let err = stderr(&out);
assert!(
!err.contains("Send this now?"),
"this surface must never prompt: {err}"
);
assert!(
ledger(&dir).is_empty(),
"the user layer alone never sends: {:?}",
ledger(&dir)
);
}
#[test]
fn no_remote_is_a_quiet_local_run() {
let dir = fresh_repo("draft-no-remote");
project_layer(&dir, UNREACHABLE_LOOPBACK, "");
user_layer(&dir, "[remote]\nenabled = true\n");
let out = roteiro(&dir, &["spec", "draft", "store", "--no-remote"]);
let err = stderr(&out);
assert!(
!err.contains("did **not** fall back"),
"nobody asked for the tier, so nothing was downgraded: {err}"
);
assert!(ledger(&dir).is_empty());
}
#[test]
fn a_granted_draft_records_its_egress_and_still_refuses_to_fall_back() {
let dir = fresh_repo("draft-granted");
project_layer(&dir, UNREACHABLE_LOOPBACK, "");
user_layer(&dir, "[remote]\nenabled = true\n");
let out = roteiro(&dir, &["spec", "draft", "store", "--allow-remote"]);
assert!(!out.status.success(), "nothing is listening on port 1");
let err = stderr(&out);
assert!(
err.contains(UNREACHABLE_LOOPBACK),
"names the endpoint: {err}"
);
assert!(
err.contains("did **not** fall back"),
"refuses to degrade even with a local model available: {err}"
);
let entries = ledger(&dir);
assert!(
!entries.is_empty(),
"the egress is recorded before it is attempted"
);
let egress = &entries[0];
assert_eq!(egress["endpoint"], UNREACHABLE_LOOPBACK);
assert_eq!(egress["model"], MODEL);
assert_eq!(
egress["trust"], "vendor_asserted",
"a hosted model's identity is a claim, and the record says so"
);
let body = egress["body"].as_str().expect("the recorded body");
assert!(
body.contains("You are drafting"),
"the rebuilt instruction is what was sent: {body}"
);
assert!(
body.contains("sym:") || body.contains("doc:"),
"and the graph reached it as context items, so grounding did happen: {body}"
);
for lifted in ["Reference the real symbols above", "Relevant code symbols:"] {
assert!(
!body.contains(lifted),
"the local prompt string must not be lifted onto the wire — graph content \
reaches the endpoint only through the allow-list, and `{lifted}` is how it \
would arrive if it did not: {body}"
);
}
}
#[test]
fn a_granted_draft_without_an_endpoint_names_the_missing_key() {
let dir = fresh_repo("draft-no-endpoint");
user_layer(&dir, "[remote]\nenabled = true\n");
let out = roteiro(&dir, &["spec", "draft", "store", "--allow-remote"]);
assert!(!out.status.success());
assert!(
stderr(&out).contains("`[remote] endpoint` is not set"),
"{}",
stderr(&out)
);
assert!(ledger(&dir).is_empty());
}
#[test]
fn a_configured_but_unconsented_repository_drafts_nothing_remotely() {
let dir = fresh_repo("draft-default-off");
project_layer(&dir, UNREACHABLE_LOOPBACK, "");
let out = roteiro(&dir, &["spec", "draft", "store"]);
let err = stderr(&out);
assert!(
!err.contains("via the remote model tier"),
"nothing was drafted remotely: {err}"
);
assert!(
!err.contains("bytes total"),
"no receipt was printed: {err}"
);
assert!(
ledger(&dir).is_empty(),
"the tier is off until both layers grant: {:?}",
ledger(&dir)
);
}
#[test]
fn a_remote_model_colliding_with_a_local_id_is_refused_naming_both() {
let dir = fresh_repo("model-collision");
std::fs::write(
dir.join("roteiro.toml"),
format!("[remote]\nendpoint = \"{UNREACHABLE_LOOPBACK}\"\nmodel = \"qwen3-0.6b\"\n"),
)
.expect("write");
user_layer(&dir, "[remote]\nenabled = true\n");
let out = roteiro(&dir, &["spec", "draft", "store", "--allow-remote"]);
assert!(!out.status.success(), "a colliding id must not be usable");
let err = stderr(&out);
assert!(
err.contains("[remote] model"),
"names the key that is wrong: {err}"
);
assert!(
err.contains("qwen3-0.6b"),
"and the local id it collides with: {err}"
);
assert!(
err.contains("roteiro model list"),
"actionable — the names that are taken: {err}"
);
assert!(
ledger(&dir).is_empty(),
"nothing left the machine under a borrowed name: {:?}",
ledger(&dir)
);
let status = roteiro(&dir, &["remote", "status", "--json"]);
assert!(status.status.success(), "status still reports: {status:?}");
let gate: serde_json::Value =
serde_json::from_str(&stdout(&status)).expect("status emits JSON");
let reported = gate["endpoint_error"]
.as_str()
.expect("an endpoint error is reported");
assert!(reported.contains("[remote] model"), "{reported}");
assert!(reported.contains("qwen3-0.6b"), "{reported}");
}
#[test]
fn a_vendor_model_string_that_is_not_a_registry_name_is_accepted() {
let dir = fresh_repo("model-no-collision");
project_layer(&dir, UNREACHABLE_LOOPBACK, "");
user_layer(&dir, "[remote]\nenabled = true\n");
let status = roteiro(&dir, &["remote", "status", "--json"]);
let gate: serde_json::Value =
serde_json::from_str(&stdout(&status)).expect("status emits JSON");
assert!(
gate["endpoint_error"].is_null(),
"`{MODEL}` is nobody's registry entry: {gate}"
);
assert_eq!(gate["model"], MODEL);
}