返回 DeepSeek-Reasonix
effect_session_log_test.go
根目录 / internal / boot / effect_session_log_test.go
1 package boot
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "testing"
10
11 "reasonix/internal/ablation"
12 "reasonix/internal/agent"
13 "reasonix/internal/event"
14 "reasonix/internal/provider"
15 )
16
17 // TestEffectSessionLogUpgradeKeepsModelMessagesThroughRealBuild freezes the
18 // production compatibility boundary: continuing a schema-1 transcript imports
19 // it into a final identity-bound v3 session, leaves the source bytes untouched,
20 // and preserves the provider-visible prefix (including stable message ids).
21 func TestEffectSessionLogUpgradeKeepsModelMessagesThroughRealBuild(t *testing.T) {
22 isolateConfigHome(t)
23 dir := robustTempDir(t)
24 t.Chdir(dir)
25 t.Setenv(agent.SessionLogSchemaEnv, "v1")
26
27 rec := &effectRecordingProvider{}
28 provider.Register("boot-effect-session-log", func(provider.Config) (provider.Provider, error) { return rec, nil })
29 writeFile(t, dir, "reasonix.toml", `
30 default_model = "test-model"
31
32 [agent]
33 system_prompt = "BASE"
34
35 [environment]
36 enabled = false
37
38 [[providers]]
39 name = "test-model"
40 kind = "boot-effect-session-log"
41 model = "x"
42 `)
43
44 legacyDir := filepath.Join(t.TempDir(), "sessions")
45 legacyPath := agent.NewSessionPath(legacyDir, "legacy")
46 legacy := agent.NewSession("BASE")
47 legacy.Add(provider.Message{Role: provider.RoleUser, Content: "reply ok"})
48 legacy.Add(provider.Message{Role: provider.RoleAssistant, Content: "ok"})
49 if err := legacy.Save(legacyPath); err != nil {
50 t.Fatalf("write schema-1 source: %v", err)
51 }
52 beforeSource, err := os.ReadFile(legacyPath)
53 if err != nil {
54 t.Fatal(err)
55 }
56 before, err := json.Marshal(provider.ModelMessages(legacy.Snapshot()))
57 if err != nil {
58 t.Fatal(err)
59 }
60 if err := os.Unsetenv(agent.SessionLogSchemaEnv); err != nil {
61 t.Fatal(err)
62 }
63
64 ctrl, err := Build(context.Background(), withTestSession(t, Options{SessionDir: legacyDir, Sink: event.Discard, Ablation: ablation.Set{}}))
65 if err != nil {
66 t.Fatalf("Build: %v", err)
67 }
68 ref, err := ctrl.ContinueLegacySession(context.Background(), legacyPath, "")
69 if err != nil {
70 ctrl.Close()
71 t.Fatalf("ContinueLegacySession: %v", err)
72 }
73 if ctrl.SessionPath() != "" {
74 t.Fatalf("migrated v3 runtime retained legacy execution path %q", ctrl.SessionPath())
75 }
76 if err := ctrl.Run(context.Background(), "reply again"); err != nil {
77 ctrl.Close()
78 t.Fatalf("Run after migration: %v", err)
79 }
80 after, err := json.Marshal(provider.ModelMessages(ctrl.History()))
81 if err != nil {
82 t.Fatal(err)
83 }
84 service, runtime, ok := ctrl.SessionBinding()
85 if !ok || runtime.Ref() != ref {
86 t.Fatalf("bound runtime = %+v, want %+v", runtime, ref)
87 }
88 ctrl.Close()
89
90 var beforeMsgs, afterMsgs []json.RawMessage
91 if err := json.Unmarshal(before, &beforeMsgs); err != nil {
92 t.Fatal(err)
93 }
94 if err := json.Unmarshal(after, &afterMsgs); err != nil {
95 t.Fatal(err)
96 }
97 if len(afterMsgs) <= len(beforeMsgs) {
98 t.Fatalf("migrated transcript did not grow: before %d after %d", len(beforeMsgs), len(afterMsgs))
99 }
100 for i := range beforeMsgs {
101 if string(beforeMsgs[i]) != string(afterMsgs[i]) {
102 t.Fatalf("message %d changed across legacy import\nbefore: %s\nafter: %s", i, beforeMsgs[i], afterMsgs[i])
103 }
104 }
105 if got, err := os.ReadFile(legacyPath); err != nil || !bytes.Equal(got, beforeSource) {
106 t.Fatalf("legacy source changed during import: err=%v", err)
107 }
108 cold, err := service.Query().History(context.Background(), ref)
109 if err != nil {
110 t.Fatalf("cold v3 history: %v", err)
111 }
112 if len(provider.ModelMessages(cold)) != len(afterMsgs) {
113 t.Fatalf("cold v3 history len=%d, want %d", len(provider.ModelMessages(cold)), len(afterMsgs))
114 }
115 }
116
116 lines GO