返回 DeepSeek-Reasonix
session_context_test.go
根目录 / internal / control / session_context_test.go
1 package control
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "slices"
8 "strings"
9 "testing"
10
11 "reasonix/internal/agent"
12 "reasonix/internal/event"
13 "reasonix/internal/memory"
14 "reasonix/internal/provider"
15 "reasonix/internal/sessioncontext"
16 "reasonix/internal/skill"
17 "reasonix/internal/tool"
18 )
19
20 func TestTurnContextUsesLiveSkillCatalogAcrossAddEditDelete(t *testing.T) {
21 project := t.TempDir()
22 home := t.TempDir()
23 writeControlSkill(t, project, ".reasonix/skills/alpha/SKILL.md", "---\ndescription: alpha one\n---\nbody")
24 store := skill.New(skill.Options{HomeDir: home, ProjectRoot: project, DisableBuiltins: true})
25 sess := agent.NewSession("stable system")
26 executor := agent.New(nil, tool.NewRegistry(), sess, agent.Options{}, event.Discard)
27 c := newOwnedTestController(t, Options{
28 Executor: executor, SkillStore: store, Skills: store.List(),
29 SessionContextStatic: sessioncontext.Sections{Workspace: "workspace"},
30 })
31
32 appendCurrent := func() sessioncontext.Snapshot {
33 t.Helper()
34 if !executor.AppendTurnContext(c.withTurnContext(context.Background(), true)) {
35 t.Fatal("expected a replacement context")
36 }
37 for i := range slices.Backward(sess.Messages) {
38 if snapshot, ok := sessioncontext.Parse(sess.Messages[i].Content); ok {
39 return snapshot
40 }
41 }
42 t.Fatal("no context found")
43 return sessioncontext.Snapshot{}
44 }
45
46 first := appendCurrent()
47 if !strings.Contains(first.Sections.SkillsCatalog, "alpha one") {
48 t.Fatalf("first catalog = %q", first.Sections.SkillsCatalog)
49 }
50 betaPath := filepath.Join(project, ".reasonix", "skills", "beta", "SKILL.md")
51 writeControlSkill(t, project, ".reasonix/skills/beta/SKILL.md", "---\ndescription: beta one\n---\nbody")
52 store.Invalidate("test skill added")
53 second := appendCurrent()
54 if second.Digest == first.Digest || !strings.Contains(second.Sections.SkillsCatalog, "beta one") {
55 t.Fatalf("added-skill snapshot = %+v", second)
56 }
57 writeControlSkill(t, project, ".reasonix/skills/beta/SKILL.md", "---\ndescription: beta edited\n---\nbody")
58 store.Invalidate("test skill edited")
59 third := appendCurrent()
60 if third.Digest == second.Digest || !strings.Contains(third.Sections.SkillsCatalog, "beta edited") {
61 t.Fatalf("edited-skill snapshot = %+v", third)
62 }
63 if err := os.Remove(betaPath); err != nil {
64 t.Fatal(err)
65 }
66 store.Invalidate("test skill deleted")
67 fourth := appendCurrent()
68 if fourth.Digest == third.Digest || strings.Contains(fourth.Sections.SkillsCatalog, "beta") {
69 t.Fatalf("deleted-skill snapshot = %+v", fourth)
70 }
71 if got := sessionContextCount(sess.Messages); got != 4 {
72 t.Fatalf("context count = %d, want one full snapshot per change", got)
73 }
74 if executor.AppendTurnContext(c.withTurnContext(context.Background(), true)) {
75 t.Fatal("unchanged live catalog should deduplicate")
76 }
77 }
78
79 func TestTurnContextPublishesBackgroundMemoryReplacementWithoutLegacyUpdate(t *testing.T) {
80 root := t.TempDir()
81 userDir := filepath.Join(root, "user")
82 project := filepath.Join(root, "project")
83 if err := os.MkdirAll(project, 0o755); err != nil {
84 t.Fatal(err)
85 }
86 mem := memory.Load(memory.Options{CWD: project, UserDir: userDir})
87 sess := agent.NewSession("stable system")
88 executor := agent.New(nil, tool.NewRegistry(), sess, agent.Options{}, event.Discard)
89 c := newOwnedTestController(t, Options{Executor: executor, Memory: mem, SessionContextStatic: sessioncontext.Sections{Workspace: "workspace"}})
90 if !executor.AppendTurnContext(c.withTurnContext(context.Background(), true)) {
91 t.Fatal("initial runtime snapshot was not published")
92 }
93 if _, err := c.SaveMemory(memory.Memory{
94 Name: "currency", Description: "balance currency", Scope: memory.FactScopeGlobal,
95 Type: memory.TypeUser, Body: "Use RMB.",
96 }); err != nil {
97 t.Fatal(err)
98 }
99 if composed := c.Compose("hello"); strings.Contains(composed, "<memory-update>") {
100 t.Fatalf("background save generated legacy update: %q", composed)
101 }
102 if !executor.AppendTurnContext(c.withTurnContext(context.Background(), true)) {
103 t.Fatal("memory change did not publish a context")
104 }
105 snapshot, ok := latestControlSessionContext(sess.Messages)
106 if !ok || !strings.Contains(snapshot.Sections.BackgroundMemory, "currency") || !strings.Contains(snapshot.Sections.BackgroundMemory, "Use RMB.") {
107 t.Fatalf("memory snapshot = %+v", snapshot)
108 }
109 if err := c.ForgetMemory("currency"); err != nil {
110 t.Fatal(err)
111 }
112 if !executor.AppendTurnContext(c.withTurnContext(context.Background(), true)) {
113 t.Fatal("forget did not publish a replacement context")
114 }
115 latest, ok := latestControlSessionContext(sess.Messages)
116 if !ok || strings.Contains(latest.Sections.BackgroundMemory, "currency") {
117 t.Fatalf("forgotten fact remained in latest snapshot: %+v", latest)
118 }
119 }
120
121 func sessionContextCount(messages []provider.Message) int {
122 count := 0
123 for _, message := range messages {
124 if _, ok := sessioncontext.Parse(message.Content); ok && message.Origin == provider.MessageOriginHost {
125 count++
126 }
127 }
128 return count
129 }
130
131 func latestControlSessionContext(messages []provider.Message) (sessioncontext.Snapshot, bool) {
132 for i := range slices.Backward(messages) {
133 if snapshot, ok := sessioncontext.Parse(messages[i].Content); ok && messages[i].Origin == provider.MessageOriginHost {
134 return snapshot, true
135 }
136 }
137 return sessioncontext.Snapshot{}, false
138 }
139
139 lines GO