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