| 1 | package acp |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "reasonix/internal/agent" |
| 8 | "reasonix/internal/permission" |
| 9 | "reasonix/internal/provider" |
| 10 | ) |
| 11 | |
| 12 | func leaseTestFactory(dir string) *e2eFactory { |
| 13 | return &e2eFactory{ |
| 14 | prov: &scriptedProvider{name: "fake", responses: [][]provider.Chunk{ |
| 15 | {{Type: provider.ChunkText, Text: "unused"}, {Type: provider.ChunkDone}}, |
| 16 | }}, |
| 17 | tool: fakeTool{name: "peek", ro: true, out: "unused"}, |
| 18 | policy: permission.New("ask", nil, nil, nil), |
| 19 | sessionDir: dir, |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | // TestSessionLoadRefusedWhenLeaseHeld proves session/load returns a protocol |
| 24 | // error naming the holder when the transcript is owned by another runtime |
| 25 | // (a desktop window or CLI writing the same session), and does not register |
| 26 | // the session. |
| 27 | func TestSessionLoadRefusedWhenLeaseHeld(t *testing.T) { |
| 28 | dir := t.TempDir() |
| 29 | const id = "sess-held-lease" |
| 30 | path := transcriptPath(dir, id) |
| 31 | s := agent.NewSession("sys") |
| 32 | s.Add(provider.Message{Role: provider.RoleUser, Content: "editor session"}) |
| 33 | if err := s.Save(path); err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | |
| 37 | holder, err := agent.TryAcquireSessionLease(path) |
| 38 | if err != nil { |
| 39 | t.Fatalf("test holder acquire: %v", err) |
| 40 | } |
| 41 | defer holder.Release() |
| 42 | |
| 43 | client, stop := startServer(t, leaseTestFactory(dir)) |
| 44 | defer stop() |
| 45 | client.call(t, "initialize", InitializeParams{ProtocolVersion: 1}) |
| 46 | |
| 47 | resp := client.call(t, "session/load", SessionLoadParams{SessionID: id, Cwd: t.TempDir()}) |
| 48 | if resp.Error == nil { |
| 49 | t.Fatalf("session/load of a held session succeeded, want protocol error") |
| 50 | } |
| 51 | if !strings.Contains(resp.Error.Message, "in use by another Reasonix") { |
| 52 | t.Fatalf("session/load error = %q, want holder wording", resp.Error.Message) |
| 53 | } |
| 54 | if strings.Contains(resp.Error.Message, path) { |
| 55 | t.Fatalf("session/load error leaks the session path: %q", resp.Error.Message) |
| 56 | } |
| 57 | |
| 58 | // The refused load must not have bound the session: a prompt is unknown. |
| 59 | promptResp := client.call(t, "session/prompt", SessionPromptParams{ |
| 60 | SessionID: id, |
| 61 | Prompt: []ContentBlock{{Type: "text", Text: "should not run"}}, |
| 62 | }) |
| 63 | if promptResp.Error == nil { |
| 64 | t.Fatalf("session/prompt after refused load succeeded, want unknown session") |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // TestSessionCloseReleasesLease proves the lease taken by session/new is |
| 69 | // released by session/close, so another runtime can bind the transcript. |
| 70 | func TestSessionCloseReleasesLease(t *testing.T) { |
| 71 | dir := t.TempDir() |
| 72 | client, stop := startServer(t, leaseTestFactory(dir)) |
| 73 | defer stop() |
| 74 | |
| 75 | sid := openSession(t, client) |
| 76 | path := transcriptPath(dir, sid) |
| 77 | |
| 78 | // Held while the session is open. |
| 79 | if lease, err := agent.TryAcquireSessionLease(path); err == nil { |
| 80 | lease.Release() |
| 81 | t.Fatalf("transcript lease not held after session/new") |
| 82 | } |
| 83 | |
| 84 | resp := client.call(t, "session/close", SessionCloseParams{SessionID: sid}) |
| 85 | if resp.Error != nil { |
| 86 | t.Fatalf("session/close errored: %+v", resp.Error) |
| 87 | } |
| 88 | lease, err := agent.TryAcquireSessionLease(path) |
| 89 | if err != nil { |
| 90 | t.Fatalf("transcript lease not released by session/close: %v", err) |
| 91 | } |
| 92 | lease.Release() |
| 93 | } |
| 94 |