| 1 | package session |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | |
| 11 | bolt "go.etcd.io/bbolt" |
| 12 | |
| 13 | "reasonix/internal/provider" |
| 14 | ) |
| 15 | |
| 16 | func TestCatalogPreviewUsesAuthoredTextBeforeTruncation(t *testing.T) { |
| 17 | const task = "修复左侧会话标题" |
| 18 | wrapped := "<response-language>" + strings.Repeat("internal preference ", 40) + "</response-language>\n<capability-route version=\"1\">tools</capability-route>\n" + task |
| 19 | for _, tc := range []struct{ name, content, raw, want string }{ |
| 20 | {"legacy", wrapped, "", task}, |
| 21 | {"raw", wrapped, "用户实际输入", "用户实际输入"}, |
| 22 | {"raw markup is authored", wrapped, "<response-language>literal example</response-language>", "<response-language>literal example</response-language>"}, |
| 23 | {"raw only", "", task, task}, |
| 24 | } { |
| 25 | t.Run(tc.name, func(t *testing.T) { |
| 26 | message := provider.Message{Role: provider.RoleUser, Content: tc.content, RawContent: tc.raw, Origin: provider.MessageOriginUser} |
| 27 | if got := messagePreview(message); got != tc.want { |
| 28 | t.Fatalf("preview = %q, want %q", got, tc.want) |
| 29 | } |
| 30 | metadata := metadataFromProjection(Manifest{}, 0, Projection{Title: "My custom title", Messages: []provider.Message{ |
| 31 | {Role: provider.RoleUser, Origin: provider.MessageOriginHost, Content: "host context"}, message, |
| 32 | }}) |
| 33 | if metadata.Title != "My custom title" || metadata.Preview != tc.want { |
| 34 | t.Fatalf("metadata = %+v", metadata) |
| 35 | } |
| 36 | }) |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | func TestCatalogPreviewSurvivesColdReopen(t *testing.T) { |
| 41 | dir := filepath.Join(t.TempDir(), "sessions", "preview") |
| 42 | s, err := CreateWithOptions(dir, "preview", OpenOptions{ExternalHistory: true}) |
| 43 | if err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | for i, message := range []provider.Message{ |
| 47 | {ID: "host", Role: provider.RoleUser, Origin: provider.MessageOriginHost, Content: "host context"}, |
| 48 | {ID: "user", Role: provider.RoleUser, Content: "<reasoning-language>" + strings.Repeat("internal", 80) + "</reasoning-language>\n实际请求"}, |
| 49 | } { |
| 50 | payload, _ := json.Marshal(map[string]any{"message": message}) |
| 51 | if _, err := s.Append(t.Context(), Batch{OperationID: string(rune('a' + i)), Events: []Event{{Kind: "message/complete", Payload: payload}}}); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | } |
| 55 | if _, err := s.Flush(t.Context()); err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | if got := s.CatalogMetadata().Preview; got != "实际请求" { |
| 59 | t.Fatalf("live preview = %q", got) |
| 60 | } |
| 61 | if err := s.Close(t.Context()); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | // Simulate a pre-fix checkpoint: its truncated prefix has lost the task. |
| 65 | db, err := bolt.Open(filepath.Join(recoveryCacheDir(dir), recoveryDBName), 0o600, &bolt.Options{Timeout: time.Second}) |
| 66 | if err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | err = db.Update(func(tx *bolt.Tx) error { |
| 70 | bucket := tx.Bucket(recoveryCheckpointBucket) |
| 71 | for _, key := range [][]byte{recoveryCurrentKey, recoveryPreviousKey} { |
| 72 | if data := bucket.Get(key); data != nil { |
| 73 | var checkpoint recoveryCheckpoint |
| 74 | if err := decodeRecoveryValue(data, &checkpoint); err != nil { |
| 75 | return err |
| 76 | } |
| 77 | checkpoint.ProjectionVersion = 2 |
| 78 | checkpoint.CatalogPreview = "<reasoning-language>internal..." |
| 79 | encoded, err := encodeRecoveryValue(checkpoint) |
| 80 | if err != nil { |
| 81 | return err |
| 82 | } |
| 83 | if err := bucket.Put(key, encoded); err != nil { |
| 84 | return err |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | return nil |
| 89 | }) |
| 90 | closeErr := db.Close() |
| 91 | if err != nil { |
| 92 | t.Fatal(err) |
| 93 | } |
| 94 | if closeErr != nil { |
| 95 | t.Fatal(closeErr) |
| 96 | } |
| 97 | var stats RecoveryOpenStats |
| 98 | reopened, err := OpenWithOptions(dir, "preview", OpenOptions{ExternalHistory: true, ObserveRecovery: func(got RecoveryOpenStats) { stats = got }}) |
| 99 | if err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | t.Cleanup(func() { _ = reopened.Close(context.Background()) }) |
| 103 | if stats.UsedCheckpoint { |
| 104 | t.Fatal("old display checkpoint was reused") |
| 105 | } |
| 106 | if got := reopened.CatalogMetadata().Preview; got != "实际请求" { |
| 107 | t.Fatalf("cold preview = %q", got) |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | func TestCatalogRejectsOldDisplayProjection(t *testing.T) { |
| 112 | dir := t.TempDir() |
| 113 | manifest := Manifest{SessionID: "old-preview", CreatedAt: time.Unix(1, 0)} |
| 114 | metadata := metadataFromProjection(manifest, 0, Projection{}) |
| 115 | metadata.Version = 1 |
| 116 | metadata.Preview = "<response-language>" + strings.Repeat("internal", 40) |
| 117 | if err := writeCatalogMetadata(dir, metadata); err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | if _, err := readCatalogMetadata(dir, manifest, logRevision{}); err == nil { |
| 121 | t.Fatal("old truncated preview was reused") |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | func TestMessagePreviewDoesNotExposeHostProtocolBeforeHydration(t *testing.T) { |
| 126 | message := provider.Message{Role: provider.RoleUser, Origin: provider.MessageOriginHost, |
| 127 | Content: "<session-context version=\"1\">" + strings.Repeat("environment", 4000) + "</session-context>", RawContent: "host-only metadata"} |
| 128 | if got := messagePreview(message); got != "" { |
| 129 | t.Fatalf("host preview = %q", got) |
| 130 | } |
| 131 | message.Origin = provider.MessageOriginUser |
| 132 | message.RawContent = "用户引用的上下文" |
| 133 | if got := messagePreview(message); got != message.RawContent { |
| 134 | t.Fatalf("authored preview = %q", got) |
| 135 | } |
| 136 | message.Origin = "" |
| 137 | message.RawContent = "" |
| 138 | message.Content = "<session-context version=\"1\">\nThis host-generated snapshot supersedes every earlier session-context snapshot.\n" + strings.Repeat("environment", 4000) |
| 139 | if got := messagePreview(message); got != "" { |
| 140 | t.Fatalf("legacy host preview = %q", got) |
| 141 | } |
| 142 | } |
| 143 |