package session import ( "os" "path/filepath" "runtime" "testing" "nub/internal/llm" ) func TestStore_CreateAppendLoad_Roundtrip(t *testing.T) { dir := t.TempDir() st, err := Create(dir) if err != nil { t.Fatal(err) } n1 := NewNode("", textMsg(llm.RoleUser, "hello"), NodeMeta{Model: "test-model"}) must(t, st.Append(n1)) n2 := NewNode(n1.ID, textMsg(llm.RoleAssistant, "hi there"), NodeMeta{Model: "test-model"}) must(t, st.Append(n2)) id := st.ID must(t, st.Close()) loaded, err := Load(dir, id) if err != nil { t.Fatal(err) } defer loaded.Close() if loaded.Head != n2.ID { t.Errorf("head = %s, want %s", loaded.Head, n2.ID) } msgs := loaded.PathToHead() if len(msgs) != 2 || msgs[1].Content[0].Text != "hi there" { t.Errorf("unexpected path: %+v", msgs) } } // TestStore_RewindAndBranch_BothPathsRemainLoadable ist das M2-Fertig- // Kriterium aus Abschnitt 6: Rewind auf einen früheren Knoten + Weiterarbeit // erzeugt einen zweiten Ast, beide bleiben ladbar. func TestStore_RewindAndBranch_BothPathsRemainLoadable(t *testing.T) { dir := t.TempDir() st, err := Create(dir) if err != nil { t.Fatal(err) } root := NewNode("", textMsg(llm.RoleUser, "root"), NodeMeta{}) must(t, st.Append(root)) tipA := NewNode(root.ID, textMsg(llm.RoleAssistant, "original continuation"), NodeMeta{}) must(t, st.Append(tipA)) id := st.ID must(t, st.Close()) // Rewind: erneut laden, auf root zurückspringen, neu weiterarbeiten. resumed, err := Load(dir, id) if err != nil { t.Fatal(err) } must(t, resumed.Branch(root.ID)) tipB := NewNode(root.ID, textMsg(llm.RoleAssistant, "rewound continuation"), NodeMeta{}) must(t, resumed.Append(tipB)) must(t, resumed.Close()) // Beide Äste müssen nach erneutem Laden noch da sein. final, err := Load(dir, id) if err != nil { t.Fatal(err) } defer final.Close() if _, ok := final.Nodes[tipA.ID]; !ok { t.Error("original branch tip missing after reload") } if _, ok := final.Nodes[tipB.ID]; !ok { t.Error("rewound branch tip missing after reload") } if final.Head != tipB.ID { t.Errorf("head after reload = %s, want %s (latest branch)", final.Head, tipB.ID) } must(t, final.Branch(tipA.ID)) msgsA := final.PathToHead() if got := msgsA[len(msgsA)-1].Content[0].Text; got != "original continuation" { t.Errorf("branch A tip = %q", got) } must(t, final.Branch(tipB.ID)) msgsB := final.PathToHead() if got := msgsB[len(msgsB)-1].Content[0].Text; got != "rewound continuation" { t.Errorf("branch B tip = %q", got) } } func TestStore_FilePermissions(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Unix-Dateirechte, siehe Abschnitt 9 (kein Windows-Pfad)") } dir := t.TempDir() st, err := Create(dir) if err != nil { t.Fatal(err) } defer st.Close() dirInfo, err := os.Stat(filepath.Join(dir, ".nub", "sessions")) if err != nil { t.Fatal(err) } if perm := dirInfo.Mode().Perm(); perm != 0o700 { t.Errorf(".nub/sessions perm = %o, want 0700", perm) } fileInfo, err := os.Stat(filepath.Join(dir, ".nub", "sessions", st.ID+".jsonl")) if err != nil { t.Fatal(err) } if perm := fileInfo.Mode().Perm(); perm != 0o600 { t.Errorf("session file perm = %o, want 0600", perm) } } func TestStore_Load_RejectsUnsupportedVersion(t *testing.T) { dir := t.TempDir() sessDir := filepath.Join(dir, ".nub", "sessions") must(t, os.MkdirAll(sessDir, 0o700)) path := filepath.Join(sessDir, "bad.jsonl") must(t, os.WriteFile(path, []byte(`{"v":2,"node":{"id":"x"}}`+"\n"), 0o600)) if _, err := Load(dir, "bad"); err == nil { t.Fatal("expected error for unsupported record version") } } func TestStore_List_ReturnsSessionsNewestFirst(t *testing.T) { dir := t.TempDir() st1, err := Create(dir) if err != nil { t.Fatal(err) } must(t, st1.Append(NewNode("", textMsg(llm.RoleUser, "first session"), NodeMeta{}))) must(t, st1.Close()) st2, err := Create(dir) if err != nil { t.Fatal(err) } must(t, st2.Append(NewNode("", textMsg(llm.RoleUser, "second session"), NodeMeta{}))) must(t, st2.Close()) infos, err := List(dir) if err != nil { t.Fatal(err) } if len(infos) != 2 { t.Fatalf("len = %d, want 2", len(infos)) } found := map[string]bool{} for _, info := range infos { found[info.Summary] = true } if !found["first session"] || !found["second session"] { t.Errorf("infos = %+v", infos) } }