use lex_ast::canonicalize_program;
use lex_store::{Operation, OperationKind, StageTransition, Store, StoreError, DEFAULT_BRANCH};
use lex_syntax::parse_source;
use lex_vcs::{AttestationKind, AttestationResult};
use std::collections::BTreeSet;
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)
}
fn add_fac_op() -> (Operation, StageTransition) {
let op = Operation::new(
OperationKind::AddFunction {
sig_id: "fac".into(),
stage_id: "stg-1".into(),
effects: BTreeSet::new(),
budget_cost: None,
},
[],
);
let t = StageTransition::Create {
sig_id: "fac".into(),
stage_id: "stg-1".into(),
};
(op, t)
}
#[test]
fn apply_operation_checked_advances_head_on_clean_candidate() {
let (s, _tmp) = fresh();
let candidate = parse(
"fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n",
);
let (op, t) = add_fac_op();
let op_id = s
.apply_operation_checked(DEFAULT_BRANCH, op, t, &candidate)
.expect("clean candidate should pass the gate");
let head = s.branch_head(DEFAULT_BRANCH).unwrap();
assert_eq!(head.get("fac"), Some(&"stg-1".to_string()));
let b = s.get_branch(DEFAULT_BRANCH).unwrap().unwrap();
assert_eq!(b.head_op.as_deref(), Some(op_id.as_str()));
}
#[test]
fn apply_operation_checked_rejects_type_broken_candidate() {
let (s, _tmp) = fresh();
let candidate = parse("fn broken(x :: Int) -> Int { not_defined(x) }\n");
let (op, t) = add_fac_op();
let err = s
.apply_operation_checked(DEFAULT_BRANCH, op, t, &candidate)
.expect_err("expected TypeError");
assert!(matches!(err, StoreError::TypeError(_)));
assert!(
s.get_branch(DEFAULT_BRANCH).unwrap().is_none(),
"branch should not have been created on the rejection path",
);
let ops_dir = s.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 apply_operation_checked_does_not_advance_head_after_rejection() {
let (s, _tmp) = fresh();
let initial_candidate = parse(
"fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n",
);
let (op1, t1) = add_fac_op();
let head1 = s
.apply_operation_checked(DEFAULT_BRANCH, op1, t1, &initial_candidate)
.expect("first op should land");
let bad_candidate = parse("fn broken(x :: Int) -> Int { not_defined(x) }\n");
let op2 = Operation::new(
OperationKind::ModifyBody {
sig_id: "fac".into(),
from_stage_id: "stg-1".into(),
to_stage_id: "stg-2".into(),
from_budget: None,
to_budget: None,
},
[head1.clone()],
);
let t2 = StageTransition::Replace {
sig_id: "fac".into(),
from: "stg-1".into(),
to: "stg-2".into(),
};
let err = s
.apply_operation_checked(DEFAULT_BRANCH, op2, t2, &bad_candidate)
.expect_err("expected TypeError");
assert!(matches!(err, StoreError::TypeError(_)));
let b_after = s.get_branch(DEFAULT_BRANCH).unwrap().unwrap();
assert_eq!(
b_after.head_op.as_deref(),
Some(head1.as_str()),
"branch head should still point at head1 after rejected op",
);
}
#[test]
fn apply_operation_checked_emits_typecheck_attestation_for_created_stage() {
let (s, _tmp) = fresh();
let candidate = parse(
"fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n",
);
let (op, t) = add_fac_op();
let op_id = s
.apply_operation_checked(DEFAULT_BRANCH, op, t, &candidate)
.expect("clean candidate should pass the gate");
let log = s.attestation_log().unwrap();
let listing = log.list_for_stage(&"stg-1".to_string()).unwrap();
assert_eq!(listing.len(), 1, "exactly one attestation for the created stage");
let att = &listing[0];
assert!(matches!(att.kind, AttestationKind::TypeCheck));
assert!(matches!(att.result, AttestationResult::Passed));
assert_eq!(att.op_id.as_deref(), Some(op_id.as_str()));
assert_eq!(att.stage_id, "stg-1");
assert_eq!(att.produced_by.tool, "lex-store");
}
#[test]
fn apply_operation_checked_does_not_emit_typecheck_attestation_on_rejection() {
let (s, _tmp) = fresh();
let candidate = parse("fn broken(x :: Int) -> Int { not_defined(x) }\n");
let (op, t) = add_fac_op();
let _ = s
.apply_operation_checked(DEFAULT_BRANCH, op, t, &candidate)
.expect_err("expected TypeError");
let attlog = s.attestation_log().unwrap();
let all = attlog.list_all().unwrap();
let n_typecheck = all.iter()
.filter(|a| matches!(a.kind, lex_vcs::AttestationKind::TypeCheck))
.count();
assert_eq!(n_typecheck, 0,
"no TypeCheck attestation should be written on rejection");
let n_hint = all.iter()
.filter(|a| matches!(a.kind, lex_vcs::AttestationKind::RepairHint { .. }))
.count();
assert_eq!(n_hint, 1, "exactly one RepairHint should land (#281)");
}
#[test]
fn apply_operation_checked_attestation_dedups_across_runs() {
let (s, _tmp) = fresh();
let candidate = parse(
"fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n",
);
let (op, t) = add_fac_op();
let op_id = s
.apply_operation_checked(DEFAULT_BRANCH, op, t, &candidate)
.unwrap();
let log = s.attestation_log().unwrap();
let listing_a = log.list_for_stage(&"stg-1".to_string()).unwrap();
assert_eq!(listing_a.len(), 1);
let same = lex_vcs::Attestation::with_timestamp(
"stg-1".to_string(),
Some(op_id.clone()),
None,
AttestationKind::TypeCheck,
AttestationResult::Passed,
lex_vcs::ProducerDescriptor {
tool: "lex-store".into(),
version: env!("CARGO_PKG_VERSION").into(),
model: None,
},
None,
listing_a[0].timestamp + 100,
);
log.put(&same).unwrap();
let listing_b = log.list_for_stage(&"stg-1".to_string()).unwrap();
assert_eq!(listing_b.len(), 1, "content addressing dedups across re-puts");
}
#[test]
fn apply_operation_checked_propagates_apply_errors() {
let (s, _tmp) = fresh();
let candidate = parse(
"fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n",
);
let (op1, t1) = add_fac_op();
let _ = s
.apply_operation_checked(DEFAULT_BRANCH, op1, t1, &candidate)
.unwrap();
let bogus = Operation::new(
OperationKind::ModifyBody {
sig_id: "fac".into(),
from_stage_id: "stg-1".into(),
to_stage_id: "stg-2".into(),
from_budget: None,
to_budget: None,
},
["someone-else".into()],
);
let t = StageTransition::Replace {
sig_id: "fac".into(),
from: "stg-1".into(),
to: "stg-2".into(),
};
let err = s
.apply_operation_checked(DEFAULT_BRANCH, bogus, t, &candidate)
.expect_err("expected Apply(StaleParent)");
match err {
StoreError::Apply(_) => {}
other => panic!("expected StoreError::Apply, got {other:?}"),
}
}