nub/internal/session/store_test.go
Tom a97013d876 initial commit
- v 0.1.0 siehe CHANGELOG.md
2026-07-25 11:08:01 +02:00

169 lines
4.3 KiB
Go

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)
}
}