diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 97760472..25d0420d 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -98,3 +98,42 @@ jobs: workspaces: rust -> target - name: Run workspace clippy run: cargo clippy --workspace + + windows-smoke: + name: windows PowerShell smoke + runs-on: windows-latest + defaults: + run: + working-directory: rust + shell: pwsh + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + - name: Build CLI for Windows smoke + run: cargo build -p rusty-claude-cli + - name: Smoke local commands without live credentials + env: + ANTHROPIC_API_KEY: "" + ANTHROPIC_AUTH_TOKEN: "" + OPENAI_API_KEY: "" + XAI_API_KEY: "" + DASHSCOPE_API_KEY: "" + run: | + $ErrorActionPreference = "Stop" + $env:CLAW_CONFIG_HOME = Join-Path $env:RUNNER_TEMP "claw config home" + New-Item -ItemType Directory -Force -Path $env:CLAW_CONFIG_HOME | Out-Null + $workspace = Join-Path $env:RUNNER_TEMP "claw path smoke" + New-Item -ItemType Directory -Force -Path $workspace | Out-Null + $claw = Join-Path $env:GITHUB_WORKSPACE "rust\target\debug\claw.exe" + Push-Location $workspace + try { + & $claw help + & $claw status + & $claw config env + & $claw doctor + } finally { + Pop-Location + } diff --git a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs index bc4fbe2c..071d53a8 100644 --- a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs +++ b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs @@ -215,6 +215,48 @@ fn doctor_command_runs_as_a_local_shell_entrypoint() { fs::remove_dir_all(temp_dir).expect("cleanup temp dir"); } +#[test] +fn local_smoke_commands_do_not_require_live_credentials() { + let temp_dir = unique_temp_dir("offline-local-smoke path with spaces"); + let config_home = temp_dir.join("home with spaces").join(".claw"); + fs::create_dir_all(&config_home).expect("config home should exist"); + fs::create_dir_all(&temp_dir).expect("temp dir should exist"); + + for args in [ + &["help"][..], + &["status"][..], + &["config", "env"][..], + &["doctor"][..], + ] { + let output = offline_command_in(&temp_dir, &config_home) + .args(args) + .output() + .unwrap_or_else(|error| panic!("claw {args:?} should launch: {error}")); + + assert_success(&output); + let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); + let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); + assert!( + stdout.contains("claw") + || stdout.contains("Status") + || stdout.contains("Config") + || stdout.contains("Doctor"), + "unexpected stdout for {args:?}: {stdout}" + ); + assert!( + !stderr.contains("missing Anthropic credentials") + && !stderr.contains("auth_unavailable"), + "local smoke command {args:?} should not require live credentials: {stderr}" + ); + assert!( + !stdout.contains("Thinking"), + "local smoke command {args:?} should not enter prompt runtime" + ); + } + + fs::remove_dir_all(temp_dir).expect("cleanup temp dir"); +} + #[test] fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() { let temp_dir = unique_temp_dir("subcommand-help"); @@ -258,6 +300,19 @@ fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() { fs::remove_dir_all(temp_dir).expect("cleanup temp dir"); } +fn offline_command_in(cwd: &Path, config_home: &Path) -> Command { + let mut command = command_in(cwd); + command + .env("CLAW_CONFIG_HOME", config_home) + .env_remove("ANTHROPIC_API_KEY") + .env_remove("ANTHROPIC_AUTH_TOKEN") + .env_remove("OPENAI_API_KEY") + .env_remove("XAI_API_KEY") + .env_remove("DASHSCOPE_API_KEY") + .env("ANTHROPIC_BASE_URL", "http://127.0.0.1:9"); + command +} + fn command_in(cwd: &Path) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_claw")); command.current_dir(cwd);