| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "path/filepath" |
| 6 | "testing" |
| 7 | |
| 8 | "reasonix/internal/agent" |
| 9 | "reasonix/internal/control" |
| 10 | ) |
| 11 | |
| 12 | func TestBindAndLoadCLIResumeHoldsLeaseBeforeReading(t *testing.T) { |
| 13 | path := filepath.Join(t.TempDir(), "resume.jsonl") |
| 14 | saveTestSession(t, path, "latest prompt") |
| 15 | |
| 16 | leases := control.NewSessionLeaseKeeper() |
| 17 | defer leases.Release() |
| 18 | loaded, err := bindAndLoadCLIResume(leases, path, func(candidate string) (*agent.Session, error) { |
| 19 | contender, acquireErr := agent.TryAcquireSessionLease(candidate) |
| 20 | if contender != nil { |
| 21 | contender.Release() |
| 22 | } |
| 23 | if !errors.Is(acquireErr, agent.ErrSessionLeaseHeld) { |
| 24 | t.Fatalf("loader observed lease error %v, want held", acquireErr) |
| 25 | } |
| 26 | return agent.LoadSession(candidate) |
| 27 | }) |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | if loaded == nil { |
| 32 | t.Fatal("resume returned a nil session") |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | func TestBindAndLoadCLIResumeDoesNotReadHeldSession(t *testing.T) { |
| 37 | path := filepath.Join(t.TempDir(), "held.jsonl") |
| 38 | holder, err := agent.TryAcquireSessionLease(path) |
| 39 | if err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | defer holder.Release() |
| 43 | |
| 44 | leases := control.NewSessionLeaseKeeper() |
| 45 | defer leases.Release() |
| 46 | called := false |
| 47 | _, err = bindAndLoadCLIResume(leases, path, func(string) (*agent.Session, error) { |
| 48 | called = true |
| 49 | return nil, nil |
| 50 | }) |
| 51 | if !errors.Is(err, agent.ErrSessionLeaseHeld) { |
| 52 | t.Fatalf("bindAndLoadCLIResume error = %v, want held", err) |
| 53 | } |
| 54 | if called { |
| 55 | t.Fatal("held session was read before takeover acquired its lease") |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func TestCommitSessionSwitchAcquiresTargetBeforeLoading(t *testing.T) { |
| 60 | dir := t.TempDir() |
| 61 | source := filepath.Join(dir, "source.jsonl") |
| 62 | target := filepath.Join(dir, "target.jsonl") |
| 63 | leases := control.NewSessionLeaseKeeper() |
| 64 | defer leases.Release() |
| 65 | if err := leases.Rebind(source); err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | m := &chatTUI{leases: leases} |
| 69 | wantErr := errors.New("stop after ownership check") |
| 70 | err := m.commitSessionSwitchWithLoader(target, func(candidate string) (*agent.Session, error) { |
| 71 | contender, acquireErr := agent.TryAcquireSessionLease(candidate) |
| 72 | if contender != nil { |
| 73 | contender.Release() |
| 74 | } |
| 75 | if !errors.Is(acquireErr, agent.ErrSessionLeaseHeld) { |
| 76 | t.Fatalf("switch loader observed lease error %v, want held", acquireErr) |
| 77 | } |
| 78 | return nil, wantErr |
| 79 | }) |
| 80 | if !errors.Is(err, wantErr) { |
| 81 | t.Fatalf("commitSessionSwitch error = %v, want %v", err, wantErr) |
| 82 | } |
| 83 | if got := leases.HeldPath(); got != agent.CanonicalSessionPath(source) { |
| 84 | t.Fatalf("source lease was not restored after load failure: %q", got) |
| 85 | } |
| 86 | } |
| 87 |