返回 DeepSeek-Reasonix
session_turn_markers_test.go
根目录 / internal / agent / session_turn_markers_test.go
1 package agent
2
3 import (
4 "strings"
5 "testing"
6 "time"
7
8 "reasonix/internal/provider"
9 )
10
11 func TestDAGTurnMarkersRideTheNextSave(t *testing.T) {
12 path := dagTestSession(t)
13 s := dagSavedSession(t, path, "q1", "a1")
14 if !s.QueueTurnBegin("turn-1", true) {
15 t.Fatal("schema-2 session must queue turn markers")
16 }
17 s.Add(provider.Message{Role: provider.RoleUser, Content: "q2"})
18 s.Add(provider.Message{Role: provider.RoleAssistant, Content: "a2"})
19 if err := s.Save(path); err != nil {
20 t.Fatal(err)
21 }
22 types := dagEntryTypes(t, path)
23 if got := strings.Join(types[len(types)-3:], ","); got != "message,message,turn_begin" {
24 t.Fatalf("tail entries = %v", types)
25 }
26 st := dagReplay(t, path)
27 turn := st.heads[SessionMainHead].openTurn
28 if turn == nil || turn.turn != "turn-1" || turn.leaf != s.Messages[2].ID || !turn.preserveUser {
29 t.Fatalf("open turn = %+v, want leaf before the turn", turn)
30 }
31 reloaded, err := LoadSession(path)
32 if err != nil {
33 t.Fatal(err)
34 }
35 if open, ok := reloaded.OpenTurn(); !ok || open.TurnID != "turn-1" || open.LeafID != s.Messages[2].ID || open.HeadID != SessionMainHead {
36 t.Fatalf("OpenTurn after reload = %+v ok=%v", open, ok)
37 }
38 // Ending the turn rides a save even when no message changed.
39 if !s.QueueTurnEnd("turn-1") {
40 t.Fatal("queue end")
41 }
42 if err := s.Save(path); err != nil {
43 t.Fatal(err)
44 }
45 types = dagEntryTypes(t, path)
46 if types[len(types)-1] != sessionDAGTypeTurnEnd {
47 t.Fatalf("tail entries = %v", types)
48 }
49 reloaded, err = LoadSession(path)
50 if err != nil {
51 t.Fatal(err)
52 }
53 if _, ok := reloaded.OpenTurn(); ok {
54 t.Fatal("turn must be closed after turn_end")
55 }
56 if _, ok := s.OpenTurn(); ok {
57 t.Fatal("live session must forget the closed turn")
58 }
59 }
60
61 func TestDAGTurnMarkersAreNotQueuedForSchemaOne(t *testing.T) {
62 useSchemaOneLog(t)
63 path := dagTestSession(t)
64 s := dagSavedSession(t, path, "q1")
65 if s.QueueTurnBegin("turn-1", true) || s.QueueTurnEnd("turn-1") {
66 t.Fatal("schema-1 sessions keep the sidecar marker")
67 }
68 }
69
70 func TestDAGTurnContinuedOnOtherHead(t *testing.T) {
71 path := dagTestSession(t)
72 a := dagSavedSession(t, path, "q1", "a1")
73 b, err := LoadSession(path)
74 if err != nil {
75 t.Fatal(err)
76 }
77 leaf := a.LeafID()
78 a.QueueTurnBegin("turn-a", true)
79 a.Add(provider.Message{Role: provider.RoleUser, Content: "q2"})
80 if err := a.Save(path); err != nil {
81 t.Fatal(err)
82 }
83 b.Add(provider.Message{Role: provider.RoleUser, Content: "q2-b"})
84 if err := b.Save(path); err != nil {
85 t.Fatal(err)
86 }
87 reloadedA, err := LoadSession(path)
88 if err != nil {
89 t.Fatal(err)
90 }
91 // LoadSession opens the newest head (b's fork); a's open turn is on main.
92 if ref, _ := reloadedA.Head(); ref.HeadID == SessionMainHead {
93 t.Fatalf("expected newest head to be the fork, got %+v", ref)
94 }
95 if !reloadedA.TurnContinuedOnOtherHead(leaf) {
96 t.Fatal("a message on another head descends from the turn's leaf")
97 }
98 if reloadedA.TurnContinuedOnOtherHead("nope") {
99 t.Fatal("unknown leaf must not count as continued")
100 }
101 if events := reloadedA.DrainHeadEvents(); len(events) != 1 || events[0].Kind != HeadEventMultipleRecentHeads {
102 t.Fatalf("load events = %+v", events)
103 }
104 }
105
106 func TestLoadHeadEventsIgnoresStaleHeads(t *testing.T) {
107 path := dagTestSession(t)
108 _, base := dagLinearLog(t, path)
109 dagAppend(t, path,
110 sessionDAGEntry{Type: sessionDAGTypeFork, Head: SessionMainHead, NewHead: "old", From: "U1", Kind: HeadKindConcurrent, At: base.Add(-48 * time.Hour)},
111 )
112 st := dagReplay(t, path)
113 if events := loadHeadEvents(st, st.selectedHead()); len(events) != 0 {
114 t.Fatalf("a head idle for two days must not trigger a notice: %+v", events)
115 }
116 dagAppend(t, path,
117 sessionDAGEntry{Type: sessionDAGTypeFork, Head: SessionMainHead, NewHead: "fresh", From: "U1", Kind: HeadKindConcurrent, At: base.Add(-time.Hour)},
118 )
119 st = dagReplay(t, path)
120 if events := loadHeadEvents(st, st.selectedHead()); len(events) != 1 || events[0].HeadID != SessionMainHead {
121 t.Fatalf("events = %+v", events)
122 }
123 }
124
124 lines GO