返回 DeepSeek-Reasonix
controller_lock_test.go
根目录 / internal / control / controller_lock_test.go
1 package control
2
3 import (
4 "context"
5 "strings"
6 "sync"
7 "testing"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/event"
11 "reasonix/internal/provider"
12 )
13
14 // TestCompactRefusedWhileRunning locks in the same guard Rewind/Branch have:
15 // the run loop is the only sanctioned writer of the live session during a
16 // turn, so a manual compact must be refused instead of rewriting the log
17 // underneath it.
18 func TestCompactRefusedWhileRunning(t *testing.T) {
19 sess := agent.NewSession("sys")
20 sess.Add(provider.Message{Role: provider.RoleUser, Content: "hi"})
21 exec := agent.New(nil, nil, sess, agent.Options{}, event.Discard)
22 c := New(Options{
23 Executor: exec,
24 SessionDir: t.TempDir(),
25 Label: "test",
26 Sink: event.Discard,
27 })
28
29 c.mu.Lock()
30 c.running = true
31 c.mu.Unlock()
32
33 err := c.Compact(context.Background(), "")
34 if err == nil {
35 t.Fatal("Compact while running should be refused")
36 }
37 if !strings.Contains(err.Error(), "cannot compact") {
38 t.Fatalf("err = %v, want 'cannot compact' guard error", err)
39 }
40 }
41
42 // TestRewindConcurrentWithHistoryReads exercises the conversation-rewind
43 // truncation against parallel History/CheckpointHasBoundary readers; before
44 // Rewind switched to Session.Snapshot/Replace the bare
45 // `s.Messages = s.Messages[:boundary]` write raced them (caught by -race).
46 func TestRewindConcurrentWithHistoryReads(t *testing.T) {
47 c, ag, _ := runTwoTurns(t)
48
49 c.checkpoints.mu.Lock()
50 lastTurn := c.checkpoints.turn - 1
51 c.checkpoints.mu.Unlock()
52
53 stop := make(chan struct{})
54 var wg sync.WaitGroup
55 for i := 0; i < 4; i++ {
56 wg.Add(1)
57 go func() {
58 defer wg.Done()
59 for {
60 select {
61 case <-stop:
62 return
63 default:
64 _ = c.History()
65 _ = c.CheckpointHasBoundary(lastTurn)
66 }
67 }
68 }()
69 }
70
71 err := c.Rewind(lastTurn, RewindConversation)
72 close(stop)
73 wg.Wait()
74 if err != nil {
75 t.Fatalf("Rewind: %v", err)
76 }
77
78 // The rewind truncated the log back to the last turn's boundary; History
79 // must still serve a consistent snapshot afterwards.
80 if got, want := ag.Session().Len(), 3; got != want { // sys + first prompt/answer
81 t.Fatalf("messages after rewind = %d, want %d", got, want)
82 }
83 }
84
84 lines GO