#![cfg(feature = "remote")]
use std::path::{Path, PathBuf};
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_roteiro");
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)
.output()
.expect("run roteiro")
}
fn fresh_repo(label: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("roteiro-remote-{label}-{}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).expect("mkdir");
git(&dir, &["init", "-q"]);
std::fs::write(dir.join("README.md"), "# fixture\n\nsome captured prose.\n").expect("write");
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
dir
}
const ENDPOINT: &str = "[remote]\nendpoint = \"https://models.example/v1/chat/completions\"\n\
model = \"some-vendor/model-2026-05\"\n";
const UNREACHABLE_LOOPBACK: &str = "http://127.0.0.1:1/v1/chat/completions";
fn project_layer(dir: &Path, body: &str) {
std::fs::write(dir.join("roteiro.toml"), format!("{ENDPOINT}{body}")).expect("write");
}
fn project_layer_at(dir: &Path, url: &str, body: &str) {
std::fs::write(
dir.join("roteiro.toml"),
format!("[remote]\nendpoint = \"{url}\"\nmodel = \"some-vendor/model-2026-05\"\n{body}"),
)
.expect("write");
}
fn ledger(dir: &Path) -> Vec<serde_json::Value> {
let path = dir.join("remote").join("egress.jsonl");
let Ok(text) = std::fs::read_to_string(path) 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 user_layer(dir: &Path, body: &str) {
std::fs::write(dir.join("config.toml"), body).expect("write");
}
fn stdout(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn status_json(dir: &Path, flags: &[&str]) -> serde_json::Value {
let mut args = vec!["remote", "status", "--json"];
args.extend_from_slice(flags);
let out = roteiro(dir, &args);
assert!(out.status.success(), "remote status failed: {out:?}");
serde_json::from_str(&stdout(&out)).expect("status emits JSON")
}
#[test]
fn a_project_layer_grant_does_not_enable_egress() {
let dir = fresh_repo("project-grant");
project_layer(&dir, "enabled = true\n");
let gate = status_json(&dir, &["--allow-remote"]);
assert_eq!(
gate["granted"], false,
"a committed file cannot grant egress"
);
assert_eq!(gate["reason"], "user_layer_unset");
assert_eq!(
gate["project_grant_ignored"], true,
"the discarded grant is reported, not swallowed"
);
assert_eq!(
gate["layers"]["project"], true,
"the file really did say yes"
);
let out = roteiro(&dir, &["remote", "status", "--allow-remote"]);
let text = stdout(&out);
assert!(text.contains("DENIED"), "{text}");
assert!(text.contains("read and ignored"), "{text}");
assert!(text.contains("never grant"), "{text}");
assert!(
text.contains("~/.roteiro/config.toml"),
"the remedy names the only file that could grant it: {text}"
);
}
#[test]
fn a_project_layer_denial_wins_over_the_user_layer_and_the_invocation() {
let dir = fresh_repo("project-denial");
project_layer(&dir, "enabled = false\n");
user_layer(&dir, "[remote]\nenabled = true\n");
let gate = status_json(&dir, &["--allow-remote"]);
assert_eq!(gate["granted"], false, "the project denied it for everyone");
assert_eq!(gate["reason"], "project_denied");
assert_eq!(gate["layers"]["user"], true, "the user really did grant");
assert_eq!(gate["layers"]["invocation"], true, "so did the invocation");
let remedy = gate["remedy"].as_str().expect("a remedy");
assert!(remedy.contains("no flag overrides"), "{remedy}");
let text = stdout(&roteiro(&dir, &["config"]));
assert!(text.contains("enabled = Some(false)"), "{text}");
}
#[test]
fn the_user_layer_and_the_invocation_are_both_required() {
let dir = fresh_repo("both-required");
project_layer(&dir, "");
for (user_grants, flag, granted, reason) in [
(false, None, false, "user_layer_unset"),
(false, Some("--allow-remote"), false, "user_layer_unset"),
(true, None, false, "invocation_unset"),
(true, Some("--allow-remote"), true, "granted"),
] {
if user_grants {
user_layer(&dir, "[remote]\nenabled = true\n");
} else {
std::fs::remove_file(dir.join("config.toml")).ok();
}
let gate = status_json(&dir, flag.as_slice());
assert_eq!(
gate["granted"], granted,
"user={user_grants} flag={flag:?} -> {gate}"
);
assert_eq!(gate["reason"], reason, "user={user_grants} flag={flag:?}");
}
user_layer(&dir, "[remote]\nenabled = true\n");
let gate = status_json(&dir, &["--no-remote"]);
assert_eq!(gate["granted"], false);
assert_eq!(gate["reason"], "invocation_denied");
}
#[test]
fn with_nothing_configured_the_tier_is_off_and_says_what_would_change_it() {
let dir = fresh_repo("default-off");
let gate = status_json(&dir, &[]);
assert_eq!(gate["granted"], false);
assert_eq!(gate["reason"], "user_layer_unset");
assert_eq!(gate["project_grant_ignored"], false);
assert!(
gate["endpoint"].is_null(),
"nothing is configured, so there is nowhere to send: {gate}"
);
assert!(
gate["endpoint_error"]
.as_str()
.is_some_and(|e| e.contains("`[remote] endpoint` is not set")),
"and it says so rather than inventing a destination: {gate}"
);
}
#[test]
fn a_dry_run_prints_the_exact_payload_and_records_nothing() {
let dir = fresh_repo("dry-run");
project_layer(&dir, "");
assert!(
roteiro(&dir, &["sync"]).status.success(),
"the fixture graph must build"
);
let out = roteiro(
&dir,
&[
"remote",
"dry-run",
"--json",
"what is this repository about?",
"--key",
"file:README.md",
],
);
assert!(out.status.success(), "dry-run failed: {out:?}");
let report: serde_json::Value = serde_json::from_str(&stdout(&out)).expect("JSON");
assert_eq!(report["sent"], false, "a dry-run sends nothing");
assert_eq!(
report["endpoint"], "https://models.example/v1/chat/completions",
"it names where the bytes would have gone"
);
assert_eq!(report["trust"], "vendor_asserted");
let body = report["body"].as_str().expect("a body");
assert!(body.contains("what is this repository about?"), "{body}");
assert!(body.contains("file:README.md"), "{body}");
assert!(
body.contains("some captured prose"),
"captured prose is disclosed, and the dry-run shows it: {body}"
);
assert_eq!(
report["bytes"].as_u64(),
Some(body.len() as u64),
"the byte count describes the body it is printed beside"
);
let disclosure = report["disclosure"].as_str().expect("a disclosure");
assert!(
disclosure.contains("no redaction chokepoint"),
"{disclosure}"
);
assert!(
disclosure.contains("commercially sensitive"),
"{disclosure}"
);
let log = stdout(&roteiro(&dir, &["remote", "log"]));
assert!(log.contains("nothing has left this machine"), "{log}");
assert!(
!dir.join("remote").join("egress.jsonl").exists(),
"a dry-run does not even create the ledger"
);
}
#[test]
fn config_reports_the_remote_layers_and_the_inversion() {
let dir = fresh_repo("config-report");
project_layer(&dir, "enabled = true\n");
user_layer(&dir, "[remote]\nenabled = false\n");
let text = stdout(&roteiro(&dir, &["config"]));
assert!(text.contains("[remote]"), "{text}");
assert!(text.contains("may deny, never grant"), "{text}");
assert!(
text.contains("read and ignored"),
"the project's grant is called out where it was written: {text}"
);
assert!(
text.contains("--allow-remote"),
"and the reader is told the invocation is still required: {text}"
);
assert!(text.contains("enabled = Some(false)"), "{text}");
assert!(
text.contains("endpoint = Some(\"https://models.example/v1/chat/completions\") (project)"),
"{text}"
);
}
#[test]
fn a_granted_call_records_its_egress_before_failing_loudly_at_the_endpoint() {
let dir = fresh_repo("call-unreachable");
project_layer_at(&dir, UNREACHABLE_LOOPBACK, "");
user_layer(&dir, "[remote]\nenabled = true\n");
assert!(
roteiro(&dir, &["sync"]).status.success(),
"the fixture graph must build"
);
let request = ["what is this repository about?", "--key", "file:README.md"];
let mut preview_args = vec!["remote", "dry-run", "--json"];
preview_args.extend_from_slice(&request);
let preview = roteiro(&dir, &preview_args);
assert!(preview.status.success(), "dry-run failed: {preview:?}");
let preview: serde_json::Value = serde_json::from_str(&stdout(&preview)).expect("JSON");
let previewed = preview["body"].as_str().expect("a body").to_owned();
assert!(
ledger(&dir).is_empty(),
"a dry-run is an inspection, not a disclosure"
);
let mut call_args = vec!["remote", "call", "--allow-remote"];
call_args.extend_from_slice(&request);
let out = roteiro(&dir, &call_args);
assert!(!out.status.success(), "the endpoint cannot be reached");
let text = stderr(&out);
assert!(
text.contains(UNREACHABLE_LOOPBACK),
"names where it went: {text}"
);
assert!(
text.contains("did **not** fall back"),
"an unannounced downgrade is the failure ADR-0019 most needs to prevent: {text}"
);
assert!(
text.contains("--allow-remote"),
"and says how to get the local answer deliberately instead: {text}"
);
let entries = ledger(&dir);
assert_eq!(entries.len(), 2, "an egress and its outcome: {entries:?}");
assert_eq!(entries[0]["event"], "egress");
assert_eq!(entries[1]["event"], "outcome");
assert_eq!(
entries[0]["call"], entries[1]["call"],
"one call, two lines"
);
assert_eq!(entries[0]["endpoint"], UNREACHABLE_LOOPBACK);
assert_eq!(entries[0]["trust"], "vendor_asserted");
assert_eq!(
entries[1]["ok"], false,
"and it is recorded as having failed"
);
assert_eq!(
entries[0]["body"].as_str(),
Some(previewed.as_str()),
"what was recorded is what the dry-run showed, byte for byte"
);
let log = stdout(&roteiro(&dir, &["remote", "log"]));
assert!(!log.contains("nothing has left this machine"), "{log}");
assert!(log.contains("SENT"), "{log}");
assert!(log.contains("FAILED"), "{log}");
}
#[test]
fn status_and_dry_run_never_prompt_even_when_a_prompt_could_open_the_gate() {
let dir = fresh_repo("never-prompt");
project_layer_at(&dir, UNREACHABLE_LOOPBACK, "");
user_layer(&dir, "[remote]\nenabled = true\n");
let gate = status_json(&dir, &[]);
assert_eq!(gate["reason"], "invocation_unset", "the promptable state");
for args in [
vec!["remote", "status"],
vec!["remote", "dry-run", "anything at all"],
] {
let out = roteiro(&dir, &args);
assert!(out.status.success(), "{args:?} failed: {out:?}");
let text = format!("{}{}", stdout(&out), stderr(&out));
assert!(
!text.contains("[y/N]"),
"{args:?} asked for consent it does not need: {text}"
);
assert!(
!text.contains("Send this now?"),
"{args:?} asked to send: {text}"
);
}
assert!(
ledger(&dir).is_empty(),
"and neither of them disclosed anything"
);
}
#[test]
fn a_non_interactive_run_is_refused_rather_than_assumed_to_consent() {
let dir = fresh_repo("non-interactive");
project_layer_at(&dir, UNREACHABLE_LOOPBACK, "");
user_layer(&dir, "[remote]\nenabled = true\n");
let out = roteiro(&dir, &["remote", "call", "what is this?"]);
assert!(!out.status.success(), "nobody granted this run");
let text = stderr(&out);
assert!(text.contains("not interactive"), "{text}");
assert!(text.contains("Nothing was sent"), "{text}");
assert!(text.contains("--allow-remote"), "{text}");
assert!(
ledger(&dir).is_empty(),
"a refusal disclosed nothing, so it records nothing"
);
}
#[test]
fn a_denied_call_reaches_no_transport_and_leaves_no_record() {
let dir = fresh_repo("call-denied");
project_layer_at(&dir, UNREACHABLE_LOOPBACK, "");
user_layer(&dir, "[remote]\nenabled = true\n");
let out = roteiro(&dir, &["remote", "call", "--no-remote", "what is this?"]);
assert!(!out.status.success(), "this run denied itself");
let text = stderr(&out);
assert!(text.contains("not enabled for this run"), "{text}");
assert!(ledger(&dir).is_empty(), "nothing left, so nothing recorded");
project_layer_at(&dir, UNREACHABLE_LOOPBACK, "enabled = false\n");
let out = roteiro(&dir, &["remote", "call", "--allow-remote", "what is this?"]);
assert!(!out.status.success());
assert!(
stderr(&out).contains("no flag overrides that"),
"{}",
stderr(&out)
);
assert!(ledger(&dir).is_empty());
}
#[test]
fn status_reports_the_backend_and_that_a_credential_is_set_but_never_its_value() {
const SECRET: &str = "sk-do-not-print-this-anywhere";
let dir = fresh_repo("credential");
project_layer_at(&dir, UNREACHABLE_LOOPBACK, "");
let out = Command::new(BIN)
.args(["remote", "status", "--json"])
.current_dir(&dir)
.env("ROTEIRO_HOME", &dir)
.env("HOME", &dir)
.env("USERPROFILE", &dir)
.env("ROTEIRO_REMOTE_API_KEY", SECRET)
.output()
.expect("run roteiro");
let text = stdout(&out);
assert!(!text.contains(SECRET), "the credential was printed: {text}");
let gate: serde_json::Value = serde_json::from_str(&text).expect("JSON");
assert_eq!(gate["credential_set"], true);
assert_eq!(gate["credential_env"], "ROTEIRO_REMOTE_API_KEY");
assert_eq!(
gate["backend"], "ureq",
"this build can send, and says so rather than leaving it to be discovered"
);
let gate = status_json(&dir, &[]);
assert_eq!(gate["credential_set"], false);
}
#[test]
fn remote_is_not_a_default_feature() {
let manifest =
std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"))
.expect("read this crate's manifest");
let default = manifest
.lines()
.find(|line| line.starts_with("default = ["))
.expect("the manifest declares a default feature set");
assert!(
!default.contains("remote"),
"`remote` reached the default feature set: {default}. ADR-0019 makes this capability \
the project's one exemption from Principle 10, and the exemption is only tolerable \
because the capability is absent unless someone asked for it at build time, again in \
their own user config, and again per invocation."
);
}