| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "path/filepath" |
| 5 | "slices" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "reasonix/internal/agent" |
| 10 | "reasonix/internal/event" |
| 11 | "reasonix/internal/provider" |
| 12 | "reasonix/internal/tool" |
| 13 | ) |
| 14 | |
| 15 | // TestTurnDoneStampsHeadReferenceForSchemaTwo pins that the terminal turn |
| 16 | // envelope names the head and leaf message the turn ended on, so projection |
| 17 | // acks and orphan repair can identify the transcript position without a |
| 18 | // revision/digest pair. |
| 19 | func TestTurnDoneStampsHeadReferenceForSchemaTwo(t *testing.T) { |
| 20 | dir := t.TempDir() |
| 21 | path := filepath.Join(dir, "session.jsonl") |
| 22 | reply := [][]provider.Chunk{{{Type: provider.ChunkText, Text: "ok"}, {Type: provider.ChunkDone}}} |
| 23 | exec := agent.New(&recordingProvider{streams: reply}, tool.NewRegistry(), agent.NewSession("SYS"), agent.Options{}, event.Discard) |
| 24 | done := make(chan event.Event, 4) |
| 25 | c := newOwnedTestController(t, Options{Runner: exec, Executor: exec, SystemPrompt: "SYS", SessionDir: dir, SessionPath: path, Label: "test", |
| 26 | Sink: event.FuncSink(func(e event.Event) { |
| 27 | if e.Kind == event.TurnDone { |
| 28 | done <- e |
| 29 | } |
| 30 | })}) |
| 31 | t.Cleanup(c.Close) |
| 32 | // The first turn's save creates the schema-2 log; the second turn ends on it. |
| 33 | for _, input := range []string{"first", "second"} { |
| 34 | c.Submit(input) |
| 35 | select { |
| 36 | case <-done: |
| 37 | case <-time.After(5 * time.Second): |
| 38 | t.Fatalf("turn %q did not finish", input) |
| 39 | } |
| 40 | } |
| 41 | records, err := c.TurnEventsAfter(0) |
| 42 | if err != nil { |
| 43 | t.Fatalf("TurnEventsAfter: %v", err) |
| 44 | } |
| 45 | var terminal int |
| 46 | for i, record := range slices.Backward(records) { |
| 47 | if record.Kind == "turn_done" { |
| 48 | terminal = i |
| 49 | break |
| 50 | } |
| 51 | } |
| 52 | ref, ok := exec.Session().Head() |
| 53 | if !ok { |
| 54 | t.Fatal("session must be schema 2 after two saved turns") |
| 55 | } |
| 56 | got := records[terminal] |
| 57 | if got.HeadID != ref.HeadID || got.LeafMessageID == "" || got.LeafMessageID != exec.Session().LeafID() { |
| 58 | t.Fatalf("terminal envelope head=%q leaf=%q, want head %q leaf %q", got.HeadID, got.LeafMessageID, ref.HeadID, exec.Session().LeafID()) |
| 59 | } |
| 60 | view, err := c.TurnEventReplay(0) |
| 61 | if err != nil || view.HeadID != ref.HeadID || view.LeafMessageID != got.LeafMessageID { |
| 62 | t.Fatalf("replay view = %+v err=%v", view, err) |
| 63 | } |
| 64 | } |
| 65 |