169 lines
4.3 KiB
Go
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)
|
|
}
|
|
}
|