mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 14:00:09 +02:00
`ensureSymlink` previously short-circuited whenever `dst` already existed
as a regular file ("Regular file exists — don't overwrite"). On Windows
that branch is reachable via the createFileLink copy fallback that fires
when `os.Symlink` is unavailable, so once a per-task `codex-home/auth.json`
was written as a copy it would never be refreshed by subsequent
Prepare/Reuse calls. If the shared `~/.codex/auth.json` rotated (e.g.
Codex Desktop refreshed the token in the background), the daemon kept
handing Codex a now-revoked refresh_token, which the OAuth server
rejected with `refresh_token_reused` / `token_expired`. Renaming the
workspace directory was the only recovery path.
Treat any non-matching dst — wrong-target symlink, broken symlink, or
stale regular file — as something to delete and re-create via
createFileLink, so each Prepare/Reuse mirrors the current shared source.
Add a `logCodexAuthState` info log (file kind, link target, size, mtime —
never contents) so operators chasing the same symptom can see at a glance
whether the per-task home is tracking the shared auth or has drifted.
Tests cover: stale regular-file dst is replaced, copy-fallback dst is
refreshed when the shared source rotates, and a high-level
prepareCodexHome regression simulating the Windows + token-rotation
scenario from issue #2081.
Co-authored-by: multica-agent <github@multica.ai>
247 lines
6.1 KiB
Go
247 lines
6.1 KiB
Go
package execenv
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
)
|
|
|
|
func assertDirLinkTarget(t *testing.T, dst, src string) {
|
|
t.Helper()
|
|
|
|
target, err := os.Readlink(dst)
|
|
if err == nil {
|
|
if target != src {
|
|
t.Errorf("link target = %q, want %q", target, src)
|
|
}
|
|
return
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
if fi, statErr := os.Stat(dst); statErr != nil || !fi.IsDir() {
|
|
t.Fatalf("expected accessible linked directory, stat err: %v", statErr)
|
|
}
|
|
return
|
|
}
|
|
t.Fatalf("Readlink: %v", err)
|
|
}
|
|
|
|
func TestEnsureDirSymlink_CreatesLink(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
src := filepath.Join(dir, "shared-sessions")
|
|
dst := filepath.Join(dir, "task-sessions")
|
|
|
|
if err := ensureDirSymlink(src, dst); err != nil {
|
|
t.Fatalf("ensureDirSymlink: %v", err)
|
|
}
|
|
|
|
// Source dir should be created.
|
|
if fi, err := os.Stat(src); err != nil || !fi.IsDir() {
|
|
t.Fatal("expected source directory to be created")
|
|
}
|
|
|
|
// dst should resolve to src, or be an accessible junction on Windows.
|
|
assertDirLinkTarget(t, dst, src)
|
|
}
|
|
|
|
func TestEnsureDirSymlink_Idempotent(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
src := filepath.Join(dir, "shared")
|
|
dst := filepath.Join(dir, "link")
|
|
|
|
if err := ensureDirSymlink(src, dst); err != nil {
|
|
t.Fatalf("first call: %v", err)
|
|
}
|
|
if err := ensureDirSymlink(src, dst); err != nil {
|
|
t.Fatalf("second call: %v", err)
|
|
}
|
|
|
|
assertDirLinkTarget(t, dst, src)
|
|
}
|
|
|
|
func TestEnsureDirSymlink_ReplacesWrongTarget(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
oldSrc := filepath.Join(dir, "old")
|
|
newSrc := filepath.Join(dir, "new")
|
|
dst := filepath.Join(dir, "link")
|
|
|
|
os.MkdirAll(oldSrc, 0o755)
|
|
if err := os.Symlink(oldSrc, dst); err != nil {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skipf("directory symlink unavailable on this Windows session: %v", err)
|
|
}
|
|
t.Fatalf("seed wrong symlink: %v", err)
|
|
}
|
|
|
|
if err := ensureDirSymlink(newSrc, dst); err != nil {
|
|
t.Fatalf("ensureDirSymlink: %v", err)
|
|
}
|
|
|
|
assertDirLinkTarget(t, dst, newSrc)
|
|
}
|
|
|
|
func TestEnsureDirSymlink_SkipsExistingRegularDir(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
src := filepath.Join(dir, "shared")
|
|
dst := filepath.Join(dir, "existing")
|
|
os.MkdirAll(dst, 0o755)
|
|
|
|
if err := ensureDirSymlink(src, dst); err != nil {
|
|
t.Fatalf("ensureDirSymlink: %v", err)
|
|
}
|
|
|
|
// Should not be replaced — still a regular directory.
|
|
fi, _ := os.Lstat(dst)
|
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|
t.Error("expected regular dir to be preserved, not replaced with symlink")
|
|
}
|
|
}
|
|
|
|
func TestEnsureSymlink_SkipsWhenSourceMissing(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
src := filepath.Join(dir, "missing.json")
|
|
dst := filepath.Join(dir, "link.json")
|
|
|
|
if err := ensureSymlink(src, dst); err != nil {
|
|
t.Fatalf("ensureSymlink: %v", err)
|
|
}
|
|
|
|
if _, err := os.Lstat(dst); !os.IsNotExist(err) {
|
|
t.Error("expected dst to not be created when src is missing")
|
|
}
|
|
}
|
|
|
|
func TestEnsureSymlink_ReplacesStaleRegularFile(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
src := filepath.Join(dir, "source.json")
|
|
dst := filepath.Join(dir, "existing.json")
|
|
os.WriteFile(src, []byte("new"), 0o644)
|
|
os.WriteFile(dst, []byte("old"), 0o644)
|
|
|
|
// Regression for issue #2081: a regular file at dst (e.g. left over from
|
|
// the Windows copy fallback in createFileLink) must be replaced so the
|
|
// per-task home picks up changes to the shared source — otherwise a
|
|
// once-stale auth.json never refreshes across env reuses.
|
|
if err := ensureSymlink(src, dst); err != nil {
|
|
t.Fatalf("ensureSymlink: %v", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Fatalf("read dst: %v", err)
|
|
}
|
|
if string(data) != "new" {
|
|
t.Errorf("dst content = %q, want %q (file should be re-linked/re-copied from src)", data, "new")
|
|
}
|
|
}
|
|
|
|
func TestEnsureSymlink_RefreshesAfterCopyFallbackThenSrcChange(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
src := filepath.Join(dir, "auth.json")
|
|
dst := filepath.Join(dir, "task-auth.json")
|
|
|
|
// Simulate the Windows copy fallback: first link is a copy of v1.
|
|
os.WriteFile(src, []byte(`{"refresh_token":"v1"}`), 0o644)
|
|
if err := copyFile(src, dst); err != nil {
|
|
t.Fatalf("seed copy fallback: %v", err)
|
|
}
|
|
|
|
// Shared source rotates to v2 (e.g. Codex Desktop refreshed the token).
|
|
os.WriteFile(src, []byte(`{"refresh_token":"v2"}`), 0o644)
|
|
|
|
// Reuse path runs ensureSymlink again — expected to refresh dst from src.
|
|
if err := ensureSymlink(src, dst); err != nil {
|
|
t.Fatalf("ensureSymlink: %v", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Fatalf("read dst: %v", err)
|
|
}
|
|
if string(data) != `{"refresh_token":"v2"}` {
|
|
t.Errorf("dst content after refresh = %q, want v2 contents", data)
|
|
}
|
|
}
|
|
|
|
func TestCreateDirLink(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
src := filepath.Join(dir, "src")
|
|
dst := filepath.Join(dir, "dst")
|
|
os.MkdirAll(src, 0o755)
|
|
os.WriteFile(filepath.Join(src, "test.txt"), []byte("hello"), 0o644)
|
|
|
|
if err := createDirLink(src, dst); err != nil {
|
|
t.Fatalf("createDirLink: %v", err)
|
|
}
|
|
|
|
// Should be able to read files through the link.
|
|
data, err := os.ReadFile(filepath.Join(dst, "test.txt"))
|
|
if err != nil {
|
|
t.Fatalf("read through link: %v", err)
|
|
}
|
|
if string(data) != "hello" {
|
|
t.Errorf("content = %q, want %q", data, "hello")
|
|
}
|
|
}
|
|
|
|
func TestCreateFileLink(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
src := filepath.Join(dir, "source.json")
|
|
dst := filepath.Join(dir, "link.json")
|
|
os.WriteFile(src, []byte(`{"key":"value"}`), 0o644)
|
|
|
|
if err := createFileLink(src, dst); err != nil {
|
|
t.Fatalf("createFileLink: %v", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Fatalf("read link: %v", err)
|
|
}
|
|
if string(data) != `{"key":"value"}` {
|
|
t.Errorf("content = %q", data)
|
|
}
|
|
}
|
|
|
|
func TestCopyFile(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
src := filepath.Join(dir, "src.txt")
|
|
dst := filepath.Join(dir, "dst.txt")
|
|
os.WriteFile(src, []byte("content"), 0o644)
|
|
|
|
if err := copyFile(src, dst); err != nil {
|
|
t.Fatalf("copyFile: %v", err)
|
|
}
|
|
|
|
data, _ := os.ReadFile(dst)
|
|
if string(data) != "content" {
|
|
t.Errorf("content = %q", data)
|
|
}
|
|
|
|
// Verify it's a copy, not a symlink.
|
|
fi, _ := os.Lstat(dst)
|
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|
t.Error("expected regular file, not symlink")
|
|
}
|
|
}
|