//go:build windows || (cgo && (linux || darwin || freebsd)) package pluginhost import ( "crypto/sha256" "encoding/hex" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" ) func testPluginLibraryExtension() string { switch runtime.GOOS { case "windows": return ".dll" case "darwin": return ".dylib" default: return ".so" } } func TestShadowPluginDirUsesSharedCache(t *testing.T) { dir, errDir := shadowPluginDir() if errDir != nil { t.Fatalf("shadowPluginDir() error = %v", errDir) } want := filepath.Join(os.TempDir(), "cliproxy-pluginhost", "cache") if dir != want { t.Fatalf("shadowPluginDir() = %q, want %q", dir, want) } } func TestShadowCopyPluginReusesAcrossProcessRestartAndUsesLocalShadowDir(t *testing.T) { pluginDir := t.TempDir() source := filepath.Join(pluginDir, "alpha-v1.0.0"+testPluginLibraryExtension()) content := []byte("plugin-v1") if errWrite := os.WriteFile(source, content, 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } file := pluginFile{ID: "alpha", Path: source, Version: "1.0.0"} first, errFirst := shadowCopyPlugin(file) if errFirst != nil { t.Fatalf("shadowCopyPlugin() first error = %v", errFirst) } wantShadowDir := filepath.Join(pluginDir, ".shadow") if filepath.Dir(first) != wantShadowDir { t.Fatalf("shadowCopyPlugin() dir = %q, want %q", filepath.Dir(first), wantShadowDir) } past := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC) if errCh := os.Chtimes(first, past, past); errCh != nil { t.Fatalf("Chtimes(first) error = %v", errCh) } if errCh := os.Chtimes(wantShadowDir, past, past); errCh != nil { t.Fatalf("Chtimes(shadowDir) error = %v", errCh) } childPath := runChildShadowCopyPlugin(t, file) if childPath != first { t.Fatalf("child process shadowCopyPlugin() = %q, want reused %q", childPath, first) } info, errStat := os.Stat(first) if errStat != nil { t.Fatalf("Stat(%s) after child restart error = %v", first, errStat) } if !info.ModTime().Equal(past) { t.Fatalf("shadow file ModTime = %v, want unchanged %v", info.ModTime(), past) } dirInfo, errDirStat := os.Stat(wantShadowDir) if errDirStat != nil { t.Fatalf("Stat(%s) after child restart error = %v", wantShadowDir, errDirStat) } if !dirInfo.ModTime().Equal(past) { t.Fatalf("shadow dir ModTime = %v, want unchanged %v (no temp file should be created on cache hit)", dirInfo.ModTime(), past) } } func TestShadowCopyPluginToDirSkipsTempWriteWhenShadowMatches(t *testing.T) { dir := t.TempDir() source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension()) if errWrite := os.WriteFile(source, []byte("plugin-v1"), 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } file := pluginFile{ID: "alpha", Path: source} first, errFirst := shadowCopyPluginToDir(file, dir) if errFirst != nil { t.Fatalf("shadowCopyPluginToDir() first error = %v", errFirst) } past := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC) if errCh := os.Chtimes(first, past, past); errCh != nil { t.Fatalf("Chtimes(first) error = %v", errCh) } if errCh := os.Chtimes(dir, past, past); errCh != nil { t.Fatalf("Chtimes(dir) error = %v", errCh) } second, errSecond := shadowCopyPluginToDir(file, dir) if errSecond != nil { t.Fatalf("shadowCopyPluginToDir() second error = %v", errSecond) } if second != first { t.Fatalf("second shadow path = %q, want %q", second, first) } dirInfo, errDirStat := os.Stat(dir) if errDirStat != nil { t.Fatalf("Stat(dir) error = %v", errDirStat) } if !dirInfo.ModTime().Equal(past) { t.Fatalf("shadow dir ModTime = %v, want unchanged %v (temp file was written before checking shadowPluginMatches)", dirInfo.ModTime(), past) } } func TestShadowCopyPluginFallsBackToSharedCacheWhenLocalShadowDirUnavailable(t *testing.T) { pluginDir := t.TempDir() // Create a regular file named ".shadow" so creating the local .shadow directory fails. blockerPath := filepath.Join(pluginDir, shadowPluginLocalDirName) if errWrite := os.WriteFile(blockerPath, []byte("blocker"), 0o644); errWrite != nil { t.Fatalf("WriteFile(blocker) error = %v", errWrite) } source := filepath.Join(pluginDir, "fallback-v1.0.0"+testPluginLibraryExtension()) if errWrite := os.WriteFile(source, []byte("plugin-fallback"), 0o644); errWrite != nil { t.Fatalf("WriteFile(source) error = %v", errWrite) } file := pluginFile{ID: "fallback", Path: source, Version: "1.0.0"} got, errShadow := shadowCopyPlugin(file) if errShadow != nil { t.Fatalf("shadowCopyPlugin() error = %v", errShadow) } t.Cleanup(func() { removeShadowPlugin(got) }) wantDir, errDir := shadowPluginFallbackDir(source) if errDir != nil { t.Fatalf("shadowPluginFallbackDir() error = %v", errDir) } if filepath.Dir(got) != wantDir { t.Fatalf("shadowCopyPlugin() fallback dir = %q, want %q", filepath.Dir(got), wantDir) } } func TestShadowCopyPluginFallbackCacheIsolatesDistinctPluginDirs(t *testing.T) { ext := testPluginLibraryExtension() rootA := t.TempDir() platformDirA := filepath.Join(rootA, runtime.GOOS, runtime.GOARCH) if errMkdir := os.MkdirAll(platformDirA, 0o755); errMkdir != nil { t.Fatalf("MkdirAll(platformDirA) error = %v", errMkdir) } if errWrite := os.WriteFile(filepath.Join(platformDirA, shadowPluginLocalDirName), []byte("blocker"), 0o644); errWrite != nil { t.Fatalf("WriteFile(blockerA) error = %v", errWrite) } sourceA := filepath.Join(platformDirA, "alpha-v1.0.0"+ext) if errWrite := os.WriteFile(sourceA, []byte("alpha-install-A"), 0o644); errWrite != nil { t.Fatalf("WriteFile(sourceA) error = %v", errWrite) } fileA := pluginFile{ID: "alpha", Path: sourceA, Version: "1.0.0"} rootB := t.TempDir() platformDirB := filepath.Join(rootB, runtime.GOOS, runtime.GOARCH) if errMkdir := os.MkdirAll(platformDirB, 0o755); errMkdir != nil { t.Fatalf("MkdirAll(platformDirB) error = %v", errMkdir) } if errWrite := os.WriteFile(filepath.Join(platformDirB, shadowPluginLocalDirName), []byte("blocker"), 0o644); errWrite != nil { t.Fatalf("WriteFile(blockerB) error = %v", errWrite) } sourceB := filepath.Join(platformDirB, "alpha-v1.0.0"+ext) if errWrite := os.WriteFile(sourceB, []byte("alpha-install-B"), 0o644); errWrite != nil { t.Fatalf("WriteFile(sourceB) error = %v", errWrite) } fileB := pluginFile{ID: "alpha", Path: sourceB, Version: "1.0.0"} shadowA, errShadowA := shadowCopyPlugin(fileA) if errShadowA != nil { t.Fatalf("shadowCopyPlugin(fileA) error = %v", errShadowA) } t.Cleanup(func() { removeShadowPluginDir(filepath.Dir(shadowA)) }) shadowB, errShadowB := shadowCopyPlugin(fileB) if errShadowB != nil { t.Fatalf("shadowCopyPlugin(fileB) error = %v", errShadowB) } t.Cleanup(func() { removeShadowPluginDir(filepath.Dir(shadowB)) }) if filepath.Dir(shadowA) == filepath.Dir(shadowB) { t.Fatalf("fallback cache dirs collided across installations: %q", filepath.Dir(shadowA)) } if errCleanup := cleanupUnselectedPluginFiles(rootA, []pluginFile{fileA}); errCleanup != nil { t.Fatalf("cleanupUnselectedPluginFiles(rootA) error = %v", errCleanup) } if _, errStat := os.Stat(shadowB); errStat != nil { t.Fatalf("Stat(shadowB %s) after rootA cleanup error = %v, want kept", shadowB, errStat) } } func TestShadowDirectoryDigestResolvesRelativePathsByWorkingDirectory(t *testing.T) { origWD, errWD := os.Getwd() if errWD != nil { t.Fatalf("Getwd() error = %v", errWD) } t.Cleanup(func() { _ = os.Chdir(origWD) }) workA := t.TempDir() workB := t.TempDir() relDir := filepath.Join("plugins", runtime.GOOS, runtime.GOARCH) if errChdir := os.Chdir(workA); errChdir != nil { t.Fatalf("Chdir(workA) error = %v", errChdir) } digestA := shadowDirectoryDigest(relDir) if errChdir := os.Chdir(workB); errChdir != nil { t.Fatalf("Chdir(workB) error = %v", errChdir) } digestB := shadowDirectoryDigest(relDir) if digestA == digestB { t.Fatalf("shadowDirectoryDigest(%q) collided across working directories: %s", relDir, digestA) } } func TestShadowCopyPluginConcurrentPublishNeverUnlinksExistingTarget(t *testing.T) { dir := t.TempDir() source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension()) content := []byte("concurrent-plugin-content") if errWrite := os.WriteFile(source, content, 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } file := pluginFile{ID: "alpha", Path: source} first, errFirst := shadowCopyPluginToDir(file, dir) if errFirst != nil { t.Fatalf("shadowCopyPluginToDir() initial error = %v", errFirst) } infoBefore, errStatBefore := os.Stat(first) if errStatBefore != nil { t.Fatalf("Stat(first) error = %v", errStatBefore) } size, digest, errHash := hashPluginFile(source) if errHash != nil { t.Fatalf("hashPluginFile() error = %v", errHash) } // Simulate a racer that already wrote its temp file before winner published target, // and now attempts publishShadowTempFile against the already-published target. racerTmp := filepath.Join(dir, shadowPluginTempPrefix+"racer"+testPluginLibraryExtension()) if errWrite := os.WriteFile(racerTmp, content, 0o644); errWrite != nil { t.Fatalf("WriteFile(racerTmp) error = %v", errWrite) } if errPublish := publishShadowTempFile(racerTmp, first, size, digest); errPublish != nil { t.Fatalf("publishShadowTempFile() error = %v", errPublish) } if _, errStatTmp := os.Stat(racerTmp); !os.IsNotExist(errStatTmp) { t.Fatalf("Stat(racerTmp) error = %v, want not exist", errStatTmp) } infoAfter, errStatAfter := os.Stat(first) if errStatAfter != nil { t.Fatalf("Stat(first) after race error = %v", errStatAfter) } if !os.SameFile(infoBefore, infoAfter) { t.Fatal("existing target shadow file was unlinked/replaced by racer") } } func TestCleanupUnselectedPluginFilesDoesNotConfusePrefixPluginIDs(t *testing.T) { root := t.TempDir() platformDir := filepath.Join(root, runtime.GOOS, runtime.GOARCH) if errMkdir := os.MkdirAll(platformDir, 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } ext := testPluginLibraryExtension() alphaOldPath := filepath.Join(platformDir, "alpha-v1.0.0"+ext) alphaNewPath := filepath.Join(platformDir, "alpha-v2.0.0"+ext) alphaBetaPath := filepath.Join(platformDir, "alpha-beta-v1.0.0"+ext) for path, content := range map[string]string{ alphaOldPath: "alpha-v1", alphaNewPath: "alpha-v2", alphaBetaPath: "alpha-beta-v1", } { if errWrite := os.WriteFile(path, []byte(content), 0o644); errWrite != nil { t.Fatalf("WriteFile(%s) error = %v", path, errWrite) } } alphaOldShadow, errOld := shadowCopyPlugin(pluginFile{ID: "alpha", Path: alphaOldPath, Version: "1.0.0"}) if errOld != nil { t.Fatalf("shadowCopyPlugin(alpha old) error = %v", errOld) } alphaNewFile := pluginFile{ID: "alpha", Path: alphaNewPath, Version: "2.0.0"} alphaNewShadow, errNew := shadowCopyPlugin(alphaNewFile) if errNew != nil { t.Fatalf("shadowCopyPlugin(alpha new) error = %v", errNew) } alphaBetaShadow, errBeta := shadowCopyPlugin(pluginFile{ID: "alpha-beta", Path: alphaBetaPath, Version: "1.0.0"}) if errBeta != nil { t.Fatalf("shadowCopyPlugin(alpha-beta) error = %v", errBeta) } removedPath := filepath.Join(platformDir, "removed-v1.0.0"+ext) if errWrite := os.WriteFile(removedPath, []byte("removed-v1"), 0o644); errWrite != nil { t.Fatalf("WriteFile(removedPath) error = %v", errWrite) } removedShadow, errRemoved := shadowCopyPlugin(pluginFile{ID: "removed", Path: removedPath, Version: "1.0.0"}) if errRemoved != nil { t.Fatalf("shadowCopyPlugin(removed) error = %v", errRemoved) } if errRemove := os.Remove(removedPath); errRemove != nil { t.Fatalf("Remove(removedPath) error = %v", errRemove) } if errCleanup := cleanupUnselectedPluginFiles(root, []pluginFile{alphaNewFile}); errCleanup != nil { t.Fatalf("cleanupUnselectedPluginFiles() error = %v", errCleanup) } if _, errStat := os.Stat(alphaOldShadow); !os.IsNotExist(errStat) { t.Fatalf("Stat(alphaOldShadow %s) error = %v, want not exist", alphaOldShadow, errStat) } if _, errStat := os.Stat(removedShadow); !os.IsNotExist(errStat) { t.Fatalf("Stat(removedShadow %s) error = %v, want not exist", removedShadow, errStat) } if _, errStat := os.Stat(alphaNewShadow); errStat != nil { t.Fatalf("Stat(alphaNewShadow %s) error = %v, want kept", alphaNewShadow, errStat) } if _, errStat := os.Stat(alphaBetaShadow); errStat != nil { t.Fatalf("Stat(alphaBetaShadow %s) error = %v, want kept for sibling plugin", alphaBetaShadow, errStat) } } func TestPruneShadowDirPreservesConcurrentlyInstalledPluginAndLiveGuard(t *testing.T) { root := t.TempDir() platformDir := filepath.Join(root, runtime.GOOS, runtime.GOARCH) if errMkdir := os.MkdirAll(platformDir, 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } ext := testPluginLibraryExtension() v1Path := filepath.Join(platformDir, "alpha-v1.0.0"+ext) if errWrite := os.WriteFile(v1Path, []byte("alpha-v1"), 0o644); errWrite != nil { t.Fatalf("WriteFile(v1Path) error = %v", errWrite) } v1File := pluginFile{ID: "alpha", Path: v1Path, Version: "1.0.0"} v1Shadow, errV1 := shadowCopyPlugin(v1File) if errV1 != nil { t.Fatalf("shadowCopyPlugin(v1) error = %v", errV1) } // Simulate another process installing v2 and publishing its shadow file after keepNames was computed for v1. v2Path := filepath.Join(platformDir, "alpha-v2.0.0"+ext) if errWrite := os.WriteFile(v2Path, []byte("alpha-v2"), 0o644); errWrite != nil { t.Fatalf("WriteFile(v2Path) error = %v", errWrite) } v2File := pluginFile{ID: "alpha", Path: v2Path, Version: "2.0.0"} v2Shadow, errV2 := shadowCopyPlugin(v2File) if errV2 != nil { t.Fatalf("shadowCopyPlugin(v2) error = %v", errV2) } shadowDir := filepath.Dir(v1Shadow) pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)}) if _, errStat := os.Stat(v2Shadow); errStat != nil { t.Fatalf("Stat(v2Shadow %s) error = %v, want preserved because v2 exists in sourceDir", v2Shadow, errStat) } // Write v2 content to a separate temp source before removing v2Path from platformDir so we can // also test the interleaving where v2 was removed from platformDir while another process holds a guard. detachedV2Path := filepath.Join(t.TempDir(), "alpha-v2.0.0"+ext) if errWrite := os.WriteFile(detachedV2Path, []byte("alpha-v2"), 0o644); errWrite != nil { t.Fatalf("WriteFile(detachedV2Path) error = %v", errWrite) } v2File = pluginFile{ID: "alpha", Path: detachedV2Path, Version: "2.0.0"} if errRemove := os.Remove(v2Path); errRemove != nil { t.Fatalf("Remove(v2Path) error = %v", errRemove) } _, releaseGuard, errAcquire := acquireShadowPluginLoad(v1File) if errAcquire != nil { t.Fatalf("acquireShadowPluginLoad() error = %v", errAcquire) } pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)}) if _, errStat := os.Stat(v2Shadow); errStat != nil { releaseGuard() t.Fatalf("Stat(v2Shadow %s) while load guard active error = %v, want preserved", v2Shadow, errStat) } releaseGuard() pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)}) if _, errStat := os.Stat(v2Shadow); !os.IsNotExist(errStat) { t.Fatalf("Stat(v2Shadow %s) after guard released error = %v, want not exist", v2Shadow, errStat) } // Verify deterministic interleaving: pruneShadowDirForSourceDir has already read directory entries, // and before it acquires the coord lock to delete a candidate shadow file, another caller acquires a load guard. v2ShadowRe, errV2Re := shadowCopyPluginToDir(v2File, shadowDir) if errV2Re != nil { t.Fatalf("shadowCopyPluginToDir(v2) error = %v", errV2Re) } var releaseInterleavedGuard func() origHook := pruneShadowBeforeLockHook t.Cleanup(func() { pruneShadowBeforeLockHook = origHook if releaseInterleavedGuard != nil { releaseInterleavedGuard() } }) pruneShadowBeforeLockHook = func(shadowFileName string) { if shadowFileName == filepath.Base(v2ShadowRe) && releaseInterleavedGuard == nil { _, release, errLoad := acquireShadowPluginLoad(v1File) if errLoad != nil { t.Errorf("interleaved acquireShadowPluginLoad() error = %v", errLoad) return } releaseInterleavedGuard = release } } pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)}) pruneShadowBeforeLockHook = origHook if _, errStat := os.Stat(v2ShadowRe); errStat != nil { t.Fatalf("Stat(v2ShadowRe %s) after interleaved guard acquisition error = %v, want preserved", v2ShadowRe, errStat) } // Even when keepNames is empty (e.g., all source files were removed or failed hashing), // an active load guard must still prevent pruning shadow files. pruneShadowDirForSourceDir(shadowDir, platformDir, nil) if _, errStat := os.Stat(v2ShadowRe); errStat != nil { t.Fatalf("Stat(v2ShadowRe %s) with empty keepNames while guard active error = %v, want preserved", v2ShadowRe, errStat) } if releaseInterleavedGuard != nil { releaseInterleavedGuard() releaseInterleavedGuard = nil } // Verify that if a process crashes/exits abnormally while holding the OS coordination lock, // the kernel automatically releases the lock so subsequent loads and pruning recover immediately. cmd := exec.Command(os.Args[0], "-test.run=^TestShadowHelperHoldLockAndExit$") cmd.Env = append( os.Environ(), "GO_WANT_SHADOW_HOLD_LOCK_EXIT_HELPER=1", "CLIPROXY_SHADOW_HELPER_SOURCE_DIR="+platformDir, "CLIPROXY_SHADOW_HELPER_SHADOW_DIR="+shadowDir, ) if output, errRun := cmd.CombinedOutput(); errRun == nil { t.Fatalf("helper unexpectedly exited with status 0: %s", string(output)) } _, releaseAfterCrash, errAfterCrash := acquireShadowPluginLoad(v1File) if errAfterCrash != nil { t.Fatalf("acquireShadowPluginLoad() after crashing lock holder error = %v", errAfterCrash) } releaseAfterCrash() pruneShadowDirForSourceDir(shadowDir, platformDir, []string{filepath.Base(v1Shadow)}) if _, errStat := os.Stat(v2ShadowRe); !os.IsNotExist(errStat) { t.Fatalf("Stat(v2ShadowRe %s) after crashing lock holder and prune error = %v, want not exist", v2ShadowRe, errStat) } } func TestRemoveStaleShadowTempFilesPreservesLiveProcessTempFile(t *testing.T) { dir := t.TempDir() ext := testPluginLibraryExtension() currentPID := os.Getpid() deadPID := startExitedProcessPID(t) if deadPID == currentPID { t.Skip("exited helper process reused the test pid") } liveOther, liveOtherPID := startWaitingProcess(t) if liveOtherPID == currentPID || liveOtherPID == deadPID { t.Skip("live helper process pid collided with another test pid") } defer stopWaitingProcess(t, liveOther) selfTemp := filepath.Join(dir, fmt.Sprintf("%s%s%d-alpha-self%s", shadowPluginTempPrefix, shadowPluginProcessDirPrefix, currentPID, ext)) liveTemp := filepath.Join(dir, fmt.Sprintf("%s%s%d-alpha-live%s", shadowPluginTempPrefix, shadowPluginProcessDirPrefix, liveOtherPID, ext)) deadTemp := filepath.Join(dir, fmt.Sprintf("%s%s%d-alpha-dead%s", shadowPluginTempPrefix, shadowPluginProcessDirPrefix, deadPID, ext)) legacyTemp := filepath.Join(dir, shadowPluginTempPrefix+"alpha-legacy"+ext) for _, path := range []string{selfTemp, liveTemp, deadTemp, legacyTemp} { if errWrite := os.WriteFile(path, []byte("temp"), 0o644); errWrite != nil { t.Fatalf("WriteFile(%s) error = %v", path, errWrite) } } removeStaleShadowTempFiles(dir) for _, deadPath := range []string{deadTemp, legacyTemp} { if _, errStat := os.Stat(deadPath); !os.IsNotExist(errStat) { t.Fatalf("Stat(%s) error = %v, want not exist", deadPath, errStat) } } for _, livePath := range []string{selfTemp, liveTemp} { if _, errStat := os.Stat(livePath); errStat != nil { t.Fatalf("Stat(%s) error = %v, want kept for live process", livePath, errStat) } } } func TestCleanupUnselectedPluginFilesPrunesStaleShadowPlugins(t *testing.T) { root := t.TempDir() platformDir := filepath.Join(root, runtime.GOOS, runtime.GOARCH) if errMkdir := os.MkdirAll(platformDir, 0o755); errMkdir != nil { t.Fatalf("MkdirAll() error = %v", errMkdir) } ext := testPluginLibraryExtension() oldPath := filepath.Join(platformDir, "alpha-v1.0.0"+ext) newPath := filepath.Join(platformDir, "alpha-v2.0.0"+ext) if errWrite := os.WriteFile(oldPath, []byte("alpha-v1"), 0o644); errWrite != nil { t.Fatalf("WriteFile(oldPath) error = %v", errWrite) } if errWrite := os.WriteFile(newPath, []byte("alpha-v2"), 0o644); errWrite != nil { t.Fatalf("WriteFile(newPath) error = %v", errWrite) } oldShadow, errOldShadow := shadowCopyPlugin(pluginFile{ID: "alpha", Path: oldPath, Version: "1.0.0"}) if errOldShadow != nil { t.Fatalf("shadowCopyPlugin(old) error = %v", errOldShadow) } newFile := pluginFile{ID: "alpha", Path: newPath, Version: "2.0.0"} newShadow, errNewShadow := shadowCopyPlugin(newFile) if errNewShadow != nil { t.Fatalf("shadowCopyPlugin(new) error = %v", errNewShadow) } tempShadow := filepath.Join(filepath.Dir(newShadow), shadowPluginTempPrefix+"alpha-stale"+ext) if errWrite := os.WriteFile(tempShadow, []byte("partial"), 0o644); errWrite != nil { t.Fatalf("WriteFile(tempShadow) error = %v", errWrite) } if errCleanup := cleanupUnselectedPluginFiles(root, []pluginFile{newFile}); errCleanup != nil { t.Fatalf("cleanupUnselectedPluginFiles() error = %v", errCleanup) } if _, errStat := os.Stat(oldShadow); !os.IsNotExist(errStat) { t.Fatalf("Stat(oldShadow %s) error = %v, want not exist", oldShadow, errStat) } if _, errStat := os.Stat(tempShadow); !os.IsNotExist(errStat) { t.Fatalf("Stat(tempShadow %s) error = %v, want not exist", tempShadow, errStat) } if _, errStat := os.Stat(newShadow); errStat != nil { t.Fatalf("Stat(newShadow %s) error = %v, want kept", newShadow, errStat) } } func TestShadowCopyPluginReusesContentAddressedShadow(t *testing.T) { dir := t.TempDir() source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension()) content := []byte("plugin-v1") if errWrite := os.WriteFile(source, content, 0o644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } file := pluginFile{ID: "alpha", Path: source} first, errFirst := shadowCopyPluginToDir(file, dir) if errFirst != nil { t.Fatalf("shadowCopyPluginToDir() first error = %v", errFirst) } second, errSecond := shadowCopyPluginToDir(file, dir) if errSecond != nil { t.Fatalf("shadowCopyPluginToDir() second error = %v", errSecond) } if second != first { t.Fatalf("second shadow path = %q, want reused path %q", second, first) } gotContent, errRead := os.ReadFile(first) if errRead != nil { t.Fatalf("ReadFile(%s) error = %v", first, errRead) } if string(gotContent) != string(content) { t.Fatalf("shadow content = %q, want %q", gotContent, content) } digest := sha256.Sum256(content) wantDigest := hex.EncodeToString(digest[:])[:shadowPluginDigestLength] name := filepath.Base(first) if !strings.HasPrefix(name, shadowPluginPrefix+"alpha-") || !strings.Contains(name, wantDigest) { t.Fatalf("shadow file name = %q, want alpha content digest %s", name, wantDigest) } if count := countShadowPluginFiles(t, dir); count != 1 { t.Fatalf("shadow file count = %d, want 1", count) } } func TestShadowCopyPluginCreatesNewPathForChangedContent(t *testing.T) { dir := t.TempDir() source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension()) file := pluginFile{ID: "alpha", Path: source} if errWrite := os.WriteFile(source, []byte("plugin-v1"), 0o644); errWrite != nil { t.Fatalf("WriteFile() v1 error = %v", errWrite) } first, errFirst := shadowCopyPluginToDir(file, dir) if errFirst != nil { t.Fatalf("shadowCopyPluginToDir() v1 error = %v", errFirst) } if errWrite := os.WriteFile(source, []byte("plugin-v2"), 0o644); errWrite != nil { t.Fatalf("WriteFile() v2 error = %v", errWrite) } second, errSecond := shadowCopyPluginToDir(file, dir) if errSecond != nil { t.Fatalf("shadowCopyPluginToDir() v2 error = %v", errSecond) } if second == first { t.Fatalf("second shadow path reused %q after content changed", second) } if count := countShadowPluginFiles(t, dir); count != 2 { t.Fatalf("shadow file count = %d, want 2 versions", count) } } func TestShadowCopyPluginReplacesCorruptSameSizeShadow(t *testing.T) { dir := t.TempDir() source := filepath.Join(t.TempDir(), "alpha"+testPluginLibraryExtension()) content := []byte("plugin-v1") if errWrite := os.WriteFile(source, content, 0o644); errWrite != nil { t.Fatalf("WriteFile() source error = %v", errWrite) } digest := sha256.Sum256(content) target := shadowPluginPath(dir, "alpha", hex.EncodeToString(digest[:]), testPluginLibraryExtension()) if errWrite := os.WriteFile(target, []byte("corrupt!!"), 0o644); errWrite != nil { t.Fatalf("WriteFile() corrupt shadow error = %v", errWrite) } gotPath, errCopy := shadowCopyPluginToDir(pluginFile{ID: "alpha", Path: source}, dir) if errCopy != nil { t.Fatalf("shadowCopyPluginToDir() error = %v", errCopy) } if gotPath != target { t.Fatalf("shadow path = %q, want %q", gotPath, target) } gotContent, errRead := os.ReadFile(target) if errRead != nil { t.Fatalf("ReadFile(%s) error = %v", target, errRead) } if string(gotContent) != string(content) { t.Fatalf("shadow content = %q, want %q", gotContent, content) } if count := countShadowPluginFiles(t, dir); count != 1 { t.Fatalf("shadow file count = %d, want 1", count) } } func TestRemoveStaleShadowPluginsOnlyRemovesShadowFiles(t *testing.T) { dir := t.TempDir() stale := filepath.Join(dir, shadowPluginPrefix+"alpha-deadbeef"+testPluginLibraryExtension()) temp := filepath.Join(dir, shadowPluginTempPrefix+"alpha-temp"+testPluginLibraryExtension()) keep := filepath.Join(dir, "keep"+testPluginLibraryExtension()) for _, path := range []string{stale, temp, keep} { if errWrite := os.WriteFile(path, []byte("x"), 0o644); errWrite != nil { t.Fatalf("WriteFile(%s) error = %v", path, errWrite) } } removeStaleShadowPlugins(dir) for _, path := range []string{stale, temp} { if _, errStat := os.Stat(path); !os.IsNotExist(errStat) { t.Fatalf("Stat(%s) error = %v, want not exist", path, errStat) } } if _, errStat := os.Stat(keep); errStat != nil { t.Fatalf("Stat(%s) error = %v, want kept", keep, errStat) } } func TestRemoveStaleShadowPluginDirsOnlyRemovesDeadProcesses(t *testing.T) { base := t.TempDir() currentPID := os.Getpid() deadPID := startExitedProcessPID(t) if deadPID == currentPID { t.Skip("exited helper process reused the test pid") } liveDir := filepath.Join(base, shadowPluginProcessDirName(currentPID)) deadDir := filepath.Join(base, shadowPluginProcessDirName(deadPID)) otherDir := filepath.Join(base, "unrelated") // A directory owned by a different live process must survive the // liveness check itself, not just the current-pid shortcut. liveOther, liveOtherPID := startWaitingProcess(t) if liveOtherPID == currentPID || liveOtherPID == deadPID { t.Skip("live helper process pid collided with another test pid") } liveOtherDir := filepath.Join(base, shadowPluginProcessDirName(liveOtherPID)) for _, dir := range []string{liveDir, deadDir, otherDir, liveOtherDir} { if errMkdir := os.MkdirAll(dir, 0o755); errMkdir != nil { t.Fatalf("MkdirAll(%s) error = %v", dir, errMkdir) } shadowFile := filepath.Join(dir, shadowPluginPrefix+"alpha-deadbeef"+testPluginLibraryExtension()) if errWrite := os.WriteFile(shadowFile, []byte("x"), 0o644); errWrite != nil { t.Fatalf("WriteFile(%s) error = %v", shadowFile, errWrite) } } removeStaleShadowPluginDirs(base, currentPID) if _, errStat := os.Stat(deadDir); !os.IsNotExist(errStat) { t.Fatalf("Stat(%s) error = %v, want not exist", deadDir, errStat) } for _, dir := range []string{liveDir, otherDir, liveOtherDir} { if _, errStat := os.Stat(dir); errStat != nil { t.Fatalf("Stat(%s) error = %v, want kept", dir, errStat) } } stopWaitingProcess(t, liveOther) } func startExitedProcessPID(t *testing.T) int { t.Helper() cmd := exec.Command(os.Args[0], "-test.run=^TestShadowHelperProcessExits$") cmd.Env = append(os.Environ(), "CLIPROXY_SHADOW_HELPER=1") if errRun := cmd.Run(); errRun != nil { t.Fatalf("helper process error = %v", errRun) } if cmd.Process == nil { t.Fatal("helper process is nil") } return cmd.Process.Pid } func TestShadowHelperProcessExits(t *testing.T) { if os.Getenv("CLIPROXY_SHADOW_HELPER") != "1" { t.Skip("helper process entry point") } } func runChildShadowCopyPlugin(t *testing.T, file pluginFile) string { t.Helper() cmd := exec.Command(os.Args[0], "-test.run=^TestShadowHelperCopyPlugin$") cmd.Env = append( os.Environ(), "CLIPROXY_SHADOW_COPY_HELPER=1", "CLIPROXY_SHADOW_COPY_ID="+file.ID, "CLIPROXY_SHADOW_COPY_PATH="+file.Path, "CLIPROXY_SHADOW_COPY_VERSION="+file.Version, ) out, errRun := cmd.CombinedOutput() if errRun != nil { t.Fatalf("child shadowCopyPlugin helper error = %v\n%s", errRun, out) } for _, line := range strings.Split(string(out), "\n") { if strings.HasPrefix(line, "SHADOW_PATH=") { return strings.TrimSpace(strings.TrimPrefix(line, "SHADOW_PATH=")) } } t.Fatalf("child shadowCopyPlugin output missing SHADOW_PATH: %s", out) return "" } func TestShadowHelperCopyPlugin(t *testing.T) { if os.Getenv("CLIPROXY_SHADOW_COPY_HELPER") != "1" { t.Skip("shadow copy helper process entry point") } got, errShadow := shadowCopyPlugin(pluginFile{ ID: os.Getenv("CLIPROXY_SHADOW_COPY_ID"), Path: os.Getenv("CLIPROXY_SHADOW_COPY_PATH"), Version: os.Getenv("CLIPROXY_SHADOW_COPY_VERSION"), }) if errShadow != nil { t.Fatalf("shadowCopyPlugin() error = %v", errShadow) } fmt.Printf("SHADOW_PATH=%s\n", got) } func startWaitingProcess(t *testing.T) (*exec.Cmd, int) { t.Helper() cmd := exec.Command(os.Args[0], "-test.run=^TestShadowHelperProcessWaits$") cmd.Env = append(os.Environ(), "CLIPROXY_SHADOW_HELPER_WAIT=1") stdin, errStdin := cmd.StdinPipe() if errStdin != nil { t.Fatalf("StdinPipe() error = %v", errStdin) } if errStart := cmd.Start(); errStart != nil { t.Fatalf("start waiting helper error = %v", errStart) } t.Cleanup(func() { _ = stdin.Close() _ = cmd.Wait() }) if cmd.Process == nil { t.Fatal("waiting helper process is nil") } return cmd, cmd.Process.Pid } func stopWaitingProcess(t *testing.T, cmd *exec.Cmd) { t.Helper() if cmd == nil || cmd.Process == nil { return } _ = cmd.Process.Kill() _ = cmd.Wait() } func TestShadowHelperHoldLockAndExit(t *testing.T) { if os.Getenv("GO_WANT_SHADOW_HOLD_LOCK_EXIT_HELPER") != "1" { t.Skip("crash lock helper entry point") } sourceDir := os.Getenv("CLIPROXY_SHADOW_HELPER_SOURCE_DIR") shadowDir := os.Getenv("CLIPROXY_SHADOW_HELPER_SHADOW_DIR") if sourceDir == "" || shadowDir == "" { os.Exit(2) } // Acquire an exclusive coordination lock and exit abnormally without unlocking. if _, errLock := acquireShadowCoordLock(sourceDir, shadowDir, true, false); errLock != nil { os.Exit(3) } os.Exit(1) } func TestShadowHelperProcessWaits(t *testing.T) { if os.Getenv("CLIPROXY_SHADOW_HELPER_WAIT") != "1" { t.Skip("waiting helper process entry point") } buf := make([]byte, 1) _, _ = os.Stdin.Read(buf) } func countShadowPluginFiles(t *testing.T, dir string) int { t.Helper() entries, errRead := os.ReadDir(dir) if errRead != nil { t.Fatalf("ReadDir(%s) error = %v", dir, errRead) } count := 0 for _, entry := range entries { if strings.HasPrefix(entry.Name(), shadowPluginPrefix) { count++ } if strings.HasPrefix(entry.Name(), shadowPluginTempPrefix) { t.Fatalf("temporary shadow file was not cleaned up: %s", entry.Name()) } } return count }