iter ct.1.4: dev-only ail migrate-canonical-types subcommand

This commit is contained in:
2026-05-11 01:16:55 +02:00
parent 219908ed54
commit d45977f9bb
2 changed files with 597 additions and 0 deletions
@@ -0,0 +1,83 @@
//! ct.1: E2E test for `ail migrate-canonical-types <dir>`.
//!
//! Builds a synthetic workspace in a tempdir with one fixture that
//! has a bare cross-module Type::Con ref, runs the migration, then
//! asserts: (a) the rewritten fixture's `Term::Ctor.type_name` is
//! now qualified; (b) running migration again produces no diff
//! (idempotence).
//!
//! Property protected: `ail migrate-canonical-types` rewrites bare
//! cross-module Type::Con / Term::Ctor.type_name refs to their
//! qualified form using first-match-in-imports-order, AND a second
//! run on the already-canonical workspace is a byte-stable no-op.
use std::fs;
use std::path::PathBuf;
use std::process::Command;
fn tmp_dir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!(
"ailang_migrate_test_{tag}_{}",
std::process::id()
));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
}
fn ail_bin() -> PathBuf {
// CARGO_BIN_EXE_<name> is set by cargo when building integration tests.
PathBuf::from(env!("CARGO_BIN_EXE_ail"))
}
#[test]
fn migrate_canonical_types_rewrites_bare_xmod_term_ctor() {
let dir = tmp_dir("rewrites_bare_xmod");
// Defining module: `other` declares `type Foo`.
let other_path = dir.join("other.ail.json");
fs::write(&other_path, serde_json::to_vec_pretty(&serde_json::json!({
"schema": "ailang/v0",
"name": "other",
"imports": [],
"defs": [{ "kind": "type", "name": "Foo", "ctors": [{ "name": "MkFoo", "fields": [] }] }],
})).unwrap()).unwrap();
// Consumer module: imports `other`, uses bare `Foo` Term::Ctor.
let main_path = dir.join("main.ail.json");
let bare_module = serde_json::json!({
"schema": "ailang/v0",
"name": "main",
"imports": [{ "module": "other" }],
"defs": [{
"kind": "fn", "name": "f",
"type": { "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, "effects": [] },
"params": [],
"body": { "t": "ctor", "type": "Foo", "ctor": "MkFoo", "args": [] }
}],
});
fs::write(&main_path, serde_json::to_vec_pretty(&bare_module).unwrap()).unwrap();
// Run migration.
let status = Command::new(ail_bin())
.args(["migrate-canonical-types", dir.to_str().unwrap()])
.status()
.expect("ail binary must launch");
assert!(status.success(), "migration exited non-zero");
// Inspect rewritten consumer.
let after: serde_json::Value = serde_json::from_slice(
&fs::read(&main_path).unwrap()
).unwrap();
let ctor_type = after["defs"][0]["body"]["type"].as_str().unwrap();
assert_eq!(ctor_type, "other.Foo",
"Term::Ctor.type must be qualified after migration");
// Idempotence: run migration again, capture bytes, compare.
let bytes_before = fs::read(&main_path).unwrap();
let status2 = Command::new(ail_bin())
.args(["migrate-canonical-types", dir.to_str().unwrap()])
.status()
.expect("ail binary must launch");
assert!(status2.success(), "second migration exited non-zero");
let bytes_after = fs::read(&main_path).unwrap();
assert_eq!(bytes_before, bytes_after, "migration must be idempotent");
}