#[path = "support/order.rs"]
mod order;
#[path = "support/run.rs"]
mod owned_run;
use std::{
fs,
path::{Path, PathBuf},
process::{Command, Output},
};
use rusqlite::Connection;
use serde_json::{Value, json};
use support::os::{echo_program, shell};
#[path = "../../artifactize-core/tests/support/mod.rs"]
mod support;
use support::runs::RecordOutput;
struct Fixture {
root: owned_run::Root,
repo: PathBuf,
state: PathBuf,
}
impl Fixture {
fn new(name: &str) -> Self {
let root = owned_run::Root::from(support::os::tempdir());
let repo = root.path().join("repo");
support::copy_fixture(name, &repo);
let state = root.path().join("state");
root.track(&state, &repo);
Self { repo, state, root }
}
fn command(&self) -> Command {
let mut command = Command::new(support::os::cli());
command
.arg("--repo")
.arg(&self.repo)
.arg("--state-dir")
.arg(&self.state);
command
}
fn output(&self, args: &[&str], code: i32) -> Output {
let output = self.command().args(args).record_output().unwrap();
assert_eq!(
output.status.code(),
Some(code),
"{args:?}: {}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
output
}
fn json(&self, args: &[&str], code: i32) -> Value {
let mut args = args.to_vec();
args.push("--json");
let output = self.output(&args, code);
let value: Value = serde_json::from_slice(&output.stdout).unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
let expected = match value["id"].as_str() {
Some(id) if args[0] == "verify" => {
format!("Started {id} (follow: artifactize monitor)\n")
}
_ => String::new(),
};
assert_eq!(stderr, expected);
value
}
}
fn copy_directory(source: &Path, target: &Path) {
fs::create_dir_all(target).unwrap();
for entry in fs::read_dir(source).unwrap() {
let entry = entry.unwrap();
let to = target.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_directory(&entry.path(), &to);
} else {
fs::copy(entry.path(), to).unwrap();
}
}
}
fn row<'a>(view: &'a Value, kind: &str, id: &str) -> &'a Value {
view[kind]
.as_array()
.unwrap()
.iter()
.find(|row| row["id"] == id)
.unwrap()
}
#[test]
fn runtime_status_separates_current_validation_from_saved_attempts() {
let fixture = Fixture::new("runtime");
let fresh = fixture.json(&["status"], 1);
assert_eq!(fresh["selection"], json!({"kind":"all"}));
assert_eq!(row(&fresh, "artifacts", "input")["state"], "BASIS");
assert_eq!(row(&fresh, "evals", "green/check")["state"], "UNREVIEWED");
assert_eq!(row(&fresh, "evals", "green/check")["action"], "execute");
assert_eq!(
row(&fresh, "evals", "blocked/check")["state"],
"WAIT_DEPENDENCY"
);
assert_eq!(row(&fresh, "evals", "blocked/check")["action"], "wait");
assert_eq!(
row(&fresh, "evals", "blocked/check")["blockedBy"],
json!(["red/check"])
);
assert_eq!(fresh["counts"]["execute"], 5);
assert_eq!(fresh["counts"]["wait"], 2);
let text = String::from_utf8(fixture.output(&["status"], 1).stdout).unwrap();
assert!(text.contains("Verify actions: will execute 5, will reuse 0, wait 2, blocked 0"));
assert!(text.contains("wait: needs a result verify has not produced yet"));
assert!(!fixture.state.exists());
let basis = fixture.json(&["status", "input"], 0);
assert_eq!(basis["satisfied"], true);
assert_eq!(basis["obligations"], json!([]));
let run = fixture.json(&["verify", "--wait", "--all"], 2);
order::run_times(
&fixture.state,
run["id"].as_str().unwrap(),
"2000-01-01T00:00:00Z",
"2000-01-01T00:00:01Z",
);
let view = fixture.json(&["status"], 1);
assert_eq!(view["satisfied"], false);
assert_eq!(view["counts"]["reuse"], 0);
assert!(view.get("latestRun").is_none());
for (id, verdict, state, action) in [
("green/check", "GREEN", "STALE", "execute"),
("red/check", "RED", "STALE", "execute"),
("blocked/check", "BLOCKED", "WAIT_DEPENDENCY", "wait"),
("timeout/check", "ERROR", "STALE", "execute"),
(
"waiting/check",
"WAIT_DEPENDENCY",
"WAIT_DEPENDENCY",
"wait",
),
] {
let eval = row(&view, "evals", id);
assert_eq!(eval["last"], json!({"runId":run["id"],"verdict":verdict}));
assert_eq!(eval["state"], state);
assert_eq!(eval["action"], action);
}
let rerun = fixture.json(&["verify", "--wait", "green"], 0);
order::run_times(
&fixture.state,
rerun["id"].as_str().unwrap(),
"2000-01-02T00:00:00Z",
"2000-01-02T00:00:01Z",
);
assert_eq!(rerun["validation"]["satisfied"], true);
let later = fixture.json(&["status"], 1);
assert_eq!(
row(&later, "evals", "green/check")["last"]["runId"],
rerun["id"]
);
assert_eq!(
row(&later, "evals", "red/check")["last"]["runId"],
run["id"]
);
let text = String::from_utf8(fixture.output(&["status", "green"], 1).stdout).unwrap();
assert!(text.contains("Current validation: NOT SATISFIED"));
assert!(text.contains("green/check: STALE — execute"));
assert!(text.contains("Last: GREEN"));
assert!(text.contains("historical, not current evidence"));
}
#[test]
fn status_selection_policy_and_final_obligations_match_verify() {
let fixture = Fixture::new("runtime");
let selected = fixture.json(&["status", "blocked"], 1);
assert_eq!(selected["selectedEvalIds"], json!(["blocked/check"]));
assert_eq!(selected["includedEvalIds"], json!(["blocked/check"]));
assert_eq!(row(&selected, "evals", "red/check")["included"], false);
let recursive = fixture.json(&["status", "blocked", "--recursive", "--force"], 1);
assert_eq!(
recursive["includedEvalIds"],
json!(["blocked/check", "red/check"])
);
assert_eq!(row(&recursive, "evals", "blocked/check")["force"], true);
assert_eq!(row(&recursive, "evals", "blocked/check")["action"], "wait");
assert_eq!(row(&recursive, "evals", "red/check")["force"], false);
support::declaration::write(
fixture.repo.join("index.artf"),
r#"{"name":"project","basis":true}"#,
)
.unwrap();
let incomplete = fixture.json(&["status", "project"], 1);
assert_eq!(
row(&incomplete, "artifacts", "project")["state"],
"INCOMPLETE"
);
assert!(
!incomplete["obligations"]
.as_array()
.unwrap()
.contains(&json!("project"))
);
let selector = fixture.root.path().join("selection.json");
fs::write(&selector, r#"["red/check","green/check","red/check"]"#).unwrap();
let file = fixture.json(&["status", "--evals-file", selector.to_str().unwrap()], 1);
let csv = fixture.json(&["status", "--evals", "red/check,green/check"], 1);
assert_eq!(file, csv);
for args in [
vec!["status", "red", "--all"],
vec!["status", "--eval", "red/check", "--evals", "red/check"],
vec!["status", "missing"],
vec!["status", "--full"],
vec!["config", "graph", "--compact"],
vec!["config", "graph", "missing"],
vec!["verify"],
] {
fixture.json(&args, 2);
}
assert!(!fixture.state.exists());
}
#[test]
fn profiles_rebuild_static_gates_without_running_commands() {
let fixture = Fixture::new("runtime");
let path = fixture.repo.join("red/index.artf");
let mut declaration: Value = support::declaration::read(fs::read(&path).unwrap()).unwrap();
declaration["evals"][0]["profile_variants"] = json!({"dependent":{
"kind":"runtime","command":"missing-command","args":["{green}/missing.txt"]
}});
support::declaration::write(&path, declaration.to_string()).unwrap();
let view = fixture.json(&["status", "red", "--profile", "dependent"], 1);
assert_eq!(row(&view, "evals", "red/check")["state"], "WAIT_DEPENDENCY");
assert_eq!(
row(&view, "evals", "red/check")["blockedBy"],
json!(["green/check"])
);
assert_eq!(
row(&view, "evals", "red/check")["profile"]["command"],
"missing-command"
);
fixture.json(&["status", "red", "--profile", "unknown"], 2);
assert!(!fixture.state.exists());
}
#[test]
fn graph_projects_typed_edges_closure_and_dependency_first_cycles() {
let fixture = Fixture::new("runtime");
support::declaration::write(
fixture.repo.join("index.artf"),
r#"{"name":"project","basis":true}"#,
)
.unwrap();
let graph = fixture.json(&["config", "graph"], 0);
assert_eq!(graph["version"], 1);
let relations = graph["relations"].as_array().unwrap();
for (kind, field, value) in [
("child", "path", "review"),
("instruction", "evalId", "cycle-a/check"),
("argv", "evalId", "green/check"),
] {
assert!(
relations
.iter()
.any(|edge| edge["kind"] == kind && edge[field] == value)
);
}
let edge = relations
.iter()
.find(|edge| edge["kind"] == "argv")
.unwrap();
assert_eq!(edge["index"], 1);
assert_eq!(edge["name"], "input");
assert_eq!(edge["path"], "data.txt");
assert_eq!(edge["source"], "input");
assert_eq!(edge["target"], "green");
assert_eq!(edge["cyclic"], false);
let cycle = fixture.json(&["config", "graph", "cycle-a"], 0);
assert_eq!(cycle["artifacts"].as_object().unwrap().len(), 2);
assert_eq!(cycle["components"].as_array().unwrap().len(), 1);
assert_eq!(
cycle["components"][0]["artifacts"],
json!(["cycle-a", "cycle-b"])
);
assert_eq!(cycle["components"][0]["cyclic"], true);
assert!(
cycle["relations"]
.as_array()
.unwrap()
.iter()
.all(|edge| edge["cyclic"] == true)
);
for component in graph["components"].as_array().unwrap() {
for dependency in component["dependencies"].as_array().unwrap() {
assert!(dependency.as_u64().unwrap() < component["id"].as_u64().unwrap());
}
}
let green = fixture.json(&["config", "graph", "green"], 0);
assert_eq!(
green["artifacts"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
["green", "input"]
);
assert_eq!(
green["evals"][0]["declaration"]["profile"]["command"],
shell()
);
let text =
String::from_utf8(fixture.output(&["config", "graph", "cycle-a"], 0).stdout).unwrap();
assert!(text.contains("[cycle]"));
assert!(text.contains("cycle-b -> cycle-a [instruction"));
assert!(!fixture.state.exists());
}
#[test]
fn static_commands_never_execute_hooks_and_status_only_runs_fingerprint() {
let fixture = Fixture::new("declarations");
let marker = fixture.repo.join("review/hook-executed");
let hook = fixture.repo.join("review/hook.sh");
support::os::write_script(&hook, "printf called > hook-executed\nexit 91\n");
support::os::allow_execution(&hook);
fixture.json(&["config", "check"], 0);
fixture.json(&["config", "graph"], 0);
assert!(!marker.exists());
assert!(!fixture.state.exists());
let path = fixture.repo.join("review/index.artf");
let mut declaration: Value = support::declaration::read(fs::read(&path).unwrap()).unwrap();
declaration["fingerprint"]["script"] =
json!({"command":echo_program(),"args":["current-fingerprint"]});
support::declaration::write(path, declaration.to_string()).unwrap();
let view = fixture.json(&["status"], 1);
assert_eq!(row(&view, "artifacts", "unreviewed")["state"], "UNREVIEWED");
assert_eq!(row(&view, "evals", "review/agent")["action"], "execute");
assert_eq!(row(&view, "evals", "review/human")["action"], "wait");
assert_eq!(row(&view, "evals", "review/runtime")["action"], "execute");
assert!(!marker.exists());
assert!(!fixture.state.join("state.sqlite").exists());
assert_eq!(fs::read_dir(&fixture.state).unwrap().count(), 0);
}
#[test]
fn status_reads_only_this_repository_and_preserves_database_and_run_output() {
let fixture = Fixture::new("runtime");
let first = fixture.json(&["verify", "--wait", "green"], 0);
let other = fixture.root.path().join("other");
copy_directory(&fixture.repo, &other);
let output = fixture
.command()
.arg("--json")
.args(["verify", "--wait", "red"])
.current_dir(&other)
.record_output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let other_status = Command::new(support::os::cli())
.arg("--repo")
.arg(&other)
.arg("--state-dir")
.arg(&fixture.state)
.args(["status", "green", "--json"])
.record_output()
.unwrap();
assert_eq!(other_status.status.code(), Some(1));
let other_status: Value = serde_json::from_slice(&other_status.stdout).unwrap();
assert!(
row(&other_status, "evals", "green/check")
.get("last")
.is_none()
);
let db = Connection::open_with_flags(
fixture.state.join("state.sqlite"),
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
)
.unwrap();
let before: String = db
.query_row("SELECT group_concat(data) FROM runs", [], |row| row.get(0))
.unwrap();
fixture.json(&["status"], 1);
let after: String = db
.query_row("SELECT group_concat(data) FROM runs", [], |row| row.get(0))
.unwrap();
assert_eq!(before, after);
assert!(
fixture
.state
.join("runs")
.join(first["id"].as_str().unwrap())
.is_dir()
);
assert_eq!(
fs::read_to_string(
Path::new(first["requests"][0]["runDir"].as_str().unwrap()).join("output/result.txt")
)
.unwrap(),
"runtime output\n"
);
fixture.root.release(&fixture.state, &fixture.repo).unwrap();
Connection::open(fixture.state.join("state.sqlite"))
.unwrap()
.pragma_update(None, "user_version", 99)
.unwrap();
assert!(
fixture.json(&["status"], 2)["error"]
.as_str()
.unwrap()
.contains("Unsupported state schema")
);
assert_eq!(
db.pragma_query_value::<u32, _>(None, "user_version", |row| row.get(0))
.unwrap(),
99
);
let link = fixture.root.path().join("redirected-state");
support::os::link_dir(&fixture.repo, &link);
let output = Command::new(support::os::cli())
.arg("--repo")
.arg(&fixture.repo)
.arg("--state-dir")
.arg(link)
.args(["status", "--json"])
.record_output()
.unwrap();
assert_eq!(output.status.code(), Some(2));
assert!(String::from_utf8_lossy(&output.stdout).contains("outside the reviewed repository"));
}