| 1 | package session |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | |
| 6 | "reasonix/internal/event" |
| 7 | "reasonix/internal/eventwire" |
| 8 | "reasonix/internal/transcript" |
| 9 | "reasonix/internal/turnevent" |
| 10 | ) |
| 11 | |
| 12 | func TestRuntimeTranscriptCoverageIncludesNonVisibleBusinessCommits(t *testing.T) { |
| 13 | _, runtime := reviewRuntime(t) |
| 14 | p := runtime.Transcript() |
| 15 | before, err := p.Snapshot(transcript.PageRequest{}) |
| 16 | if err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | if _, err := runtime.Session().AppendBatch(t.Context(), "configuration", []Event{{Kind: "session/config", Payload: []byte(`{"modelRef":"test"}`)}}); err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | after, err := p.Snapshot(transcript.PageRequest{}) |
| 23 | if err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | if after.CoveredThroughSeq != runtime.StateSnapshot().Session.EventSequence || after.CoveredThroughSeq <= before.CoveredThroughSeq { |
| 27 | t.Fatalf("non-visible commit lost business coverage: before=%d after=%d business=%d", before.CoveredThroughSeq, after.CoveredThroughSeq, runtime.StateSnapshot().Session.EventSequence) |
| 28 | } |
| 29 | if len(after.Records) != len(before.Records) { |
| 30 | t.Fatal("configuration commit manufactured a chat record") |
| 31 | } |
| 32 | for index, text := range []string{"first ", "second"} { |
| 33 | e := eventwire.ToWire(event.Event{Kind: event.Text, MessageID: "answer", AttemptID: "answer", Text: text}) |
| 34 | // Deliberately unrelated to the business sequence. A frame number is |
| 35 | // never the accepted-log cursor, even when the values happen to match. |
| 36 | if err := runtime.PublishTranscriptFrame(turnevent.Envelope{SessionID: runtime.Ref().SessionID, RuntimeEpoch: runtime.StateSnapshot().Epoch, TurnID: "turn", Sequence: uint64(1000 + index), Kind: e.Kind, Status: event.TurnInProgress, Event: e}); err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | cut, err := p.Snapshot(transcript.PageRequest{}) |
| 40 | if err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | if cut.CoveredThroughSeq != after.CoveredThroughSeq || cut.ProjectionRevision <= after.ProjectionRevision { |
| 44 | t.Fatalf("frame conflates revision and coverage: prior=%+v current=%+v", after.Boundary, cut.Boundary) |
| 45 | } |
| 46 | after = cut |
| 47 | } |
| 48 | if len(after.Records) != 1 || after.Records[0].Message.Content != "first second" { |
| 49 | t.Fatalf("streaming prefix was not retained: %+v", after.Records) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func TestRuntimeTranscriptSurvivesExecutionReplacement(t *testing.T) { |
| 54 | _, runtime := reviewRuntime(t) |
| 55 | p := runtime.Transcript() |
| 56 | if p == nil { |
| 57 | t.Fatal("runtime lacks its transcript publisher") |
| 58 | } |
| 59 | first := &testExecution{phase: RuntimeIdle, runtime: runtime} |
| 60 | first.gen = runtime.BindExecution(first) |
| 61 | if first.gen == 0 { |
| 62 | t.Fatal("initial execution was not bound") |
| 63 | } |
| 64 | second := &testExecution{phase: RuntimeIdle, runtime: runtime} |
| 65 | second.gen = runtime.ReplaceExecution(first.gen, second) |
| 66 | if second.gen == 0 { |
| 67 | t.Fatal("idle execution replacement was rejected") |
| 68 | } |
| 69 | t.Cleanup(func() { runtime.UnbindExecution(second.gen) }) |
| 70 | if runtime.Transcript() != p { |
| 71 | t.Fatal("execution replacement created another transcript authority") |
| 72 | } |
| 73 | if _, err := runtime.Session().AppendBatch(t.Context(), "after-replacement", []Event{{Kind: "session/config", Payload: []byte(`{"modelRef":"replacement"}`)}}); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | cut, err := p.Snapshot(transcript.PageRequest{}) |
| 77 | if err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | if cut.CoveredThroughSeq != runtime.StateSnapshot().Session.EventSequence || cut.CoveredThroughSeq == 0 { |
| 81 | t.Fatal("original publisher stopped observing commits after replacement") |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | func TestRuntimeTranscriptRetractionResetsReadersAndPreservesActiveOutput(t *testing.T) { |
| 86 | _, runtime := reviewRuntime(t) |
| 87 | ctx := t.Context() |
| 88 | if _, err := runtime.Session().AppendBatch(ctx, "input", []Event{{Kind: "message/complete", Payload: []byte(`{"message":{"id":"withdrawn","role":"user","content":"synthetic input"}}`)}}); err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | e := eventwire.ToWire(event.Event{Kind: event.Text, MessageID: "active-answer", AttemptID: "active-answer", Text: "retained output"}) |
| 92 | if err := runtime.PublishTranscriptFrame(turnevent.Envelope{SessionID: runtime.Ref().SessionID, RuntimeEpoch: runtime.StateSnapshot().Epoch, Kind: e.Kind, Event: e}); err != nil { |
| 93 | t.Fatal(err) |
| 94 | } |
| 95 | before, err := runtime.FollowTranscript(ctx, transcript.FollowRequest{}) |
| 96 | if err != nil { |
| 97 | t.Fatal(err) |
| 98 | } |
| 99 | if _, err := runtime.Session().AppendBatch(ctx, "withdraw", []Event{{Kind: "message/retract", Payload: []byte(`{"messageIds":["withdrawn"]}`)}}); err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | after, err := runtime.Transcript().Snapshot(transcript.PageRequest{}) |
| 103 | if err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | if after.Identity.RewriteEpoch <= before.Snapshot.Identity.RewriteEpoch || after.CoveredThroughSeq != runtime.StateSnapshot().Session.EventSequence { |
| 107 | t.Fatal("retraction did not invalidate the old reading cut at its committed sequence") |
| 108 | } |
| 109 | if len(after.Records) != 1 || after.Records[0].Message.Content != "retained output" { |
| 110 | t.Fatalf("retraction lost unrelated active output: %+v", after.Records) |
| 111 | } |
| 112 | } |
| 113 |