138 lines
4.8 KiB
Go
138 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"reasonix/internal/agent"
|
|
)
|
|
|
|
// The lease registry folds session paths through agent.CanonicalSessionPath
|
|
// (lowercased on Windows). Every desktop "same session" comparison must fold
|
|
// the same way, or a lease the tab itself holds looks foreign and every
|
|
// model/effort/token switch self-locks with the "already open in another
|
|
// window" error (#5999, #6006, #5996).
|
|
|
|
func TestSessionRuntimeKeyMatchesLeaseKey(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Mixed-case segment on every platform; on Windows the lease key folds it.
|
|
path := filepath.Join(dir, "Sessions-Dir", "20260705-Test.jsonl")
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
lease, err := agent.TryAcquireSessionLease(path)
|
|
if err != nil {
|
|
t.Fatalf("acquire lease: %v", err)
|
|
}
|
|
defer lease.Release()
|
|
|
|
if got, want := sessionRuntimeKey(lease.Path()), sessionRuntimeKey(path); got != want {
|
|
t.Fatalf("lease path key %q != session path key %q; lease reuse checks will self-lock", got, want)
|
|
}
|
|
}
|
|
|
|
func TestSessionRuntimeKeyIsIdempotent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "Case-Mix", "20260705-Test.jsonl")
|
|
key := sessionRuntimeKey(path)
|
|
if key == "" {
|
|
t.Fatal("empty key for non-empty path")
|
|
}
|
|
if again := sessionRuntimeKey(key); again != key {
|
|
t.Fatalf("sessionRuntimeKey not idempotent: %q -> %q", key, again)
|
|
}
|
|
}
|
|
|
|
func TestSessionRuntimeKeyFoldsCaseOnWindows(t *testing.T) {
|
|
if runtime.GOOS != "windows" {
|
|
t.Skip("case-insensitive path identity is Windows-only")
|
|
}
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "Sessions", "20260705-Test.jsonl")
|
|
upper := strings.ToUpper(path)
|
|
if sessionRuntimeKey(path) != sessionRuntimeKey(upper) {
|
|
t.Fatalf("case variants of one file produced distinct keys: %q vs %q",
|
|
sessionRuntimeKey(path), sessionRuntimeKey(upper))
|
|
}
|
|
}
|
|
|
|
func TestCanonicalTabSessionPathNormalizesOutsideSessionDir(t *testing.T) {
|
|
// Project-scope sessions live outside config.SessionDir(); the fallback
|
|
// must still clean the shape so one file cannot split into two keys.
|
|
dir := t.TempDir()
|
|
base := filepath.Join(dir, "projects", "p1", "sessions", "20260705-a.jsonl")
|
|
messy := filepath.Join(dir, "projects", "p1", ".", "sessions") + string(filepath.Separator) + "20260705-a.jsonl"
|
|
if sessionRuntimeKey(base) != sessionRuntimeKey(messy) {
|
|
t.Fatalf("path shape variants split keys: %q vs %q",
|
|
sessionRuntimeKey(base), sessionRuntimeKey(messy))
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionLeaseReusesHeldLease(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "Sessions", "20260705-Reuse.jsonl")
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
tab := &WorkspaceTab{ID: "tab_reuse"}
|
|
if err := tab.ensureSessionLease(path); err != nil {
|
|
t.Fatalf("first ensure: %v", err)
|
|
}
|
|
defer tab.releaseSessionLease()
|
|
|
|
acquires := 0
|
|
sessionLeaseAcquireHookForTest = func() { acquires++ }
|
|
defer func() { sessionLeaseAcquireHookForTest = nil }()
|
|
|
|
// Same path again — and the lease's own (canonical) form: both must hit
|
|
// the fast path instead of re-acquiring against our own registry entry.
|
|
if err := tab.ensureSessionLease(path); err != nil {
|
|
t.Fatalf("re-ensure same path: %v", err)
|
|
}
|
|
if err := tab.ensureSessionLease(tab.sessionLeaseRuntimeKey()); err != nil {
|
|
t.Fatalf("re-ensure canonical form: %v", err)
|
|
}
|
|
if acquires != 0 {
|
|
t.Fatalf("expected fast-path reuse, got %d new acquires", acquires)
|
|
}
|
|
}
|
|
|
|
func TestCanReclaimAllowsMissingLeaseInfo(t *testing.T) {
|
|
// lease.json deleted or torn: Info is nil but nothing actually holds the
|
|
// session. The reclaim attempt must be allowed — the OS lock arbitrates —
|
|
// instead of wedging the session as busy on missing metadata (#5999's
|
|
// fresh-install variant).
|
|
app := NewApp()
|
|
tab := &WorkspaceTab{ID: "tab_nil_info"}
|
|
app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
|
|
path := filepath.Join(t.TempDir(), "Sessions", "20260705-a.jsonl")
|
|
|
|
errNil := &agent.SessionLeaseError{Path: path}
|
|
if !app.canReclaimCurrentProcessSessionLease(tab, path, errNil) {
|
|
t.Fatal("nil-Info lease error must allow a reclaim attempt")
|
|
}
|
|
|
|
foreign := &agent.SessionLeaseError{Path: path, Info: &agent.SessionLeaseInfo{
|
|
SessionPath: path, WriterID: "other-host-1-deadbeef", PID: os.Getpid() + 1,
|
|
}}
|
|
if app.canReclaimCurrentProcessSessionLease(tab, path, foreign) {
|
|
t.Fatal("foreign-holder lease error must not allow reclaim")
|
|
}
|
|
}
|
|
|
|
func TestSessionLeaseBusyErrorOmitsSettingClause(t *testing.T) {
|
|
generic := (&sessionLeaseBusyError{}).Error()
|
|
if strings.Contains(generic, "before changing") {
|
|
t.Fatalf("empty-setting busy error still names a setting: %q", generic)
|
|
}
|
|
withSetting := (&sessionLeaseBusyError{setting: "model"}).Error()
|
|
if !strings.Contains(withSetting, "before changing model") {
|
|
t.Fatalf("setting busy error lost its clause: %q", withSetting)
|
|
}
|
|
}
|