Skip to content

Commit e5779f1

Browse files
committed
ci: Add Windows coverage for setup integration tests
Run the integration test workflow on windows-latest using cargo test while keeping the existing Nix path for Unix runners. Update setup integration harness environment variables and expected local DB path to use Windows app-data locations consistently.
1 parent 535651f commit e5779f1

4 files changed

Lines changed: 68 additions & 33 deletions

File tree

.github/workflows/cli-integration-tests.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,25 @@ jobs:
1212
runs-on: ${{ matrix.os }}
1313
strategy:
1414
matrix:
15-
os: [ubuntu-latest, macos-latest]
15+
os: [ubuntu-latest, macos-latest, windows-latest]
1616
timeout-minutes: 25
1717
steps:
1818
- name: Check out repository
1919
uses: actions/checkout@v6
2020

2121
- name: Install Nix
22+
if: matrix.os != 'windows-latest'
2223
uses: cachix/install-nix-action@v31
2324

2425
- name: Run CLI integration test app
26+
if: matrix.os != 'windows-latest'
2527
run: nix run .#cli-integration-tests
28+
29+
- name: Install Rust toolchain
30+
if: matrix.os == 'windows-latest'
31+
uses: dtolnay/rust-toolchain@stable
32+
33+
- name: Run setup integration tests on Windows
34+
if: matrix.os == 'windows-latest'
35+
working-directory: cli
36+
run: cargo test --test setup_integration

cli/src/services/local_db.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ pub(crate) fn resolve_state_data_root() -> Result<PathBuf> {
206206
if let Some(app_data) = std::env::var_os("APPDATA") {
207207
return Ok(PathBuf::from(app_data));
208208
}
209+
210+
return Ok(resolve_home_dir()?.join("AppData").join("Local"));
209211
}
210212

211213
#[cfg(target_os = "macos")]

cli/src/services/observability.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use std::fs::{File, OpenOptions};
22
use std::io::Write;
33
#[cfg(unix)]
44
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
5-
use std::path::{Path, PathBuf};
5+
#[cfg(unix)]
6+
use std::path::Path;
7+
use std::path::PathBuf;
68
use std::sync::{Arc, Mutex};
79

810
use anyhow::{anyhow, bail, Result};

