//! 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:?}"); }