Skip to content

Commit d09f482

Browse files
authored
feat(windows): Implement CI (#185)
* chore(windows): Add CI * feat(windows): fix remaining tests
1 parent bc6c10d commit d09f482

File tree

6 files changed

+139
-22
lines changed

6 files changed

+139
-22
lines changed

.gitattributes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.rs text eol=lf
2+
Cargo.toml text eol=lf
3+
Cargo.lock text eol=lf
4+
.gitignore text eol=lf

.github/workflows/rust.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,47 @@ jobs:
9090
- name: Run tests
9191
run: cargo test --locked --workspace --lib --bins --test '*' --exclude fig_desktop-fuzz
9292

93+
cargo-clippy-windows-chat-cli:
94+
name: Clippy Windows (chat_cli)
95+
runs-on: windows-latest
96+
timeout-minutes: 60
97+
steps:
98+
- uses: actions/checkout@v4
99+
- uses: dtolnay/[email protected]
100+
id: toolchain
101+
with:
102+
components: clippy
103+
- uses: actions/cache@v4
104+
with:
105+
path: |
106+
~/.cargo/registry/index/
107+
~/.cargo/registry/cache/
108+
~/.cargo/git/db/
109+
target/
110+
key: cargo-clippy-windows-chat-cli-${{ hashFiles('**/Cargo.lock') }}-${{ steps.toolchain.outputs.cachekey }}
111+
- run: cargo clippy --locked -p chat_cli --color always -- -D warnings
112+
113+
cargo-test-windows-chat-cli:
114+
name: Test Windows (chat_cli)
115+
runs-on: windows-latest
116+
timeout-minutes: 60
117+
steps:
118+
- uses: actions/checkout@v4
119+
- uses: dtolnay/rust-toolchain@nightly
120+
id: toolchain
121+
with:
122+
components: llvm-tools-preview
123+
- uses: actions/cache@v4
124+
with:
125+
path: |
126+
~/.cargo/registry/index/
127+
~/.cargo/registry/cache/
128+
~/.cargo/git/db/
129+
target/
130+
key: cargo-test-windows-chat-cli-${{ hashFiles('**/Cargo.lock') }}-${{ steps.toolchain.outputs.cachekey }}
131+
- name: Run tests
132+
run: cargo test --locked -p chat_cli
133+
93134
cargo-fmt:
94135
name: Fmt
95136
runs-on: ubuntu-latest

crates/chat-cli/src/cli/chat/tools/mod.rs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,8 @@ fn format_path(cwd: impl AsRef<Path>, path: impl AsRef<Path>) -> String {
359359
.map(|p| p.to_string_lossy().to_string())
360360
// If we have three consecutive ".." then it should probably just stay as an absolute path.
361361
.map(|p| {
362-
if p.starts_with("../../..") {
362+
let three_up = format!("..{}..{}..", std::path::MAIN_SEPARATOR, std::path::MAIN_SEPARATOR);
363+
if p.starts_with(&three_up) {
363364
path.as_ref().to_string_lossy().to_string()
364365
} else {
365366
p
@@ -376,6 +377,10 @@ fn supports_truecolor(ctx: &Context) -> bool {
376377

377378
#[cfg(test)]
378379
mod tests {
380+
use std::path::MAIN_SEPARATOR;
381+
382+
use chat_cli::platform::ACTIVE_USER_HOME;
383+
379384
use super::*;
380385
use crate::platform::EnvProvider;
381386

@@ -384,15 +389,12 @@ mod tests {
384389
let ctx = Context::builder().with_test_home().await.unwrap().build_fake();
385390

386391
let actual = sanitize_path_tool_arg(&ctx, "~");
387-
assert_eq!(
388-
actual,
389-
ctx.fs().chroot_path(ctx.env().home().unwrap()),
390-
"tilde should expand"
391-
);
392+
let expected_home = ctx.env().home().unwrap_or_default();
393+
assert_eq!(actual, ctx.fs().chroot_path(&expected_home), "tilde should expand");
392394
let actual = sanitize_path_tool_arg(&ctx, "~/hello");
393395
assert_eq!(
394396
actual,
395-
ctx.fs().chroot_path(ctx.env().home().unwrap().join("hello")),
397+
ctx.fs().chroot_path(expected_home.join("hello")),
396398
"tilde should expand"
397399
);
398400
let actual = sanitize_path_tool_arg(&ctx, "/~");
@@ -412,15 +414,39 @@ mod tests {
412414
let path = sanitize_path_tool_arg(&ctx, path);
413415
fs.create_dir_all(&cwd).await.unwrap();
414416
fs.create_dir_all(&path).await.unwrap();
415-
// Using `contains` since the chroot test directory will prefix the formatted path with a tmpdir
416-
// path.
417-
assert!(format_path(cwd, path).contains(expected));
417+
418+
let formatted = format_path(&cwd, &path);
419+
420+
if Path::new(expected).is_absolute() {
421+
// If the expected path is relative, we need to ensure it is relative to the cwd.
422+
let expected = fs.chroot_path_str(expected);
423+
424+
assert!(formatted == expected, "Expected '{}' to be '{}'", formatted, expected);
425+
426+
return;
427+
}
428+
429+
assert!(
430+
formatted.contains(expected),
431+
"Expected '{}' to be '{}'",
432+
formatted,
433+
expected
434+
);
418435
}
419-
assert_paths("/Users/testuser/src", "/Users/testuser/Downloads", "../Downloads").await;
436+
437+
// Test relative path from src to Downloads (sibling directories)
438+
assert_paths(
439+
format!("{ACTIVE_USER_HOME}{MAIN_SEPARATOR}src").as_str(),
440+
format!("{ACTIVE_USER_HOME}{MAIN_SEPARATOR}Downloads").as_str(),
441+
format!("..{MAIN_SEPARATOR}Downloads").as_str(),
442+
)
443+
.await;
444+
445+
// Test absolute path that should stay absolute (going up too many levels)
420446
assert_paths(
421-
"/Users/testuser/projects/MyProject/src",
422-
"/Volumes/projects/MyProject/src",
423-
"/Volumes/projects/MyProject/src",
447+
format!("{ACTIVE_USER_HOME}{MAIN_SEPARATOR}projects{MAIN_SEPARATOR}some{MAIN_SEPARATOR}project").as_str(),
448+
format!("{ACTIVE_USER_HOME}{MAIN_SEPARATOR}other").as_str(),
449+
format!("{ACTIVE_USER_HOME}{MAIN_SEPARATOR}other").as_str(),
424450
)
425451
.await;
426452
}

crates/chat-cli/src/platform/fs/windows.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,15 @@ pub(super) fn append(a: impl AsRef<Path>, b: impl AsRef<Path>) -> PathBuf {
3636
let b_normal_components: Vec<_> = cleaned_b.components().collect();
3737

3838
if b_normal_components.len() >= a_normal_components.len() {
39-
// Check if the beginning of b matches a
39+
// Check if the beginning of b matches a (case-insensitive on Windows)
4040
let matches = a_normal_components
4141
.iter()
4242
.zip(b_normal_components.iter())
43-
.all(|(a_comp, b_comp)| a_comp == b_comp);
43+
.all(|(a_comp, b_comp)| {
44+
// Case-insensitive comparison for Windows
45+
a_comp.as_os_str().to_string_lossy().to_lowercase()
46+
== b_comp.as_os_str().to_string_lossy().to_lowercase()
47+
});
4448

4549
if matches {
4650
// Create a new path with a's components removed from the beginning of b

crates/chat-cli/src/platform/mod.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ pub struct ContextBuilder {
8484
platform: Option<Platform>,
8585
}
8686

87+
pub const WINDOWS_USER_HOME: &str = "C:\\Users\\testuser";
88+
pub const UNIX_USER_HOME: &str = "/home/testuser";
89+
90+
pub const ACTIVE_USER_HOME: &str = if cfg!(windows) {
91+
WINDOWS_USER_HOME
92+
} else {
93+
UNIX_USER_HOME
94+
};
95+
8796
impl ContextBuilder {
8897
pub fn new() -> Self {
8998
Self::default()
@@ -132,11 +141,19 @@ impl ContextBuilder {
132141
/// [Fs] and [Env] currently set with the builder.
133142
#[cfg(test)]
134143
pub async fn with_test_home(mut self) -> Result<Self, std::io::Error> {
135-
let home = "/home/testuser";
136144
let fs = Fs::new_chroot();
137-
fs.create_dir_all(home).await?;
145+
fs.create_dir_all(ACTIVE_USER_HOME).await?;
138146
self.fs = Some(fs);
139-
self.env = Some(Env::from_slice(&[("HOME", "/home/testuser"), ("USER", "testuser")]));
147+
148+
if cfg!(windows) {
149+
self.env = Some(Env::from_slice(&[
150+
("USERPROFILE", ACTIVE_USER_HOME),
151+
("USERNAME", "testuser"),
152+
]));
153+
} else {
154+
self.env = Some(Env::from_slice(&[("HOME", ACTIVE_USER_HOME), ("USER", "testuser")]));
155+
}
156+
140157
Ok(self)
141158
}
142159

@@ -165,8 +182,18 @@ mod tests {
165182
.unwrap()
166183
.with_env_var("hello", "world")
167184
.build();
168-
assert!(ctx.fs().try_exists("/home/testuser").await.unwrap());
169-
assert_eq!(ctx.env().get("HOME").unwrap(), "/home/testuser");
185+
186+
#[cfg(windows)]
187+
{
188+
assert!(ctx.fs().try_exists(WINDOWS_USER_HOME).await.unwrap());
189+
assert_eq!(ctx.env().get("USERPROFILE").unwrap(), WINDOWS_USER_HOME);
190+
}
191+
#[cfg(not(windows))]
192+
{
193+
assert!(ctx.fs().try_exists(UNIX_USER_HOME).await.unwrap());
194+
assert_eq!(ctx.env().get("HOME").unwrap(), UNIX_USER_HOME);
195+
}
196+
170197
assert_eq!(ctx.env().get("hello").unwrap(), "world");
171198
}
172199
}

crates/chat-cli/src/util/directories.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,22 @@ pub fn home_dir(#[cfg_attr(windows, allow(unused_variables))] ctx: &Context) ->
5252
}
5353

5454
#[cfg(windows)]
55-
dirs::home_dir().ok_or(DirectoryError::NoHomeDirectory)
55+
match cfg!(test) {
56+
true => ctx
57+
.env()
58+
.get("USERPROFILE")
59+
.map_err(|_err| DirectoryError::NoHomeDirectory)
60+
.and_then(|h| {
61+
if h.is_empty() {
62+
Err(DirectoryError::NoHomeDirectory)
63+
} else {
64+
Ok(h)
65+
}
66+
})
67+
.map(PathBuf::from)
68+
.map(|p| ctx.fs().chroot_path(p)),
69+
false => dirs::home_dir().ok_or(DirectoryError::NoHomeDirectory),
70+
}
5671
}
5772

5873
/// The q data directory

0 commit comments

Comments
 (0)