From acee4f25c7247b94f61bfa6ed3960b3a5bfec825 Mon Sep 17 00:00:00 2001 From: HexLab98 Date: Sun, 19 Jul 2026 13:45:07 +0700 Subject: [PATCH] test(bootstrap): cover CP1252 stderr decode and UTF-8 BOM cache writes Locks the #67193 invariants: localized PowerShell error bytes survive decode_console_bytes / read_decoded_line (including CP1252-only 0x91/0x92 punctuation), cached .ps1 files get a single UTF-8 BOM for Windows PowerShell 5.1 -File, .sh stays BOM-less, and mutable branch caches plan a refresh with stale-cache fallback. --- .../src-tauri/src/install_script.rs | 54 +++++++++++++++++++ .../src-tauri/src/powershell.rs | 52 ++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/apps/bootstrap-installer/src-tauri/src/install_script.rs b/apps/bootstrap-installer/src-tauri/src/install_script.rs index c37a7dfdc647..f1cf6c9920eb 100644 --- a/apps/bootstrap-installer/src-tauri/src/install_script.rs +++ b/apps/bootstrap-installer/src-tauri/src/install_script.rs @@ -349,4 +349,58 @@ mod tests { assert_eq!(sanitize_ref("main"), "main"); assert_eq!(sanitize_ref("release/1.2.3"), "release_1.2.3"); } + + #[test] + fn prepare_cached_ps1_prefixes_utf8_bom() { + let out = prepare_cached_script_bytes(ScriptKind::Ps1, b"Write-Host hi\n"); + assert!(out.starts_with(UTF8_BOM), "cached .ps1 must start with UTF-8 BOM"); + assert_eq!(&out[UTF8_BOM.len()..], b"Write-Host hi\n"); + } + + #[test] + fn prepare_cached_ps1_does_not_double_bom() { + let mut already = UTF8_BOM.to_vec(); + already.extend_from_slice(b"x"); + let out = prepare_cached_script_bytes(ScriptKind::Ps1, &already); + assert_eq!(out, already); + assert_eq!(out.windows(3).filter(|w| *w == UTF8_BOM).count(), 1); + } + + #[test] + fn prepare_cached_sh_stays_bomless() { + let out = prepare_cached_script_bytes(ScriptKind::Sh, b"#!/bin/bash\n"); + assert!(!out.starts_with(UTF8_BOM)); + assert_eq!(out, b"#!/bin/bash\n"); + } + + #[test] + fn commit_pins_are_immutable_branch_pins_are_not() { + // Mirrors the resolve() immutable decision: SHA pins may reuse cache + // forever; branch pins must refresh so Retry cannot keep a bad script. + assert!(is_valid_commit("02d26981d3d4ad50e142399b8476f59ad5953ff0")); + assert!(!is_valid_commit("main")); + assert!(!is_valid_commit("release/1.2.3")); + } + + #[test] + fn existing_branch_cache_plans_refresh_with_stale_fallback() { + // Resolver-level: a prior install-main.ps1 must not short-circuit + // Retry — mutable pins refresh, and only fall back if download fails. + assert_eq!( + cache_plan(/*immutable=*/ false, /*cached_exists=*/ true), + CachePlan::Fetch { stale_ok: true } + ); + assert_eq!( + cache_plan(/*immutable=*/ true, /*cached_exists=*/ true), + CachePlan::Reuse + ); + assert_eq!( + cache_plan(/*immutable=*/ false, /*cached_exists=*/ false), + CachePlan::Fetch { stale_ok: false } + ); + assert_eq!( + cache_plan(/*immutable=*/ true, /*cached_exists=*/ false), + CachePlan::Fetch { stale_ok: false } + ); + } } diff --git a/apps/bootstrap-installer/src-tauri/src/powershell.rs b/apps/bootstrap-installer/src-tauri/src/powershell.rs index 62e9512ecca9..3683d6c9a3eb 100644 --- a/apps/bootstrap-installer/src-tauri/src/powershell.rs +++ b/apps/bootstrap-installer/src-tauri/src/powershell.rs @@ -447,4 +447,56 @@ info line "unexpected powershell path: {normalized}" ); } + + #[test] + fn decode_console_bytes_keeps_valid_utf8() { + assert_eq!(decode_console_bytes("café — ok".as_bytes()), "café — ok"); + } + + #[test] + fn decode_console_bytes_preserves_cp1252_portuguese_error() { + // "Não foi fornecido o terminador..." as Windows PowerShell 5.1 emits + // under CP1252 (0xE3 = ã). BufReader::lines() previously failed here + // with "stream did not contain valid UTF-8" and the UI only showed "No". + let bytes: &[u8] = b"N\xE3o foi fornecido o terminador"; + assert_eq!(decode_console_bytes(bytes), "Não foi fornecido o terminador"); + } + + #[test] + fn decode_console_bytes_maps_cp1252_only_punctuation() { + // 0x91/0x92 are curly quotes in Windows-1252, but C1 controls under + // Latin-1 (`b as char`). This locks the real CP1252 fallback. + let bytes: &[u8] = b"say \x91hi\x92"; + assert_eq!(decode_console_bytes(bytes), "say \u{2018}hi\u{2019}"); + assert_ne!( + decode_console_bytes(bytes), + bytes.iter().map(|&b| b as char).collect::(), + "Latin-1 byte mapping must not be used for the 0x80..=0x9F range" + ); + } + + #[tokio::test] + async fn read_decoded_line_survives_non_utf8_and_crlf() { + let data: &[u8] = b"N\xE3o erro\r\nnext\n"; + let mut reader = BufReader::new(data); + let mut buf = Vec::new(); + assert_eq!( + read_decoded_line(&mut reader, &mut buf) + .await + .unwrap() + .as_deref(), + Some("Não erro") + ); + assert_eq!( + read_decoded_line(&mut reader, &mut buf) + .await + .unwrap() + .as_deref(), + Some("next") + ); + assert!(read_decoded_line(&mut reader, &mut buf) + .await + .unwrap() + .is_none()); + } }