| 1 | package doctor |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | |
| 11 | "reasonix/internal/agent" |
| 12 | "reasonix/internal/provider" |
| 13 | "reasonix/internal/store" |
| 14 | ) |
| 15 | |
| 16 | func TestWriteSessionBundleListsSessionLogHeads(t *testing.T) { |
| 17 | home := t.TempDir() |
| 18 | t.Setenv("REASONIX_HOME", home) |
| 19 | dir := filepath.Join(home, "projects", "workspace", "sessions") |
| 20 | if err := os.MkdirAll(dir, 0o755); err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | path := filepath.Join(dir, "log-session.jsonl") |
| 24 | sess := agent.NewSession("system") |
| 25 | sess.Add(provider.Message{Role: provider.RoleUser, Content: "shared question"}) |
| 26 | sess.Add(provider.Message{Role: provider.RoleAssistant, Content: "shared answer"}) |
| 27 | if err := sess.Save(path); err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | fork, err := sess.ForkHead(path, sess.Snapshot()[2].ID, agent.HeadKindFork, "alt") |
| 31 | if err != nil { |
| 32 | t.Fatal(err) |
| 33 | } |
| 34 | sess.Add(provider.Message{Role: provider.RoleUser, Content: "alt question"}) |
| 35 | if err := sess.Save(path); err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | |
| 39 | out := filepath.Join(t.TempDir(), "diag.zip") |
| 40 | if _, err := WriteSessionBundle(SessionBundleOptions{Version: "test-version", SessionRef: path, OutputPath: out, Now: time.Unix(1700000000, 0)}); err != nil { |
| 41 | t.Fatalf("WriteSessionBundle: %v", err) |
| 42 | } |
| 43 | files := zipFiles(t, out) |
| 44 | var manifest SessionBundleManifest |
| 45 | if err := json.Unmarshal(files["manifest.json"], &manifest); err != nil { |
| 46 | t.Fatalf("manifest JSON: %v", err) |
| 47 | } |
| 48 | if strings.Contains(string(files["manifest.json"]), home) { |
| 49 | t.Fatalf("manifest leaked REASONIX_HOME path:\n%s", files["manifest.json"]) |
| 50 | } |
| 51 | if len(manifest.Sessions) != 1 { |
| 52 | t.Fatalf("manifest sessions = %+v, want the log alone: heads are not chain members", manifest.Sessions) |
| 53 | } |
| 54 | entry := manifest.Sessions[0] |
| 55 | if entry.LogFormat != 2 || entry.SelectedHead != fork || len(entry.Heads) != 2 { |
| 56 | t.Fatalf("manifest entry = %+v", entry) |
| 57 | } |
| 58 | main, alt := entry.Heads[0], entry.Heads[1] |
| 59 | if main.ID != agent.SessionMainHead || main.Kind != agent.HeadKindMain || !main.Covered || main.Selected || main.MessageCount != 3 { |
| 60 | t.Fatalf("main head = %+v", main) |
| 61 | } |
| 62 | if alt.ID != fork || alt.Kind != agent.HeadKindFork || alt.Name != "alt" || !alt.Selected || alt.Covered || alt.ParentHead != agent.SessionMainHead || alt.MessageCount != 4 { |
| 63 | t.Fatalf("fork head = %+v", alt) |
| 64 | } |
| 65 | if len(manifest.Missing) != 0 { |
| 66 | t.Fatalf("missing = %v, want none", manifest.Missing) |
| 67 | } |
| 68 | wantLog := filepath.ToSlash(filepath.Join("sessions", safeBundleName(agent.BranchID(path)), filepath.Base(store.SessionEventLog(path)))) |
| 69 | if _, ok := files[wantLog]; !ok { |
| 70 | t.Fatalf("bundle lacks the event log %q; have %v", wantLog, keysOf(files)) |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | func keysOf(files map[string][]byte) []string { |
| 75 | out := make([]string, 0, len(files)) |
| 76 | for name := range files { |
| 77 | out = append(out, name) |
| 78 | } |
| 79 | return out |
| 80 | } |
| 81 |