cli/tests/setup_integration.rs

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ impl SetupIntegrationHarness {
130130
.current_dir(&self.repo_root)
131131
.env("XDG_STATE_HOME", &self.state_home)
132132
.env("HOME", &self.home_dir)
133+
.env("LOCALAPPDATA", &self.state_home)
134+
.env("APPDATA", &self.state_home)
133135
.env("GIT_CONFIG_GLOBAL", null_device_path())
134136
.env("GIT_CONFIG_NOSYSTEM", "1");
135137
command
@@ -289,10 +291,11 @@ fn setup_backup_suffix_collision() -> TestResult<()> {
289291

290292
let expected_backup = canonical_repo_root.join(".opencode.backup.2");
291293
assert!(
292-
second.stdout.contains(&format!(
293-
"backup: existing target moved to '{}'",
294-
expected_backup.display()
295-
)),
294+
candidate_path_strings(&expected_backup)
295+
.iter()
296+
.any(|candidate| second
297+
.stdout
298+
.contains(&format!("backup: existing target moved to '{candidate}'"))),
296299
"output should report backup to .backup.2 due to collision\nstdout:\n{}",
297300
second.stdout
298301
);
@@ -361,20 +364,14 @@ fn setup_hooks_repo_relative_path() -> TestResult<()> {
361364
);
362365

363366
assert!(
364-
result.stdout.contains(&format!(
365-
"Repository root: '{}'",
366-
canonical_repo_root.display()
367-
)),
367+
stdout_contains_path_line(&result.stdout, "Repository root", &canonical_repo_root),
368368
"output should contain canonical repository root '{}'\nstdout:\n{}",
369369
canonical_repo_root.display(),
370370
result.stdout
371371
);
372372

373373
assert!(
374-
result.stdout.contains(&format!(
375-
"Hooks directory: '{}'",
376-
expected_hooks_dir.display()
377-
)),
374+
stdout_contains_path_line(&result.stdout, "Hooks directory", &expected_hooks_dir),
378375
"output should contain hooks directory '{}'\nstdout:\n{}",
379376
expected_hooks_dir.display(),
380377
result.stdout
@@ -413,20 +410,14 @@ fn setup_hooks_repo_absolute_path() -> TestResult<()> {
413410
);
414411

415412
assert!(
416-
result.stdout.contains(&format!(
417-
"Repository root: '{}'",
418-
canonical_repo_root.display()
419-
)),
413+
stdout_contains_path_line(&result.stdout, "Repository root", &canonical_repo_root),
420414
"output should contain canonical repository root '{}'\nstdout:\n{}",
421415
canonical_repo_root.display(),
422416
result.stdout
423417
);
424418

425419
assert!(
426-
result.stdout.contains(&format!(
427-
"Hooks directory: '{}'",
428-
expected_hooks_dir.display()
429-
)),
420+
stdout_contains_path_line(&result.stdout, "Hooks directory", &expected_hooks_dir),
430421
"output should contain hooks directory '{}'\nstdout:\n{}",
431422
expected_hooks_dir.display(),
432423
result.stdout
@@ -920,6 +911,7 @@ mod pty_interactive {
920911
Ok(())
921912
}
922913

914+
#[cfg(not(target_os = "windows"))]
923915
#[test]
924916
fn setup_interactive_nontty_fail() -> TestResult<()> {
925917
let harness = SetupIntegrationHarness::new("sce-setup-nontty")?;
@@ -1292,10 +1284,12 @@ fn assert_setup_hooks_install_and_rerun(
12921284
first.stderr
12931285
);
12941286
assert!(first.stdout.contains("Hook setup completed successfully."));
1295-
assert!(first.stdout.contains(&format!(
1296-
"Hooks directory: '{}'",
1297-
expected_hooks_directory.display()
1298-
)));
1287+
assert!(
1288+
stdout_contains_path_line(&first.stdout, "Hooks directory", expected_hooks_directory),
1289+
"first setup run should include hooks directory '{}':\n{}",
1290+
expected_hooks_directory.display(),
1291+
first.stdout
1292+
);
12991293

13001294
for hook in REQUIRED_HOOK_NAMES {
13011295
let expected_hook_path = expected_hooks_directory.join(hook);
@@ -1326,10 +1320,12 @@ fn assert_setup_hooks_install_and_rerun(
13261320
second.stderr
13271321
);
13281322
assert!(second.stdout.contains("Hook setup completed successfully."));
1329-
assert!(second.stdout.contains(&format!(
1330-
"Hooks directory: '{}'",
1331-
expected_hooks_directory.display()
1332-
)));
1323+
assert!(
1324+
stdout_contains_path_line(&second.stdout, "Hooks directory", expected_hooks_directory),
1325+
"second setup run should include hooks directory '{}':\n{}",
1326+
expected_hooks_directory.display(),
1327+
second.stdout
1328+
);
13331329

13341330
for hook in REQUIRED_HOOK_NAMES {
13351331
let expected_hook_path = expected_hooks_directory.join(hook);
@@ -1448,9 +1444,7 @@ fn expected_agent_trace_local_db_path(harness: &SetupIntegrationHarness) -> Path
14481444
#[cfg(target_os = "windows")]
14491445
{
14501446
harness
1451-
.home_dir
1452-
.join("AppData")
1453-
.join("Local")
1447+
.state_home
14541448
.join("sce")
14551449
.join("agent-trace")
14561450
.join("local.db")
@@ -1477,6 +1471,32 @@ fn expected_agent_trace_local_db_path(harness: &SetupIntegrationHarness) -> Path
14771471
}
14781472
}
14791473

1474+
fn stdout_contains_path_line(stdout: &str, label: &str, path: &Path) -> bool {
1475+
candidate_path_strings(path).into_iter().any(|candidate| {
1476+
stdout.contains(&format!("{label}: '{candidate}'"))
1477+
|| stdout.contains(&format!("{label}: \"{candidate}\""))
1478+
})
1479+
}
1480+
1481+
fn candidate_path_strings(path: &Path) -> Vec<String> {
1482+
let raw = path.display().to_string();
1483+
let slash = raw.replace('\\', "/");
1484+
let raw_trimmed = trim_windows_verbatim_prefix(&raw).to_string();
1485+
let slash_trimmed = trim_windows_verbatim_prefix(&slash).to_string();
1486+
1487+
let mut candidates = vec![raw, slash, raw_trimmed, slash_trimmed];
1488+
candidates.sort();
1489+
candidates.dedup();
1490+
candidates
1491+
}
1492+
1493+
fn trim_windows_verbatim_prefix(value: &str) -> &str {
1494+
value
1495+
.strip_prefix(r"\\?\")
1496+
.or_else(|| value.strip_prefix("//?/"))
1497+
.unwrap_or(value)
1498+
}
1499+
14801500
fn null_device_path() -> &'static str {
14811501
#[cfg(windows)]
14821502
{

0 commit comments

Comments
 (0)