Files
multica/server/internal/daemon/execenv/codex_session_writer_lock_test.go
55c40d5db9 MUL-7982: resume Codex across continued local worktrees (#9218)
* fix(daemon): resume Codex across continued local worktrees

MUL-7982

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): serialize Codex session store writers

(cherry picked from commit 7cbe282e1ef5fa8d44e1cfd04def683ad259a360)
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: mameikagou <[email protected]>
2026-10-13 15:05:14 +08:00

110 lines
2.7 KiB
Go

package execenv
import (
"context"
"errors"
"path/filepath"
"testing"
"time"
)
func TestCodexSessionWriterLockSerializesOneStore(t *testing.T) {
t.Setenv("CODEX_HOME", t.TempDir())
store := filepath.Join(t.TempDir(), "sessions")
first, err := AcquireCodexSessionWriterLock(context.Background(), store, nil)
if err != nil {
t.Fatalf("acquire first lock: %v", err)
}
defer first.Release()
waiting := make(chan struct{})
acquired := make(chan *CodexSessionWriterLock, 1)
errs := make(chan error, 1)
go func() {
lock, err := AcquireCodexSessionWriterLock(context.Background(), store, func() { close(waiting) })
if err != nil {
errs <- err
return
}
acquired <- lock
}()
select {
case <-waiting:
case <-time.After(2 * time.Second):
t.Fatal("second writer never reported waiting")
}
select {
case lock := <-acquired:
lock.Release()
t.Fatal("second writer acquired while the first still held the store")
case err := <-errs:
t.Fatalf("second writer failed while waiting: %v", err)
case <-time.After(75 * time.Millisecond):
}
first.Release()
select {
case lock := <-acquired:
lock.Release()
case err := <-errs:
t.Fatalf("second writer failed after release: %v", err)
case <-time.After(2 * time.Second):
t.Fatal("second writer did not acquire after release")
}
}
func TestCodexSessionWriterLockCancellationStopsWait(t *testing.T) {
t.Setenv("CODEX_HOME", t.TempDir())
store := filepath.Join(t.TempDir(), "sessions")
first, err := AcquireCodexSessionWriterLock(context.Background(), store, nil)
if err != nil {
t.Fatalf("acquire first lock: %v", err)
}
defer first.Release()
ctx, cancel := context.WithCancel(context.Background())
waiting := make(chan struct{})
done := make(chan error, 1)
go func() {
_, err := AcquireCodexSessionWriterLock(ctx, store, func() { close(waiting) })
done <- err
}()
select {
case <-waiting:
case <-time.After(2 * time.Second):
t.Fatal("second writer never reported waiting")
}
cancel()
select {
case err := <-done:
if !errors.Is(err, context.Canceled) {
t.Fatalf("wait error = %v, want context.Canceled", err)
}
case <-time.After(2 * time.Second):
t.Fatal("cancelled writer did not stop waiting")
}
}
func TestCodexSessionWriterLockDoesNotSerializeDifferentStores(t *testing.T) {
t.Setenv("CODEX_HOME", t.TempDir())
root := t.TempDir()
first, err := AcquireCodexSessionWriterLock(context.Background(), filepath.Join(root, "one"), nil)
if err != nil {
t.Fatalf("acquire first store: %v", err)
}
defer first.Release()
second, err := AcquireCodexSessionWriterLock(context.Background(), filepath.Join(root, "two"), func() {
t.Fatal("different stores must not contend")
})
if err != nil {
t.Fatalf("acquire second store: %v", err)
}
second.Release()
}