Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions apps/openant-cli/internal/python/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
Expand Down
53 changes: 53 additions & 0 deletions apps/openant-cli/internal/python/runtime_corepath_test.go
Original file line number Diff line number Diff line change
@@ -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 <corePath>`) 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")
}
}
Loading