iter cli-diag-human: route WorkspaceLoadError through diagnostic in non-JSON path
Closes P2 todo "CLI human-mode diagnostic surface for
WorkspaceLoadError" surfaced by ct.1.8 tester (2026-05-11).
Pre-iter, non-JSON `ail check` (+8 sibling subcommands) propagated
loader errors via anyhow's Display, producing
`Error: <message>` without the bracketed `[code]` prefix the JSON
path emits.
Fix: new private helper `load_workspace_human(path)` in
crates/ail/src/main.rs that wraps ailang_surface::load_workspace,
threads any WorkspaceLoadError through workspace_error_to_diagnostic,
formats the resulting Diagnostic into a single stderr line
`error: [<code>] <def>: <message>`, and exits non-zero. Pure I/O
errors still propagate as anyhow errors.
Nine call sites swapped to the helper (check non-JSON arm, build,
run, emit-ir, prose, describe, deps, diff, manifest). The JSON arm
of `ail check` keeps its explicit `match` — different output
contract (structured stdout array).
The BareCrossModuleTypeRef Diagnostic message gained the existing
migration-command hint ("Run `ail migrate-canonical-types` to fix
legacy fixtures.") so the ct.1.8 actionable-hint test stays green.
The hint previously rode on WorkspaceLoadError's thiserror Display;
making workspace_error_to_diagnostic the single source for both
JSON and human paths required moving it into the Diagnostic body.
RED test crates/ail/tests/cli_diag_human_workspace_load_error.rs
pins the bracketed-code surface on a missing-import workspace
(tempdir-based fixture; no committed corpus addition).
cargo test --workspace: 565/0/3. clippy + doc: zero warnings.
Roadmap entry struck.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
//! Iter cli-diag-human (2026-05-14): non-JSON `ail check` and
|
||||
//! sibling subcommands surface `WorkspaceLoadError` with the same
|
||||
//! bracketed `[code]` prefix that the JSON path emits.
|
||||
//!
|
||||
//! Before this iter, non-JSON CLI subcommands that called
|
||||
//! `ailang_surface::load_workspace(&path)?` propagated the error
|
||||
//! through `anyhow`'s Display, producing `Error: <message>` —
|
||||
//! missing the bracketed diagnostic code that `--json` consumers
|
||||
//! and humans alike use to triage. Fix: a `load_workspace_human`
|
||||
//! helper routes through `workspace_error_to_diagnostic` and
|
||||
//! prints `error: [<code>] <def>: <message>` before exiting.
|
||||
//!
|
||||
//! These tests pin the bracketed-code surface on a representative
|
||||
//! workspace-load error (`module-not-found`). They are tempdir-
|
||||
//! based: the fixture is authored at test time and removed at
|
||||
//! drop, so no committed fixture is needed (the surface under
|
||||
//! test is the CLI formatting, not a permanent corpus entry).
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn ail_bin() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_BIN_EXE_ail"))
|
||||
}
|
||||
|
||||
/// Build a fresh tempdir containing one .ail module that imports a
|
||||
/// non-existent sibling module. Returns the entry path. The tempdir
|
||||
/// is leaked deliberately — tests are single-shot and tempdir
|
||||
/// cleanup is the test harness's concern.
|
||||
fn tempdir_with_missing_import() -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"ail_cli_diag_human_{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("create tempdir");
|
||||
let entry = dir.join("test_missing_import.ail");
|
||||
let mut f = std::fs::File::create(&entry).expect("create entry .ail");
|
||||
writeln!(
|
||||
f,
|
||||
"(module test_missing_import\n (import this_module_does_not_exist)\n (fn main\n (type (fn-type (params) (ret (con Unit)) (effects IO)))\n (params)\n (body (do io/print_str \"hello\"))))"
|
||||
).expect("write fixture");
|
||||
entry
|
||||
}
|
||||
|
||||
/// Property: `ail check <fixture>` (non-JSON path) on a workspace
|
||||
/// with a missing import emits a stderr line of shape
|
||||
/// `error: [module-not-found] ...` and exits non-zero. Pre-iter
|
||||
/// behaviour was `Error: module ...` without the bracketed code;
|
||||
/// this is the regression guard against re-introducing that.
|
||||
#[test]
|
||||
fn check_non_json_emits_bracketed_module_not_found() {
|
||||
let fixture = tempdir_with_missing_import();
|
||||
let output = Command::new(ail_bin())
|
||||
.args(["check", fixture.to_str().unwrap()])
|
||||
.output()
|
||||
.expect("ail binary must launch");
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"ail check must exit non-zero on missing import; stderr={}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("[module-not-found]"),
|
||||
"expected `[module-not-found]` in stderr, got: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
stderr.starts_with("error: "),
|
||||
"expected stderr to start with `error: ` (lowercase, matching the JSON-mode wording), got: {stderr}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user