diff --git a/internal/app/auth_command_test.go b/internal/app/auth_command_test.go index 31447da5..8af273e9 100644 --- a/internal/app/auth_command_test.go +++ b/internal/app/auth_command_test.go @@ -37,6 +37,10 @@ import ( ) func TestAuthExportImportBase64RoundTrip(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("portable auth export is intentionally unsupported on Windows") + } + t.Setenv(keychain.DisableKeychainEnv, "1") sourceKeychain := filepath.Join(t.TempDir(), "source-keychain") sourceConfig := filepath.Join(t.TempDir(), ".dws") @@ -123,6 +127,7 @@ func TestAuthExportRejectsWindowsDPAPIBackend(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("Windows DPAPI contract requires a native Windows runner") } + t.Cleanup(CloseFileLogger) exportCmd := NewRootCommand() exportCmd.SetOut(&bytes.Buffer{}) @@ -147,6 +152,7 @@ func TestAuthExportRejectsWindowsDPAPIBackend(t *testing.T) { func TestAuthImportRequiresForceWhenPopulated(t *testing.T) { t.Setenv(keychain.DisableKeychainEnv, "1") root := t.TempDir() + t.Cleanup(CloseFileLogger) configDir := filepath.Join(root, ".dws") t.Setenv(keychain.StorageDirEnv, filepath.Join(root, "keychain")) t.Setenv("DWS_CONFIG_DIR", configDir) @@ -1120,6 +1126,7 @@ func setupAuthLogoutProfiles(t *testing.T, tokens ...*authpkg.TokenData) string ResetRuntimeTokenCache() clearCompatCache() t.Cleanup(func() { + CloseFileLogger() authpkg.SetRuntimeProfile("") ResetRuntimeTokenCache() clearCompatCache() diff --git a/internal/app/config_test.go b/internal/app/config_test.go index c78c3c49..ae2ea234 100644 --- a/internal/app/config_test.go +++ b/internal/app/config_test.go @@ -7,7 +7,7 @@ import ( func TestDefaultConfigDirUsesHomeDirectoryInOSSMode(t *testing.T) { homeDir := filepath.Join(t.TempDir(), "home") - t.Setenv("HOME", homeDir) + setTestHome(t, homeDir) t.Setenv("DWS_CONFIG_DIR", "") got := defaultConfigDir() diff --git a/internal/app/schema_agent_examples_dry_run_test.go b/internal/app/schema_agent_examples_dry_run_test.go index b3284eb6..fa4eca28 100644 --- a/internal/app/schema_agent_examples_dry_run_test.go +++ b/internal/app/schema_agent_examples_dry_run_test.go @@ -67,7 +67,7 @@ func TestManualAgentExamplesDryRun(t *testing.T) { t.Fatalf("create isolated test directory %s: %v", dir, err) } } - t.Setenv("HOME", homeDir) + setTestHome(t, homeDir) t.Setenv("DWS_CONFIG_DIR", configDir) t.Setenv("HTTP_PROXY", "http://127.0.0.1:1") t.Setenv("HTTPS_PROXY", "http://127.0.0.1:1") @@ -470,7 +470,7 @@ func TestManualAgentExamplePromptObservedRejectsInteractiveConfirmation(t *testi } func TestAitableAdvpermDisableDryRunSkipsConfirmationAndToolCall(t *testing.T) { - t.Setenv("HOME", t.TempDir()) + setTestHome(t, t.TempDir()) t.Setenv("DWS_CONFIG_DIR", t.TempDir()) args := []string{ @@ -518,7 +518,7 @@ func TestManualAgentExampleChatGroupMuteMemberUsesCommandDryRunPreview(t *testin if err := os.MkdirAll(configDir, 0o700); err != nil { t.Fatalf("create isolated config directory: %v", err) } - t.Setenv("HOME", sandboxRoot) + setTestHome(t, sandboxRoot) t.Setenv("DWS_CONFIG_DIR", configDir) capture, err := executeManualAgentExampleCapture(t, []string{ diff --git a/internal/app/skill_command_test.go b/internal/app/skill_command_test.go index 143b0dd1..226afffc 100644 --- a/internal/app/skill_command_test.go +++ b/internal/app/skill_command_test.go @@ -369,6 +369,7 @@ func TestSkillInstallCommandValidation(t *testing.T) { func TestSkillInstallInvalidTarget(t *testing.T) { // Setup: Create config directory with valid token tempDir := t.TempDir() + t.Cleanup(CloseFileLogger) configDir := filepath.Join(tempDir, "config") t.Setenv("DWS_CONFIG_DIR", configDir) @@ -402,6 +403,7 @@ func TestSkillInstallInvalidTarget(t *testing.T) { func TestSkillInstallRequiresAuth(t *testing.T) { // Setup: Create config directory without token tempDir := t.TempDir() + t.Cleanup(CloseFileLogger) configDir := filepath.Join(tempDir, "config") t.Setenv("DWS_CONFIG_DIR", configDir) diff --git a/internal/app/skill_setup_test.go b/internal/app/skill_setup_test.go index d35c5d77..de3f2cba 100644 --- a/internal/app/skill_setup_test.go +++ b/internal/app/skill_setup_test.go @@ -88,7 +88,7 @@ func TestResolveSkillSetupSourceErrorWhenMissing(t *testing.T) { // Isolate HOME so the ~/.dws/skills// fallback (added by the release // pipeline cache work) does not pick up real cached content on the // developer machine. - t.Setenv("HOME", t.TempDir()) + setTestHome(t, t.TempDir()) _, err := resolveSkillSetupSource(tmp, skillSetupModeMono) if err == nil { t.Fatalf("expected error when source missing") @@ -106,7 +106,7 @@ func TestResolveSkillSetupTargetsSingleAgent(t *testing.T) { if len(got) != 1 { t.Fatalf("expected 1 dest, got %d", len(got)) } - if !strings.Contains(got[0], ".claude/skills/dws") { + if !strings.Contains(filepath.Clean(got[0]), filepath.Join(".claude", "skills", "dws")) { t.Fatalf("expected .claude/skills/dws path, got %s", got[0]) } } @@ -125,10 +125,10 @@ func TestResolveSkillSetupTargetsMultiOmitsDwsTail(t *testing.T) { if len(got) != 1 { t.Fatalf("expected 1 dest, got %d", len(got)) } - if strings.HasSuffix(got[0], "/dws") { + if filepath.Base(got[0]) == "dws" { t.Fatalf("multi target must not end with /dws, got %s", got[0]) } - if !strings.HasSuffix(got[0], ".claude/skills") { + if !strings.HasSuffix(filepath.Clean(got[0]), filepath.Join(".claude", "skills")) { t.Fatalf("expected suffix .claude/skills, got %s", got[0]) } } diff --git a/internal/app/test_home_test.go b/internal/app/test_home_test.go new file mode 100644 index 00000000..23cbda2e --- /dev/null +++ b/internal/app/test_home_test.go @@ -0,0 +1,28 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "runtime" + "testing" +) + +func setTestHome(t *testing.T, home string) { + t.Helper() + t.Setenv("HOME", home) + if runtime.GOOS == "windows" { + // os.UserHomeDir uses USERPROFILE on Windows rather than HOME. + t.Setenv("USERPROFILE", home) + } +} diff --git a/internal/app/testmain_test.go b/internal/app/testmain_test.go index 76d74c17..d052a3ad 100644 --- a/internal/app/testmain_test.go +++ b/internal/app/testmain_test.go @@ -15,6 +15,7 @@ package app import ( "os" + "path/filepath" "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/keychain" @@ -49,8 +50,19 @@ func TestMain(m *testing.M) { _ = os.RemoveAll(tmpDir) panic("set " + keychain.StorageDirEnv + ": " + err.Error()) } + if err := os.Setenv("DWS_CONFIG_DIR", filepath.Join(tmpDir, "config")); err != nil { + _ = os.RemoveAll(tmpDir) + panic("set DWS_CONFIG_DIR: " + err.Error()) + } + // Keep the process-wide audit sink outside per-test TempDir trees. Cobra + // skips PersistentPostRunE on expected command errors, and Windows cannot + // remove a TempDir while the audit lock is still open. + setupAuditSink() openBrowserFunc = func(string) error { return nil } code := m.Run() + StopAllStdioClients() + CloseAuditSink() + CloseFileLogger() _ = os.RemoveAll(tmpDir) os.Exit(code) } diff --git a/internal/app/timing_test.go b/internal/app/timing_test.go index 410c874c..bf8f8999 100644 --- a/internal/app/timing_test.go +++ b/internal/app/timing_test.go @@ -283,7 +283,7 @@ func TestWriteReportIfEnabled_Auto(t *testing.T) { expected := filepath.Join(tmpHome, ".dws", "perf", "latest.json") // Temporarily override HOME for defaultPerfReportPath - t.Setenv("HOME", tmpHome) + setTestHome(t, tmpHome) t.Setenv(PerfReportEnv, "auto") tc := NewTimingCollector() @@ -312,7 +312,7 @@ func TestWriteReportIfEnabled_NilCollector(t *testing.T) { func TestLoadLatestReport(t *testing.T) { tmpHome := t.TempDir() - t.Setenv("HOME", tmpHome) + setTestHome(t, tmpHome) perfDir := filepath.Join(tmpHome, ".dws", "perf") if err := os.MkdirAll(perfDir, 0o700); err != nil { @@ -348,7 +348,7 @@ func TestLoadLatestReport(t *testing.T) { func TestLoadLatestReport_NotFound(t *testing.T) { tmpHome := t.TempDir() - t.Setenv("HOME", tmpHome) + setTestHome(t, tmpHome) _, err := LoadLatestReport() if err == nil {