use std::path::Path;
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)
.output()
.expect("run roteiro")
}
fn graph_db(dir: &Path) -> std::path::PathBuf {
dir.join(".git/roteiro/graph.db")
}
fn stamp_from_the_future(dir: &Path) -> (u32, u32) {
let conn = rusqlite::Connection::open(graph_db(dir)).expect("open the store directly");
let build: u32 = conn
.query_row("SELECT MAX(version) FROM schema_migrations", [], |r| {
r.get(0)
})
.expect("a synced store records its migrations");
let store = build + 1;
conn.execute(
"INSERT INTO schema_migrations (version) VALUES (?1)",
[store],
)
.expect("stamp a version this build does not know");
(store, build)
}
fn node_keys(dir: &Path) -> Vec<String> {
let conn = rusqlite::Connection::open(graph_db(dir)).expect("open the store directly");
let mut stmt = conn
.prepare("SELECT key FROM nodes ORDER BY key")
.expect("prepare");
stmt.query_map([], |r| r.get::<_, String>(0))
.expect("query")
.collect::<Result<Vec<_>, _>>()
.expect("collect")
}
#[test]
fn a_store_from_the_future_refuses_writes_and_still_serves_reads() {
let dir = std::env::temp_dir().join(format!("roteiro-store-guard-cli-{}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(dir.join("src")).expect("mkdir");
std::fs::write(
dir.join("src/lib.rs"),
"/// The symbol the store starts out knowing.\npub fn original() -> u32 { 1 }\n",
)
.expect("write");
git(&dir, &["init", "-q"]);
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
let out = roteiro(&dir, &["sync", "--committed"]);
assert!(out.status.success(), "the first sync must succeed: {out:?}");
std::fs::write(
dir.join("src/later.rs"),
"/// Only a sync of the second tree can put this in the graph.\n\
pub fn added_after_the_stamp() -> u32 { 2 }\n",
)
.expect("write");
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "second tree"]);
let (store_version, build_version) = stamp_from_the_future(&dir);
let before = node_keys(&dir);
assert!(
!before.iter().any(|k| k.contains("added_after_the_stamp")),
"setup: the stamped store must predate the new symbol"
);
let out = roteiro(&dir, &["sync", "--committed"]);
assert!(
!out.status.success(),
"sync must refuse to rewrite a store from the future, not report success: {out:?}"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains(&store_version.to_string()),
"the refusal must name the store's version ({store_version}): {err}"
);
assert!(
err.contains(&build_version.to_string()),
"the refusal must name this build's version ({build_version}): {err}"
);
assert!(
err.to_lowercase().contains("upgrade"),
"the refusal must say to upgrade the binary: {err}"
);
assert_eq!(
node_keys(&dir),
before,
"the refused sync must not have touched the graph"
);
let out = roteiro(&dir, &["check", "--committed"]);
assert!(
!out.status.success(),
"a gate must refuse to rebuild a store from the future: {out:?}"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains(&store_version.to_string()) && err.to_lowercase().contains("upgrade"),
"the gate's refusal must be as actionable as the sync's: {err}"
);
assert_eq!(
node_keys(&dir),
before,
"the refused check must not have touched the graph"
);
let out = roteiro(&dir, &["search", "original", "--json"]);
assert!(
out.status.success(),
"reads against a store from the future must keep working: {out:?}"
);
let hits: serde_json::Value = serde_json::from_slice(&out.stdout).expect("valid JSON");
assert!(
!hits.as_array().expect("array").is_empty(),
"the read must return the newer build's graph, not an empty one: {hits:?}"
);
assert_eq!(
node_keys(&dir),
before,
"a read must not rewrite the graph either — `search` rebuilds before it \
reads, and that rebuild is the same silent downgrade"
);
{
let conn = rusqlite::Connection::open(graph_db(&dir)).expect("open the store directly");
conn.execute(
"DELETE FROM schema_migrations WHERE version = ?1",
[store_version],
)
.expect("unstamp");
}
let out = roteiro(&dir, &["sync", "--committed"]);
assert!(out.status.success(), "the unstamped sync must run: {out:?}");
assert!(
node_keys(&dir)
.iter()
.any(|k| k.contains("added_after_the_stamp")),
"control: the refused sync really was a graph-changing one"
);
std::fs::remove_dir_all(&dir).ok();
}