From 43d9e34cc885fbeb665302be0dde119b6b003a56 Mon Sep 17 00:00:00 2001 From: Gadi Evron Date: Thu, 28 May 2026 12:55:51 -0700 Subject: [PATCH] fix(cli/runtime): key dependency-staleness on corePath, not only the pyproject hash The managed Python venv is a single global path (~/.openant/venv) shared by every worktree. The staleness check (depsStalenessAt) keyed only on pyproject.toml content, so two worktrees with identical pyproject.toml were considered "up to date" even though the venv's editable install (`pip install -e `) pointed at whichever worktree installed last -- a binary built in worktree B would silently import worktree A's Python source. Introduce depsHash(corePath) = sha256(corePath + "\0" + pyproject.toml contents) and use it for both the install baseline stamp and the staleness comparison. A change of corePath (switching worktrees) now reads as stale and triggers a reinstall that re-points the editable install at the active source. (hashFile remains a tested file-hash utility, no longer on the staleness path.) Tests: internal/python/runtime_corepath_test.go (two worktrees with byte-identical pyproject but different corePath -> the second is detected as stale; same corePath -> not stale). RED 1 failed -> GREEN; go test ./internal/python/ ok (existing staleness + hashFile tests unaffected); gofmt + vet clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/openant-cli/internal/python/runtime.go | 20 +++++-- .../internal/python/runtime_corepath_test.go | 53 +++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 apps/openant-cli/internal/python/runtime_corepath_test.go diff --git a/apps/openant-cli/internal/python/runtime.go b/apps/openant-cli/internal/python/runtime.go index ddaa67e7..3a32ad22 100644 --- a/apps/openant-cli/internal/python/runtime.go +++ b/apps/openant-cli/internal/python/runtime.go @@ -201,8 +201,7 @@ func CheckOpenantInstalled(pythonPath string) error { } // Save dependency hash so CheckDepsStale knows this is the baseline. - pyprojectPath := filepath.Join(corePath, "pyproject.toml") - if h, err := hashFile(pyprojectPath); err == nil { + if h, err := depsHash(corePath); err == nil { if err := writeStoredHash(h); err != nil { fmt.Fprintf(os.Stderr, "warning: could not save dependency hash at %s: %v (next run may reinstall)\n", @@ -285,13 +284,26 @@ func readStoredHash() string { return readHashAt(depsHashPath()) } // writeStoredHash saves the dependency hash to the venv marker file. func writeStoredHash(hash string) error { return writeHashAt(depsHashPath(), hash) } +// depsHash keys the dependency stamp on BOTH pyproject.toml contents AND corePath (the +// editable-install source). The managed venv is a single global path shared across worktrees; +// without corePath in the key, two worktrees with identical pyproject.toml share one editable +// install and a binary built in one silently imports the other's source. Including corePath forces +// a reinstall (re-pointing the editable install) when the active source changes. +func depsHash(corePath string) (string, error) { + pyproject, err := os.ReadFile(filepath.Join(corePath, "pyproject.toml")) + if err != nil { + return "", err + } + sum := sha256.Sum256(append([]byte(corePath+"\x00"), pyproject...)) + return hex.EncodeToString(sum[:]), nil +} + // depsStalenessAt inspects pyproject.toml at corePath and the hash stored at // hashPath, and reports whether a reinstall is needed. The boolean is true // when deps are stale (i.e. the hash differs and a reinstall is warranted). // The caller is expected to skip the check on any error. func depsStalenessAt(corePath, hashPath string) (stale bool, currentHash string, err error) { - pyprojectPath := filepath.Join(corePath, "pyproject.toml") - currentHash, err = hashFile(pyprojectPath) + currentHash, err = depsHash(corePath) if err != nil { return false, "", err } diff --git a/apps/openant-cli/internal/python/runtime_corepath_test.go b/apps/openant-cli/internal/python/runtime_corepath_test.go new file mode 100644 index 00000000..90bdb269 --- /dev/null +++ b/apps/openant-cli/internal/python/runtime_corepath_test.go @@ -0,0 +1,53 @@ +package python + +import ( + "os" + "path/filepath" + "testing" +) + +// Regression test: the dependency-staleness check keyed only on +// pyproject.toml content, blind to corePath. The managed venv is a single global path shared across +// all worktrees; with two worktrees whose pyproject.toml is identical, the staleness check reports +// "not stale" even though the venv's editable install (`pip install -e `) points at the +// OTHER worktree's source — so a binary built in worktree B silently imports worktree A's Python. +// Fix: key staleness on corePath as well, so switching the editable-install source is detected. +func TestDepsStalenessDetectsCorePathChange(t *testing.T) { + const pyproject = "[project]\nname = \"openant\"\nversion = \"1.0.0\"\n" + + coreA := t.TempDir() + coreB := t.TempDir() + if err := os.WriteFile(filepath.Join(coreA, "pyproject.toml"), []byte(pyproject), 0644); err != nil { + t.Fatal(err) + } + // coreB has BYTE-IDENTICAL pyproject.toml — only the corePath differs. + if err := os.WriteFile(filepath.Join(coreB, "pyproject.toml"), []byte(pyproject), 0644); err != nil { + t.Fatal(err) + } + + hashPath := filepath.Join(t.TempDir(), ".deps-hash") + + // Baseline: install was done from coreA -> stamp coreA's hash. + _, hashA, err := depsStalenessAt(coreA, hashPath) + if err != nil { + t.Fatalf("depsStalenessAt(coreA): %v", err) + } + if err := writeHashAt(hashPath, hashA); err != nil { + t.Fatalf("writeHashAt: %v", err) + } + + // Same corePath -> not stale. + if staleA, _, _ := depsStalenessAt(coreA, hashPath); staleA { + t.Fatalf("same corePath reported stale (should be up-to-date)") + } + + // Different corePath, identical pyproject -> MUST be stale: otherwise the two worktrees share + // one editable install and the binary imports the wrong source. + staleB, _, err := depsStalenessAt(coreB, hashPath) + if err != nil { + t.Fatalf("depsStalenessAt(coreB): %v", err) + } + if !staleB { + t.Fatalf("corePath change (identical pyproject) not detected as stale -> worktrees share the editable install") + } +}