diff --git a/architecture/notes/scripts/ci/2026-09-24-platform-preflight.md b/architecture/notes/scripts/ci/2026-09-24-platform-preflight.md new file mode 100644 index 0000000..971cc5b --- /dev/null +++ b/architecture/notes/scripts/ci/2026-09-24-platform-preflight.md @@ -0,0 +1,62 @@ +# Check the platform budget before allocating native runners + +## Status + +Implemented for review. + +## Context + +The platform-condition budget ran only during packaging. A Windows-only WSL +regression introduced one additional test-level condition, leaving the source +count at 485 against a budget of 484. All ten PR checks passed because none ran +that budget checker. Preview preparation then failed before any package build. + +## Evidence + +- Preview run 35882296442 failed in `Check platform cfg budget` on main commit + `1d26c2dc9860a6cfb61c14b630c12d36dbf95e43`. +- The added condition guarded the WSL truecolor regression, not a new production + platform branch. Five related WSL forwarding tests repeated the same Windows + compilation condition. +- Sharing that condition at the WSL test-module boundary preserves all five test + bodies and lowers the repository count to 481 without moving source outside + the scanner's declared roots or adding an exclusion. + +## Decision + +- Keep the WSL forwarding regressions together under one Windows test module in + their existing owning source file. Other identity and PATH tests retain their + existing boundaries. Production environment behavior is unchanged. +- Tighten the existing budget from 484 to the measured count of 481. +- Execute the same authoritative platform checker in required lint before matrix + planning. Keep the packaging check as an independent release precondition. +- Run the checker's contract tests with the existing lightweight CI contracts. + +## Rejected alternatives + +Raising the budget, deleting the new regression, excluding test files from the +scanner, or ignoring the packaging failure would hide the mismatch. None is +needed. This change also does not turn the historical text-count heuristic into +a claim that architectural coupling has been proven absent. + +## Consequences + +The five WSL tests share one platform boundary and remain Windows-only. Budget +failures are visible before native runner allocation. Legitimate future scanner +policy changes still need their own reproducer, tests and review. + +## Validation + +The platform checker reports 481 against 481. Validation also compares the test +function inventory, runs the Windows identity/WSL regression group, and checks +that lint executes the gate before planning without automatic budget updates or +failure suppression. + +## Supersedes + +None. + +## Revisit when + +The text-based platform counter is replaced with a reviewed semantic contract, +or native runner planning is moved to another shared preflight owner. diff --git a/nebula_app/src/agent_env.rs b/nebula_app/src/agent_env.rs index 5714be4..a6788d8 100644 --- a/nebula_app/src/agent_env.rs +++ b/nebula_app/src/agent_env.rs @@ -304,77 +304,83 @@ mod tests { assert_eq!(entries.iter().filter(|entry| same_directory(entry, directory)).count(), 1); } + // WSL forwarding has one Windows compilation boundary for its whole contract. #[cfg(windows)] - #[test] - fn wslenv_entries_match_variables() { - // 透传表里的路径条目是字面量。变量改名而这里忘记跟着改,就会静默丢掉 - // WSL 侧的可达性——用一个断言把它变成编译期之后立刻可见的失败。 - let names: Vec<&str> = WSLENV_ENTRIES.iter().copied().map(variable_name).collect(); - assert!(names.contains(&CLI_ENV), "{CLI_ENV} missing from WSLENV passthrough: {names:?}"); - assert!(names.contains(&BIN_DIR_ENV), "{BIN_DIR_ENV} missing: {names:?}"); - assert!(names.contains(&PROCESS_ENV), "{PROCESS_ENV} missing: {names:?}"); - assert!(names.contains(&crate::runtime_api::ENDPOINT_ENV)); - assert!(names.contains(&"COLORTERM")); - } + mod wsl { + use super::*; - #[cfg(windows)] - #[test] - fn wsl_receives_truecolor_without_replacing_an_explicit_forwarding_rule() { - let mut env = HashMap::from([ - ("COLORTERM".to_owned(), "truecolor".to_owned()), - ("WSLENV".to_owned(), "KEEP/p".to_owned()), - ]); - apply(&mut env, 8); - assert_eq!(env["COLORTERM"], "truecolor"); - assert!(env["WSLENV"].split(':').any(|entry| entry == "COLORTERM")); - assert!(env["WSLENV"].split(':').any(|entry| entry == "KEEP/p")); - env.insert("WSLENV".into(), "KEEP/p:COLORTERM/u".into()); - env.insert("COLORTERM".into(), "24bit".into()); - apply(&mut env, 8); - assert_eq!(env["COLORTERM"], "24bit"); - let entries: Vec<_> = - env["WSLENV"].split(':').filter(|entry| variable_name(entry) == "COLORTERM").collect(); - assert_eq!(entries, ["COLORTERM/u"]); - } + #[test] + fn wslenv_entries_match_variables() { + // 透传表里的路径条目是字面量。变量改名而这里忘记跟着改,就会静默丢掉 + // WSL 侧的可达性——用一个断言把它变成编译期之后立刻可见的失败。 + let names: Vec<&str> = WSLENV_ENTRIES.iter().copied().map(variable_name).collect(); + assert!( + names.contains(&CLI_ENV), + "{CLI_ENV} missing from WSLENV passthrough: {names:?}" + ); + assert!(names.contains(&BIN_DIR_ENV), "{BIN_DIR_ENV} missing: {names:?}"); + assert!(names.contains(&PROCESS_ENV), "{PROCESS_ENV} missing: {names:?}"); + assert!(names.contains(&crate::runtime_api::ENDPOINT_ENV)); + assert!(names.contains(&"COLORTERM")); + } - #[cfg(windows)] - #[test] - fn wslenv_keeps_foreign_entries() { - let mut env = HashMap::new(); - // `shell_detect::wsl_cwd_report_env` 先写过 cwd 上报的透传条目。 - env.insert("WSLENV".to_owned(), "PROMPT_COMMAND".to_owned()); - apply(&mut env, 3); + #[test] + fn wsl_receives_truecolor_without_replacing_an_explicit_forwarding_rule() { + let mut env = HashMap::from([ + ("COLORTERM".to_owned(), "truecolor".to_owned()), + ("WSLENV".to_owned(), "KEEP/p".to_owned()), + ]); + apply(&mut env, 8); + assert_eq!(env["COLORTERM"], "truecolor"); + assert!(env["WSLENV"].split(':').any(|entry| entry == "COLORTERM")); + assert!(env["WSLENV"].split(':').any(|entry| entry == "KEEP/p")); + env.insert("WSLENV".into(), "KEEP/p:COLORTERM/u".into()); + env.insert("COLORTERM".into(), "24bit".into()); + apply(&mut env, 8); + assert_eq!(env["COLORTERM"], "24bit"); + let entries: Vec<_> = env["WSLENV"] + .split(':') + .filter(|entry| variable_name(entry) == "COLORTERM") + .collect(); + assert_eq!(entries, ["COLORTERM/u"]); + } - let wslenv = env.get("WSLENV").expect("WSLENV"); - let entries: Vec<&str> = wslenv.split(':').collect(); - assert!(entries.contains(&"PROMPT_COMMAND"), "cwd reporting must survive: {wslenv}"); - assert!(entries.contains(&PANE_ENV), "pane identity must cross into WSL: {wslenv}"); - // 路径必须带 `/p`,否则来宾拿到无法执行的 `D:\…` 字面量。 - assert!(entries.contains(&"NEBULA_CLI/p"), "cli path needs translation: {wslenv}"); - } + #[test] + fn wslenv_keeps_foreign_entries() { + let mut env = HashMap::new(); + // `shell_detect::wsl_cwd_report_env` 先写过 cwd 上报的透传条目。 + env.insert("WSLENV".to_owned(), "PROMPT_COMMAND".to_owned()); + apply(&mut env, 3); - #[cfg(windows)] - #[test] - fn wslenv_merge_is_idempotent() { - let mut env = HashMap::new(); - apply(&mut env, 4); - let once = env.get("WSLENV").cloned().expect("WSLENV"); - apply(&mut env, 4); - assert_eq!(env.get("WSLENV"), Some(&once)); - } + let wslenv = env.get("WSLENV").expect("WSLENV"); + let entries: Vec<&str> = wslenv.split(':').collect(); + assert!(entries.contains(&"PROMPT_COMMAND"), "cwd reporting must survive: {wslenv}"); + assert!(entries.contains(&PANE_ENV), "pane identity must cross into WSL: {wslenv}"); + // 路径必须带 `/p`,否则来宾拿到无法执行的 `D:\…` 字面量。 + assert!(entries.contains(&"NEBULA_CLI/p"), "cli path needs translation: {wslenv}"); + } - #[cfg(windows)] - #[test] - fn wslenv_respects_foreign_flags_for_same_variable() { - let mut env = HashMap::new(); - // 宿主已用 `/l`(列表)传同名变量:尊重它,不改成我们的 `/p`, - // 否则会破坏那个工具原有的转换语义。 - env.insert("WSLENV".to_owned(), "NEBULA_CLI/l".to_owned()); - apply(&mut env, 5); + #[test] + fn wslenv_merge_is_idempotent() { + let mut env = HashMap::new(); + apply(&mut env, 4); + let once = env.get("WSLENV").cloned().expect("WSLENV"); + apply(&mut env, 4); + assert_eq!(env.get("WSLENV"), Some(&once)); + } - let wslenv = env.get("WSLENV").expect("WSLENV"); - let entries: Vec<&str> = wslenv.split(':').collect(); - assert!(entries.contains(&"NEBULA_CLI/l")); - assert!(!entries.contains(&"NEBULA_CLI/p")); + #[test] + fn wslenv_respects_foreign_flags_for_same_variable() { + let mut env = HashMap::new(); + // 宿主已用 `/l`(列表)传同名变量:尊重它,不改成我们的 `/p`, + // 否则会破坏那个工具原有的转换语义。 + env.insert("WSLENV".to_owned(), "NEBULA_CLI/l".to_owned()); + apply(&mut env, 5); + + let wslenv = env.get("WSLENV").expect("WSLENV"); + let entries: Vec<&str> = wslenv.split(':').collect(); + assert!(entries.contains(&"NEBULA_CLI/l")); + assert!(!entries.contains(&"NEBULA_CLI/p")); + } } } diff --git a/scripts/platform_cfg_budget.txt b/scripts/platform_cfg_budget.txt index f52aaac..ff5670f 100644 --- a/scripts/platform_cfg_budget.txt +++ b/scripts/platform_cfg_budget.txt @@ -1 +1 @@ -484 +481