use std::{fs, path::Path, process::Command};
use rusqlite::Connection;
use serde_json::{Value, json};
mod support;
const VERSION_ONE: &str = include_str!("fixtures/state-v1.sql");
const VERSION_TWO: &str = include_str!("fixtures/state-v2.sql");
const VERSION_THREE: &str = include_str!("fixtures/state-v3.sql");
struct Recorded {
sql: &'static str,
version: u32,
run: &'static str,
waiting: &'static str,
app: (&'static str, Value),
notes: (&'static str, Value),
doc: (&'static str, Value, Value, &'static str),
definitions: (Value, Value),
}
fn doctor(root: &Path) -> Value {
let output = Command::new(env!("CARGO_BIN_EXE_artifactize"))
.env("PATH", root)
.env_remove("OPENAI_API_KEY")
.env_remove("ANTHROPIC_API_KEY")
.env_remove("ARTIFACTIZE_REMOTE")
.env_remove("ARTIFACTIZE_REMOTE_TOKEN")
.arg("--state-dir")
.arg(root.join("state"))
.args(["doctor", "--json"])
.output()
.unwrap();
assert_eq!(
output.status.code(),
Some(0),
"{}",
String::from_utf8_lossy(&output.stdout)
);
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
report["checks"]
.as_array()
.unwrap()
.iter()
.find(|check| check["name"] == "schema")
.unwrap()
.clone()
}
fn json(root: &Path, args: &[&str], code: i32) -> Value {
let output = Command::new(env!("CARGO_BIN_EXE_artifactize"))
.env("PATH", support::os::path())
.env("USER", "alice")
.arg("--repo")
.arg(root.join("repo"))
.arg("--state-dir")
.arg(root.join("state"))
.args(args)
.arg("--json")
.output()
.unwrap();
assert_eq!(
output.status.code(),
Some(code),
"{args:?}: {} {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).unwrap()
}
fn names(db: &Connection, query: &str) -> Vec<String> {
db.prepare(query)
.unwrap()
.query_map([], |row| row.get(0))
.unwrap()
.collect::<Result<_, _>>()
.unwrap()
}
fn upgrades_keeps_history_and_reviews_again_once(recorded: Recorded) {
let temp = tempfile::tempdir().unwrap();
let root = support::os::canonical(temp.path());
let runtime = json!({"kind":"runtime","command":"true","args":[]});
let (app_key, app) = recorded.app;
let (notes_key, notes) = recorded.notes;
let (doc_key, doc, mounts, instruction) = recorded.doc;
for (name, key, fingerprint, profile, mounts, instruction) in [
("app", app_key, app, runtime.clone(), json!({}), "Check."),
("notes", notes_key, notes, runtime, json!({}), "Check."),
(
"doc",
doc_key,
doc,
json!({"kind":"human"}),
mounts,
instruction,
),
] {
let folder = root.join("repo").join(name);
fs::create_dir_all(&folder).unwrap();
fs::write(folder.join("key"), key).unwrap();
let declaration = json!({"name":name,"mounts":mounts,"fingerprint":fingerprint,"evals":[{
"id":"check","title":"Check","profile":profile,"payload":{"instruction":instruction},
"passSchema":{"type":"object","properties":{"approved":{"const":true}},"required":["approved"],"additionalProperties":false}
}]});
fs::write(folder.join("artifactize.json"), declaration.to_string()).unwrap();
}
fs::create_dir_all(root.join("state")).unwrap();
let database = root.join("state/state.sqlite");
Connection::open(&database)
.unwrap()
.execute_batch(&support::recorded_state(recorded.sql, &root))
.unwrap();
let current = artifactize::store::STATE_SCHEMA_VERSION;
assert_eq!(current, 4);
let recorded_bytes = fs::read(&database).unwrap();
let schema = doctor(&root);
assert_eq!(schema["status"], "PASS", "{schema}");
assert_eq!(
schema["details"],
json!({"schema":recorded.version,"upgradeTo":current})
);
assert_eq!(fs::read(&database).unwrap(), recorded_bytes);
assert_eq!(json(&root, &["cache", "list"], 0), json!([]));
assert_eq!(doctor(&root)["details"], json!({"schema":current}));
let db = Connection::open(&database).unwrap();
assert_eq!(
db.pragma_query_value::<u32, _>(None, "user_version", |row| row.get(0))
.unwrap(),
current
);
let columns = |table: &str| {
names(
&db,
&format!("SELECT name FROM pragma_table_info('{table}')"),
)
};
assert!(columns("executions").contains(&"key".to_owned()));
assert!(!columns("executions").contains(&"fingerprint".to_owned()));
assert!(!columns("executions").contains(&"eval_def_hash".to_owned()));
assert!(columns("cache_entries").contains(&"key".to_owned()));
let indexes = names(
&db,
"SELECT name FROM sqlite_master WHERE type='index' AND sql IS NOT NULL",
);
assert!(indexes.contains(&"active_key".to_owned()), "{indexes:?}");
let schema = names(&db, "SELECT sql FROM sqlite_master WHERE sql IS NOT NULL").join("\n");
assert!(
!schema.contains("ident") && !schema.contains("stale") && !schema.contains("fingerprint"),
"{schema}"
);
let saved = names(
&db,
"SELECT data FROM executions UNION ALL SELECT data FROM requests UNION ALL SELECT data FROM runs",
)
.join("\n");
assert!(
!saved.contains("\"identity\"") && !saved.contains("staleKey"),
"{saved}"
);
assert_eq!(
names(&db, "SELECT CAST(count(*) AS TEXT) FROM executions"),
["3"]
);
let saved = json(&root, &["run", "show", recorded.run], 0);
assert_eq!(saved["requests"][0]["fingerprint"], app_key);
assert_eq!(saved["requests"][0]["status"], "GREEN");
let validation = &saved["validation"]["artifacts"];
assert_eq!(validation[0]["fingerprint"], app_key);
assert_eq!(validation[0]["fingerprintKind"], "script");
assert_eq!(validation[2]["fingerprintKind"], "content");
let definitions = &saved["definitions"]["artifacts"];
assert_eq!(definitions["app"]["fingerprint"], recorded.definitions.0);
assert_eq!(definitions["notes"]["fingerprint"], recorded.definitions.1);
let status = json(&root, &["status", "--all"], 1);
let counts = &status["counts"];
assert_eq!(counts["reuse"], 0, "{status}");
assert_eq!(
counts["execute"].as_u64().unwrap() + counts["wait"].as_u64().unwrap(),
3,
"{status}"
);
json(&root, &["request", "claim", recorded.waiting], 0);
let submitted = json(
&root,
&[
"request",
"submit",
recorded.waiting,
"--verdict",
"GREEN",
"--fields",
r#"{"approved":true}"#,
],
0,
);
assert_eq!(submitted["status"], "GREEN");
assert_eq!(json(&root, &["cache", "list"], 0), json!([]));
let next = json(&root, &["verify", "--all"], 4);
assert_eq!(next["executionsStarted"], 2, "{next}");
let again = json(&root, &["verify", "--all"], 4);
assert_eq!(again["executionsStarted"], 0, "{again}");
for request in again["requests"].as_array().unwrap() {
if request["evalId"] == "doc/check" {
assert_eq!(request["status"], "WAITING_HUMAN");
} else {
assert_eq!(request["provenance"]["runId"], next["id"], "{request}");
}
}
}
#[test]
fn version_one_state_upgrades_and_reviews_again_once() {
let script = json!({"script":{"command":"cat","args":["key"]}});
upgrades_keeps_history_and_reviews_again_once(Recorded {
sql: VERSION_ONE,
version: 1,
run: "run-3aNTur",
waiting: "run-3aNTur-2",
app: ("app-v1", script.clone()),
notes: ("notes-v1", json!({})),
doc: ("doc-v1", script, json!({}), "Check."),
definitions: (
json!({"script":{"command":"cat","args":["key"],"files":[],"timeoutMs":null}}),
json!({"files":["."],"ignore":[]}),
),
});
}
#[test]
fn version_two_state_upgrades_and_reviews_again_once() {
let app = json!({"script":{"command":"cat","args":["key"],"files":["key"],"timeoutMs":5000}});
let notes = json!({"files":["key"],"ignore":["*.log"]});
upgrades_keeps_history_and_reviews_again_once(Recorded {
sql: VERSION_TWO,
version: 2,
run: "run-CTxYkO",
waiting: "run-CTxYkO-2",
app: ("app-v2", app.clone()),
notes: ("notes-v2", notes.clone()),
doc: (
"doc-v2",
json!({"script":{"command":"cat","args":["key"]}}),
json!({"notes":"notes"}),
"Check {notes}.",
),
definitions: (app, notes),
});
}
#[test]
fn version_three_state_upgrades_and_reviews_again_once() {
let app = json!({"script":{"command":"cat","args":["key"],"files":["key"],"timeoutMs":5000}});
let notes = json!({"files":["key"],"ignore":["*.log"]});
upgrades_keeps_history_and_reviews_again_once(Recorded {
sql: VERSION_THREE,
version: 3,
run: "run-9c3gzy",
waiting: "run-9c3gzy-2",
app: ("app-v3", app.clone()),
notes: ("notes-v3", notes.clone()),
doc: (
"doc-v3",
json!({"script":{"command":"cat","args":["key"]}}),
json!({"notes":"notes"}),
"Check {notes}.",
),
definitions: (app, notes),
});
}