use lex_ast::canonicalize_program;
use lex_store::{Store, StoreError, DEFAULT_BRANCH};
use lex_syntax::parse_source;
use lex_vcs::{AttestationKind, AttestationResult, DiffReport, ImportMap};
fn fresh() -> (Store, tempfile::TempDir) {
let tmp = tempfile::tempdir().unwrap();
let s = Store::open(tmp.path()).unwrap();
(s, tmp)
}
fn parse(src: &str) -> Vec<lex_ast::Stage> {
let prog = parse_source(src).expect("parse");
canonicalize_program(&prog)
}
#[test]
fn publish_program_rejects_program_with_unknown_identifier() {
let (store, _tmp) = fresh();
let stages = parse("fn broken(x :: Int) -> Int { not_defined(x) }\n");
let diff = DiffReport::default();
let imports = ImportMap::default();
let err = store
.publish_program(DEFAULT_BRANCH, &stages, &diff, &imports, true)
.expect_err("expected TypeError");
match err {
StoreError::TypeError(errs) => {
assert!(!errs.is_empty(), "expected at least one TypeError");
}
other => panic!("expected StoreError::TypeError, got {other:?}"),
}
assert!(
store.get_branch(DEFAULT_BRANCH).unwrap().is_none(),
"branch should not have been created on the rejection path",
);
let ops_dir = store.root().join("ops");
if ops_dir.exists() {
let count = std::fs::read_dir(&ops_dir).unwrap().count();
assert_eq!(count, 0, "no op records should be persisted on TypeError");
}
}
#[test]
fn publish_program_rejects_program_with_arity_mismatch() {
let (store, _tmp) = fresh();
let stages = parse(
"fn add(x :: Int, y :: Int) -> Int { x + y }\nfn caller() -> Int { add(1) }\n",
);
let err = store
.publish_program(
DEFAULT_BRANCH,
&stages,
&DiffReport::default(),
&ImportMap::default(),
true,
)
.expect_err("expected TypeError");
assert!(matches!(err, StoreError::TypeError(_)));
}
#[test]
fn publish_program_accepts_clean_program() {
let (store, _tmp) = fresh();
let stages = parse(
"fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n",
);
let outcome = store
.publish_program(
DEFAULT_BRANCH,
&stages,
&DiffReport::default(),
&ImportMap::default(),
true,
)
.expect("clean program should pass the gate");
assert!(outcome.ops.is_empty() || !outcome.ops.is_empty());
}
#[test]
fn publish_program_emits_typecheck_attestation_per_added_op() {
let (store, _tmp) = fresh();
let stages = parse(
"fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n",
);
let diff = lex_vcs::DiffReport {
added: vec![lex_vcs::diff_report::AddRemove {
name: "factorial".into(),
signature: "fn factorial(Int) -> Int".into(),
}],
..Default::default()
};
let outcome = store
.publish_program(DEFAULT_BRANCH, &stages, &diff, &ImportMap::default(), true)
.expect("clean program should pass the gate");
assert_eq!(outcome.ops.len(), 1, "exactly one AddFunction op expected");
let log = store.attestation_log().unwrap();
let stage_id = lex_ast::stage_id(&stages[0]).unwrap();
let listing = log.list_for_stage(&stage_id).unwrap();
assert_eq!(listing.len(), 1, "one TypeCheck attestation per produced stage");
let att = &listing[0];
assert!(matches!(att.kind, AttestationKind::TypeCheck));
assert!(matches!(att.result, AttestationResult::Passed));
assert_eq!(att.op_id.as_ref(), Some(&outcome.ops[0].op_id));
assert_eq!(att.produced_by.tool, "lex-store");
}