Files
AILang/crates/ailang-check/tests/workspace.rs
T
Brummel 1a448309fa Iter 6: deps hardening, multi-diagnose, DESIGN audit
- ail deps now filters builtins, fn params, and let/match-pattern
  bindings. New value_names() in ailang-check::builtins is the single
  source of truth shared with the typechecker install path. walk_term
  threads a scope set; qualified `prefix.def` refs pass through.
- check_in_workspace returns Vec<CheckError>; check_workspace
  accumulates body diagnostics across defs and modules. Pass-1
  (top-level symbol table) and per-module type-def setup stay
  fail-fast — corrupt env would taint later diagnostics.
- DESIGN.md "What the MVP is NOT" was lying (ADTs, strings landed
  in Iter 2/3). Renamed to "What is not (yet) supported" and split
  into "not yet" + supported/smoke-tested. JOURNAL Iter 6 entry
  records the architecture self-check.

Tests: 47 green (was 44). +2 deps filter tests in e2e,
+1 multi-diagnose test in ailang-check workspace integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:31:20 +02:00

171 lines
5.5 KiB
Rust

//! Integration tests for `check_workspace` (Iter 5b).
//!
//! These tests drive the canonical `examples/ws_*.ail.json` files;
//! the loader and the checker together form the pipeline that `ail check`
//! runs in JSON mode against a workspace.
use ailang_check::{check_workspace, Severity};
use ailang_core::load_workspace;
use std::path::Path;
fn examples_dir() -> std::path::PathBuf {
let manifest = env!("CARGO_MANIFEST_DIR");
Path::new(manifest).parent().unwrap().parent().unwrap().join("examples")
}
#[test]
fn happy_path_resolves_qualified_import() {
// ws_main imports ws_lib and calls `ws_lib.add` — fully typed.
// Expected: no diagnostics.
let entry = examples_dir().join("ws_main.ail.json");
let ws = load_workspace(&entry).expect("load ws_main");
let diags = check_workspace(&ws);
assert!(
diags.is_empty(),
"expected no diagnostics; got: {}",
serde_json::to_string_pretty(&diags).unwrap()
);
}
#[test]
fn unknown_import_is_reported() {
// ws_broken references `ws_lib.bogus` — module is there, def isn't.
let entry = examples_dir().join("ws_broken.ail.json");
let ws = load_workspace(&entry).expect("load ws_broken");
let diags = check_workspace(&ws);
assert_eq!(diags.len(), 1, "got: {:?}", diags);
assert!(matches!(diags[0].severity, Severity::Error));
assert_eq!(diags[0].code, "unknown-import");
// Context must point structurally to module + def name.
assert_eq!(
diags[0].ctx.get("module").and_then(|v| v.as_str()),
Some("ws_lib")
);
assert_eq!(
diags[0].ctx.get("name").and_then(|v| v.as_str()),
Some("bogus")
);
}
#[test]
fn unknown_module_prefix_is_reported() {
// ws_unknown_module has no imports but references `nope.x`.
let entry = examples_dir().join("ws_unknown_module.ail.json");
let ws = load_workspace(&entry).expect("load ws_unknown_module");
let diags = check_workspace(&ws);
assert_eq!(diags.len(), 1, "got: {:?}", diags);
assert!(matches!(diags[0].severity, Severity::Error));
assert_eq!(diags[0].code, "unknown-module");
assert_eq!(
diags[0].ctx.get("module").and_then(|v| v.as_str()),
Some("nope")
);
}
#[test]
fn invalid_def_name_with_dot_is_reported() {
// Synthetic: a module with a def whose name contains a dot.
// We construct this as a Module directly and feed it into a
// trivial workspace, because the canonical convention should not
// let this through to disk in the first place.
use ailang_core::ast::*;
use std::collections::BTreeMap;
let m = Module {
schema: ailang_core::SCHEMA.into(),
name: "t".into(),
imports: vec![],
defs: vec![Def::Const(ConstDef {
name: "weird.name".into(),
ty: Type::int(),
value: Term::Lit {
lit: Literal::Int { value: 0 },
},
doc: None,
})],
};
let mut modules = BTreeMap::new();
modules.insert(m.name.clone(), m.clone());
let ws = ailang_core::Workspace {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
};
let diags = check_workspace(&ws);
assert_eq!(diags.len(), 1, "got: {:?}", diags);
assert_eq!(diags[0].code, "invalid-def-name");
assert_eq!(
diags[0].ctx.get("reason").and_then(|v| v.as_str()),
Some("contains-dot")
);
}
/// Iter 6: body-check is multi-diagnose. A module with two independent
/// errors in different defs must produce two diagnostics — the first
/// error must not short-circuit the second.
#[test]
fn body_errors_accumulate_across_defs() {
use ailang_core::ast::*;
use std::collections::BTreeMap;
// Two fns, each with a different body error:
// bad_a: arity mismatch — calls `+` with three arguments.
// bad_b: references a name that does not exist anywhere.
let bad_a = Def::Fn(FnDef {
name: "bad_a".into(),
ty: Type::Fn {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
},
params: vec![],
body: Term::App {
callee: Box::new(Term::Var { name: "+".into() }),
args: vec![
Term::Lit { lit: Literal::Int { value: 1 } },
Term::Lit { lit: Literal::Int { value: 2 } },
Term::Lit { lit: Literal::Int { value: 3 } },
],
},
doc: None,
});
let bad_b = Def::Fn(FnDef {
name: "bad_b".into(),
ty: Type::Fn {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
},
params: vec![],
body: Term::Var {
name: "this_does_not_exist".into(),
},
doc: None,
});
let m = Module {
schema: ailang_core::SCHEMA.into(),
name: "two_errors".into(),
imports: vec![],
defs: vec![bad_a, bad_b],
};
let mut modules = BTreeMap::new();
modules.insert(m.name.clone(), m.clone());
let ws = ailang_core::Workspace {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
};
let diags = check_workspace(&ws);
assert_eq!(diags.len(), 2, "want both errors, got: {:#?}", diags);
// Each diagnostic carries the offending def name as `def`.
let defs: Vec<&str> = diags
.iter()
.filter_map(|d| d.def.as_deref())
.collect();
assert!(defs.contains(&"bad_a"), "missing bad_a; got {defs:?}");
assert!(defs.contains(&"bad_b"), "missing bad_b; got {defs:?}");
}