| 1 | package session |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/json" |
| 6 | "path/filepath" |
| 7 | "testing" |
| 8 | |
| 9 | "reasonix/internal/provider" |
| 10 | ) |
| 11 | |
| 12 | func TestSubmissionMetadataSurvivesReplayForkAndRetraction(t *testing.T) { |
| 13 | root := t.TempDir() |
| 14 | dir := filepath.Join(root, "parent") |
| 15 | s, err := Open(dir, "parent") |
| 16 | if err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | receipt := SubmissionReceipt{SessionID: "parent", SubmissionID: "send", Fingerprint: "fingerprint", TurnID: "turn", MessageID: "user"} |
| 20 | data, _ := json.Marshal(receipt) |
| 21 | message := provider.Message{ID: "user", Role: provider.RoleUser, Content: "synthetic compatibility fixture"} |
| 22 | payload, _ := json.Marshal(map[string]any{"message": message}) |
| 23 | commit, err := s.Append(t.Context(), Batch{OperationID: "accept", TurnID: "turn", Events: []Event{ |
| 24 | {Kind: "turn/start"}, {Kind: "submission/accepted", Optional: true, Payload: data}, |
| 25 | {Kind: "message/complete", Payload: payload}, {Kind: "turn/end", Payload: json.RawMessage(`{"status":"completed"}`)}, |
| 26 | }}) |
| 27 | if err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | if _, err := s.Fork(t.Context(), filepath.Join(root, "child"), "child", commit.LastSequence()); err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | if err := s.Close(t.Context()); err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | s, err = Open(dir, "parent") |
| 37 | if err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | defer s.Close(t.Context()) |
| 41 | if got, ok := s.Submission("send"); !ok || got != receipt { |
| 42 | t.Fatalf("receipt after restart: %+v %v", got, ok) |
| 43 | } |
| 44 | model, _ := json.Marshal(s.DeriveMessages()) |
| 45 | if bytes.Contains(model, []byte("submissionId")) || bytes.Contains(model, []byte("fingerprint")) { |
| 46 | t.Fatal("host metadata entered model messages") |
| 47 | } |
| 48 | entries := []PersistentMessage{{MessageID: "user"}} |
| 49 | attachSubmissionEntries(s.Snapshot().Projection.Submissions, "parent", entries) |
| 50 | if entries[0].SubmissionID != "send" { |
| 51 | t.Fatal("recent baseline lost submission identity") |
| 52 | } |
| 53 | if rows := s.transcriptRows(s.DeriveMessages()); len(rows) != 1 || rows[0].SubmissionID != "send" { |
| 54 | t.Fatalf("history: %+v", rows) |
| 55 | } |
| 56 | child, err := Open(filepath.Join(root, "child"), "child") |
| 57 | if err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | defer child.Close(t.Context()) |
| 61 | if _, found := child.Submission("send"); found { |
| 62 | t.Fatal("parent identity leaked into child acceptance scope") |
| 63 | } |
| 64 | if len(child.DeriveMessages()) != 1 { |
| 65 | t.Fatal("fork lost the historical message") |
| 66 | } |
| 67 | if _, err := s.Append(t.Context(), Batch{OperationID: "retract", Events: []Event{{Kind: "message/retract", Payload: json.RawMessage(`{"messageIds":["user"]}`)}}}); err != nil { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | if got, ok := s.Submission("send"); !ok || got != receipt { |
| 71 | t.Fatal("retraction removed acceptance") |
| 72 | } |
| 73 | } |
| 74 |