package store import ( "context" "path/filepath" "testing" ) func TestStoreUsersWorkspacesSessions(t *testing.T) { ctx := context.Background() st, err := Open(ctx, filepath.Join(t.TempDir(), "bot.db")) if err != nil { t.Fatal(err) } defer st.Close() allowed, err := st.IsAllowed(ctx, 42) if err != nil { t.Fatal(err) } if allowed { t.Fatal("new user should not be allowed") } if err := st.AddAllowedUser(ctx, 42, "alice", "owner"); err != nil { t.Fatal(err) } allowed, err = st.IsAllowed(ctx, 42) if err != nil { t.Fatal(err) } if !allowed { t.Fatal("user should be allowed") } workspacePath := t.TempDir() ws, err := st.AddWorkspace(ctx, workspacePath, "repo", true) if err != nil { t.Fatal(err) } if !ws.IsDefault { t.Fatal("workspace should be default") } session, err := st.GetOrCreateSession(ctx, 42, "test-model", "read-only") if err != nil { t.Fatal(err) } if session.Model != "test-model" || session.Sandbox != "read-only" || session.ReasoningEffort != "" { t.Fatalf("unexpected session defaults: %+v", session) } if err := st.SetSessionWorkspace(ctx, 42, ws.ID); err != nil { t.Fatal(err) } effort := "value-returned-by-server" if err := st.SetSessionReasoningEffort(ctx, 42, effort); err != nil { t.Fatal(err) } session, err = st.GetSession(ctx, 42) if err != nil { t.Fatal(err) } if session.ActiveWorkspaceID != ws.ID || session.ReasoningEffort != effort { t.Fatalf("workspace not saved: %+v", session) } if err := st.SetSessionSettingsMessage(ctx, 42, 1001, 2002); err != nil { t.Fatal(err) } session, err = st.GetSession(ctx, 42) if err != nil { t.Fatal(err) } if session.SettingsChatID != 1001 || session.SettingsMessageID != 2002 { t.Fatalf("settings message not saved: %+v", session) } if err := st.SetActiveTurn(ctx, 42, "turn-123"); err != nil { t.Fatal(err) } if err := st.ClearActiveTurns(ctx); err != nil { t.Fatal(err) } session, err = st.GetSession(ctx, 42) if err != nil { t.Fatal(err) } if session.ActiveTurnID != "" { t.Fatalf("active turn not cleared: %+v", session) } } func TestListThreadsPage(t *testing.T) { ctx := context.Background() st, err := Open(ctx, filepath.Join(t.TempDir(), "bot.db")) if err != nil { t.Fatal(err) } defer st.Close() ws, err := st.AddWorkspace(ctx, t.TempDir(), "repo", true) if err != nil { t.Fatal(err) } for i := 0; i < 3; i++ { if _, err := st.CreateThread(ctx, 42, string(rune('a'+i)), ws.ID, "thread"); err != nil { t.Fatal(err) } } threads, err := st.ListThreadsPage(ctx, 42, false, 2, 0) if err != nil { t.Fatal(err) } if len(threads) != 2 { t.Fatalf("got %d threads, want 2", len(threads)) } threads, err = st.ListThreadsPage(ctx, 42, false, 2, 2) if err != nil { t.Fatal(err) } if len(threads) != 1 { t.Fatalf("got %d threads on second page, want 1", len(threads)) } } func TestRenameThread(t *testing.T) { ctx := context.Background() st, err := Open(ctx, filepath.Join(t.TempDir(), "bot.db")) if err != nil { t.Fatal(err) } defer st.Close() ws, err := st.AddWorkspace(ctx, t.TempDir(), "repo", true) if err != nil { t.Fatal(err) } thread, err := st.CreateThread(ctx, 42, "codex-thread", ws.ID, "old title") if err != nil { t.Fatal(err) } if err := st.RenameThread(ctx, 42, thread.ID, "new title"); err != nil { t.Fatal(err) } thread, err = st.GetThreadByID(ctx, 42, thread.ID) if err != nil { t.Fatal(err) } if thread.Title != "new title" { t.Fatalf("title = %q", thread.Title) } if err := st.RenameThreadByCodexID(ctx, "codex-thread", "codex title"); err != nil { t.Fatal(err) } thread, err = st.GetThreadByID(ctx, 42, thread.ID) if err != nil { t.Fatal(err) } if thread.Title != "codex title" { t.Fatalf("title = %q", thread.Title) } if err := st.SyncThreadTitle(ctx, 42, thread.ID, "synced title"); err != nil { t.Fatal(err) } thread, err = st.GetThreadByID(ctx, 42, thread.ID) if err != nil { t.Fatal(err) } if thread.Title != "synced title" { t.Fatalf("title = %q", thread.Title) } if err := st.SyncThreadTitleByCodexID(ctx, "codex-thread", "synced codex title"); err != nil { t.Fatal(err) } thread, err = st.GetThreadByID(ctx, 42, thread.ID) if err != nil { t.Fatal(err) } if thread.Title != "synced codex title" { t.Fatalf("title = %q", thread.Title) } ws2, err := st.AddWorkspace(ctx, t.TempDir(), "repo2", false) if err != nil { t.Fatal(err) } if err := st.SyncThreadWorkspace(ctx, 42, thread.ID, ws2.ID); err != nil { t.Fatal(err) } thread, err = st.GetThreadByID(ctx, 42, thread.ID) if err != nil { t.Fatal(err) } if thread.WorkspaceID != ws2.ID { t.Fatalf("workspace id = %d, want %d", thread.WorkspaceID, ws2.ID) } } func TestValidateWorkspacePath(t *testing.T) { if _, err := ValidateWorkspacePath("relative/path"); err == nil { t.Fatal("relative path should be rejected") } if _, err := ValidateWorkspacePath("/"); err == nil { t.Fatal("filesystem root should be rejected") } input := string(filepath.Separator) + filepath.Join("tmp", "..", "tmp", "project") clean, err := ValidateWorkspacePath(input) if err != nil { t.Fatal(err) } if clean != filepath.Join(string(filepath.Separator), "tmp", "project") { t.Fatalf("unexpected clean path: %s", clean) } }