Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

2788 lines
94 KiB
Go

package workspace
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
agentsessionstore "github.com/tutti-os/tutti/packages/agent/daemon/activity"
agentactivitybiz "github.com/tutti-os/tutti/packages/agent/store-sqlite"
agenttargetbiz "github.com/tutti-os/tutti/services/tuttid/biz/agenttarget"
userprojectbiz "github.com/tutti-os/tutti/services/tuttid/biz/userproject"
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
)
type sqlitePragmaExecutorStub struct {
errors []error
calls int
}
func (s *sqlitePragmaExecutorStub) Exec(string, ...any) (sql.Result, error) {
index := s.calls
s.calls++
if index < len(s.errors) {
return nil, s.errors[index]
}
return nil, nil
}
type sqliteCodedTestError struct {
code int
}
func (e sqliteCodedTestError) Error() string { return fmt.Sprintf("sqlite error %d", e.code) }
func (e sqliteCodedTestError) Code() int { return e.code }
func TestEnableSQLiteWALRetriesTransientTruncateErrors(t *testing.T) {
executor := &sqlitePragmaExecutorStub{errors: []error{
sqliteCodedTestError{code: sqliteIOErrTruncate},
sqliteCodedTestError{code: sqliteIOErrTruncate},
}}
var delays []time.Duration
if err := enableSQLiteWAL(executor, func(delay time.Duration) { delays = append(delays, delay) }); err != nil {
t.Fatalf("enableSQLiteWAL() error = %v", err)
}
if executor.calls != 3 {
t.Fatalf("Exec calls = %d, want 3", executor.calls)
}
if !slices.Equal(delays, sqliteWALRetryDelays) {
t.Fatalf("retry delays = %v, want %v", delays, sqliteWALRetryDelays)
}
}
func TestEnableSQLiteWALDoesNotRetryOtherErrors(t *testing.T) {
executor := &sqlitePragmaExecutorStub{errors: []error{sqliteCodedTestError{code: 5}}}
err := enableSQLiteWAL(executor, func(time.Duration) { t.Fatal("unexpected retry") })
if err == nil {
t.Fatal("enableSQLiteWAL() error = nil, want failure")
}
if executor.calls != 1 {
t.Fatalf("Exec calls = %d, want 1", executor.calls)
}
}
func TestEnableSQLiteWALStopsAfterTransientRetryBudget(t *testing.T) {
executor := &sqlitePragmaExecutorStub{errors: []error{
sqliteCodedTestError{code: sqliteIOErrTruncate},
sqliteCodedTestError{code: sqliteIOErrTruncate},
sqliteCodedTestError{code: sqliteIOErrTruncate},
}}
var delays []time.Duration
err := enableSQLiteWAL(executor, func(delay time.Duration) { delays = append(delays, delay) })
if err == nil {
t.Fatal("enableSQLiteWAL() error = nil, want failure")
}
if executor.calls != 3 {
t.Fatalf("Exec calls = %d, want 3", executor.calls)
}
if !slices.Equal(delays, sqliteWALRetryDelays) {
t.Fatalf("retry delays = %v, want %v", delays, sqliteWALRetryDelays)
}
}
func TestSQLiteDSNPathUsesFileURLFormOnWindows(t *testing.T) {
got := sqliteDSNPath(`C:\Users\Tutti User\state\tuttid.db`, "windows")
if got != "/C:/Users/Tutti User/state/tuttid.db" {
t.Fatalf("sqliteDSNPath() = %q", got)
}
}
func TestSQLiteStoreListEmptyDatabase(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
items, err := store.List(context.Background())
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(items) != 0 {
t.Fatalf("List() len = %d, want 0", len(items))
}
}
func TestSQLiteStoreBackupToCreatesConsistentReadableSnapshot(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-1", Name: "Recorded"}); err != nil {
t.Fatal(err)
}
destination := filepath.Join(t.TempDir(), "baseline.db")
if err := store.BackupTo(ctx, destination); err != nil {
t.Fatal(err)
}
backup, err := OpenSQLiteStore(destination)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = backup.Close() })
if err := backup.openReadPool(ctx); err != nil {
t.Fatal(err)
}
items, err := backup.List(ctx)
if err != nil {
t.Fatal(err)
}
if len(items) != 1 || items[0].ID != "workspace-1" {
t.Fatalf("backup workspaces = %#v", items)
}
info, err := os.Stat(destination)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm()&0o077 != 0 {
t.Fatalf("backup permissions = %o, want private", info.Mode().Perm())
}
}
func TestSQLiteStoreConfiguresSeparateReadAndWritePools(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
if got := store.writeDB.Stats().MaxOpenConnections; got != 1 {
t.Fatalf("write pool max open connections = %d, want 1", got)
}
if got := store.readDB.Stats().MaxOpenConnections; got != defaultSQLiteReaderConnections {
t.Fatalf("read pool max open connections = %d, want %d", got, defaultSQLiteReaderConnections)
}
ctx := context.Background()
connections := make([]*sql.Conn, 0, defaultSQLiteReaderConnections)
defer func() {
for _, connection := range connections {
_ = connection.Close()
}
}()
for index := 0; index < defaultSQLiteReaderConnections; index++ {
connection, err := store.readDB.Conn(ctx)
if err != nil {
t.Fatalf("read pool connection %d: %v", index, err)
}
connections = append(connections, connection)
var queryOnly int
if err := connection.QueryRowContext(ctx, "PRAGMA query_only").Scan(&queryOnly); err != nil {
t.Fatalf("read pool connection %d query_only: %v", index, err)
}
if queryOnly != 1 {
t.Fatalf("read pool connection %d query_only = %d, want 1", index, queryOnly)
}
var busyTimeout int
if err := connection.QueryRowContext(ctx, "PRAGMA busy_timeout").Scan(&busyTimeout); err != nil {
t.Fatalf("read pool connection %d busy_timeout: %v", index, err)
}
if busyTimeout != defaultSQLiteBusyTimeoutMillisec {
t.Fatalf("read pool connection %d busy_timeout = %d, want %d", index, busyTimeout, defaultSQLiteBusyTimeoutMillisec)
}
}
if _, err := connections[0].ExecContext(ctx, "CREATE TABLE read_pool_must_not_write (id INTEGER)"); err == nil {
t.Fatal("read pool write succeeded, want read-only error")
}
}
func TestSQLiteStoreReadsCommittedDataWhileWriterConnectionIsBusy(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
const workspaceID = "ws-read-write-pools"
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Committed"}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: workspaceID,
AgentSessionID: "session-visible-to-reader",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
tx, err := store.writeDB.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("BeginTx() error = %v", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, "UPDATE workspaces SET name = ? WHERE id = ?", "Uncommitted", workspaceID); err != nil {
t.Fatalf("hold writer transaction: %v", err)
}
readCtx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
sessions, ok, err := store.ListSessions(readCtx, workspaceID)
if err != nil {
t.Fatalf("ListSessions() while writer is busy error = %v", err)
}
if !ok || len(sessions) != 1 || sessions[0].ID != "session-visible-to-reader" {
t.Fatalf("ListSessions() while writer is busy = %#v, ok=%v", sessions, ok)
}
}
func TestSQLiteStoreMigrationDropsLegacyLocalPathColumn(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
hasLocalPath, err := store.hasColumn(context.Background(), "workspaces", "local_path")
if err != nil {
t.Fatalf("hasColumn() error = %v", err)
}
if hasLocalPath {
t.Fatal("expected local_path column to be removed")
}
}
func TestSQLiteStoreMigrationRepairsAgentTargetIDColumnWhenMarkerExists(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "tuttid.db")
createLegacyAgentActivityV4MarkedDatabaseWithoutAgentTargetID(t, dbPath)
store, err := OpenSQLiteStore(dbPath)
if err != nil {
t.Fatalf("OpenSQLiteStore() error = %v", err)
}
t.Cleanup(func() {
_ = store.Close()
})
ctx := context.Background()
if err := store.Migrate(ctx); err != nil {
t.Fatalf("Migrate() error = %v", err)
}
hasAgentTargetID, err := store.hasColumn(ctx, "workspace_agent_sessions", "agent_target_id")
if err != nil {
t.Fatalf("hasColumn() error = %v", err)
}
if !hasAgentTargetID {
t.Fatal("agent_target_id column was not repaired")
}
sessions, ok, err := store.ListSessions(ctx, "ws-legacy-agent-target-id")
if err != nil {
t.Fatalf("ListSessions() error = %v", err)
}
if !ok {
t.Fatal("ListSessions() ok = false, want true")
}
if len(sessions) != 2 {
t.Fatalf("ListSessions() len = %d, want 2", len(sessions))
}
targetBySessionID := map[string]string{}
for _, session := range sessions {
targetBySessionID[session.ID] = session.AgentTargetID
}
if targetBySessionID["session-legacy"] != agenttargetbiz.IDLocalCodex {
t.Fatalf("codex AgentTargetID = %q, want %s", targetBySessionID["session-legacy"], agenttargetbiz.IDLocalCodex)
}
if targetBySessionID["session-legacy-claude"] != agenttargetbiz.IDLocalClaudeCode {
t.Fatalf("claude AgentTargetID = %q, want %s", targetBySessionID["session-legacy-claude"], agenttargetbiz.IDLocalClaudeCode)
}
}
func TestSQLiteStoreCreateUpdateAndList(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-1",
Name: "Workspace One",
}); err != nil {
t.Fatalf("Create() first error = %v", err)
}
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-2",
Name: "Workspace Two",
}); err != nil {
t.Fatalf("Create() second error = %v", err)
}
if err := store.Update(ctx, workspacebiz.Summary{
ID: "ws-1",
Name: "Workspace One Updated",
}); err != nil {
t.Fatalf("Update() error = %v", err)
}
items, err := store.List(ctx)
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(items) != 2 {
t.Fatalf("List() len = %d, want 2", len(items))
}
found := map[string]string{}
for _, item := range items {
found[item.ID] = item.Name
}
if found["ws-1"] != "Workspace One Updated" {
t.Fatalf("workspace ws-1 name = %q", found["ws-1"])
}
if found["ws-2"] != "Workspace Two" {
t.Fatalf("workspace ws-2 name = %q", found["ws-2"])
}
}
func TestSQLiteStoreCreateAndList(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-10",
Name: "Workspace Ten",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
items, err := store.List(ctx)
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(items) != 1 {
t.Fatalf("List() len = %d, want 1", len(items))
}
if items[0].ID != "ws-10" || items[0].Name != "Workspace Ten" {
t.Fatalf("item = %#v", items[0])
}
}
func TestSQLiteStoreGetUpdateDelete(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-20",
Name: "Workspace Twenty",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
item, err := store.Get(ctx, "ws-20")
if err != nil {
t.Fatalf("Get() error = %v", err)
}
if item.Name != "Workspace Twenty" {
t.Fatalf("Get() name = %q", item.Name)
}
if err := store.Update(ctx, workspacebiz.Summary{
ID: "ws-20",
Name: "Workspace Twenty Updated",
}); err != nil {
t.Fatalf("Update() error = %v", err)
}
item, err = store.Get(ctx, "ws-20")
if err != nil {
t.Fatalf("Get() after update error = %v", err)
}
if item.Name != "Workspace Twenty Updated" {
t.Fatalf("Get() updated name = %q", item.Name)
}
if err := store.Delete(ctx, "ws-20"); err != nil {
t.Fatalf("Delete() error = %v", err)
}
if _, err := store.Get(ctx, "ws-20"); !errors.Is(err, ErrWorkspaceNotFound) {
t.Fatalf("Get() after delete error = %v", err)
}
}
func TestSQLiteStoreOpenTracksLastOpenedAt(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-open-1",
Name: "Workspace Open",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
opened, err := store.Open(ctx, "ws-open-1")
if err != nil {
t.Fatalf("Open() error = %v", err)
}
if opened.LastOpenedAt == nil {
t.Fatal("Open() lastOpenedAt = nil")
}
if time.Since(*opened.LastOpenedAt) > 5*time.Second {
t.Fatalf("Open() lastOpenedAt too old = %v", opened.LastOpenedAt)
}
}
func TestSQLiteStoreGetStartup(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
startup, err := store.GetStartup(ctx)
if err != nil {
t.Fatalf("GetStartup() empty error = %v", err)
}
if startup != nil {
t.Fatalf("GetStartup() empty = %#v, want nil", startup)
}
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-start-1",
Name: "Workspace Start One",
}); err != nil {
t.Fatalf("Create() first error = %v", err)
}
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-start-2",
Name: "Workspace Start Two",
}); err != nil {
t.Fatalf("Create() second error = %v", err)
}
if _, err := store.Open(ctx, "ws-start-1"); err != nil {
t.Fatalf("Open() first error = %v", err)
}
time.Sleep(2 * time.Millisecond)
if _, err := store.Open(ctx, "ws-start-2"); err != nil {
t.Fatalf("Open() second error = %v", err)
}
startup, err = store.GetStartup(ctx)
if err != nil {
t.Fatalf("GetStartup() populated error = %v", err)
}
if startup == nil {
t.Fatal("GetStartup() populated = nil")
return
}
if startup.ID != "ws-start-2" {
t.Fatalf("GetStartup() id = %q, want %q", startup.ID, "ws-start-2")
}
}
func TestSQLiteStoreReportAndListAgentActivityMessages(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-activity",
Name: "Workspace Agent Activity",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
state, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-activity",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
ProviderSessionID: "provider-session-1",
Cwd: "/workspace",
Title: "hello",
Status: "running",
OccurredAtUnixMS: 100,
StartedAtUnixMS: 90,
})
if err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
if !state.Accepted || state.LastEventUnixMS != 100 {
t.Fatalf("state result = %#v", state)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-activity", "session-1", "turn-1", "codex", 105)
first, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-activity",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-1",
TurnID: "turn-1",
Role: "assistant",
Kind: "text",
Status: "running",
Payload: map[string]any{"text": "hel"},
OccurredAtUnixMS: 110,
}},
})
if err != nil {
t.Fatalf("ReportSessionMessages(first) error = %v", err)
}
if first.AcceptedCount != 1 || first.LatestVersion != 1 {
t.Fatalf("first result = %#v", first)
}
second, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-activity",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-1",
Status: "completed",
ContentDelta: "lo",
CompletedAtUnixMS: 120,
}},
})
if err != nil {
t.Fatalf("ReportSessionMessages(second) error = %v", err)
}
if second.AcceptedCount != 1 || second.LatestVersion != 2 {
t.Fatalf("second result = %#v", second)
}
page, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-activity",
AgentSessionID: "session-1",
Limit: 10,
})
if err != nil {
t.Fatalf("ListSessionMessages() error = %v", err)
}
if !ok {
t.Fatal("ListSessionMessages() ok = false, want true")
}
if page.LatestVersion != 2 || page.HasMore || len(page.Messages) != 1 {
t.Fatalf("page = %#v", page)
}
message := page.Messages[0]
if message.Version != 2 || message.Role != "assistant" || message.Kind != "text" || message.Status != "completed" {
t.Fatalf("message metadata = %#v", message)
}
if message.Payload["text"] != "hello" {
t.Fatalf("message payload = %#v, want text hello", message.Payload)
}
next, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-activity",
AgentSessionID: "session-1",
AfterVersion: 1,
Limit: 10,
})
if err != nil {
t.Fatalf("ListSessionMessages(after) error = %v", err)
}
if !ok || len(next.Messages) != 1 || next.Messages[0].Version != 2 {
t.Fatalf("next page = %#v ok=%v", next, ok)
}
if _, accepted, err := store.agentStore().RecordTurnTransition(ctx, agentactivitybiz.TurnTransition{
WorkspaceID: "ws-agent-activity", AgentSessionID: "session-1", TurnID: "turn-1",
Phase: agentactivitybiz.TurnPhaseSettled, Outcome: agentactivitybiz.TurnOutcomeCompleted, OccurredAtUnixMS: 125,
}); err != nil || !accepted {
t.Fatalf("settle first turn accepted=%v error=%v", accepted, err)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-activity", "session-1", "turn-2", "codex", 126)
third, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-activity",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-2",
TurnID: "turn-2",
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "newest"},
OccurredAtUnixMS: 130,
}},
})
if err != nil {
t.Fatalf("ReportSessionMessages(third) error = %v", err)
}
if third.AcceptedCount != 1 || third.LatestVersion != 3 {
t.Fatalf("third result = %#v", third)
}
turnPage, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-activity",
AgentSessionID: "session-1",
TurnID: "turn-2",
Limit: 10,
})
if err != nil {
t.Fatalf("ListSessionMessages(turn) error = %v", err)
}
if !ok || len(turnPage.Messages) != 1 || turnPage.Messages[0].MessageID != "message-2" {
t.Fatalf("turn page = %#v ok=%v", turnPage, ok)
}
latest, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-activity",
AgentSessionID: "session-1",
Limit: 1,
Order: agentactivitybiz.MessageOrderDesc,
})
if err != nil {
t.Fatalf("ListSessionMessages(desc) error = %v", err)
}
if !ok || !latest.HasMore || len(latest.Messages) != 1 || latest.Messages[0].Version != 3 {
t.Fatalf("latest page = %#v ok=%v", latest, ok)
}
older, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-activity",
AgentSessionID: "session-1",
BeforeVersion: 3,
Limit: 1,
Order: agentactivitybiz.MessageOrderDesc,
})
if err != nil {
t.Fatalf("ListSessionMessages(desc before) error = %v", err)
}
if !ok || len(older.Messages) != 1 || older.Messages[0].Version != 2 {
t.Fatalf("older page = %#v ok=%v", older, ok)
}
}
func TestSQLiteStoreClassifiesAgentSessionRailSections(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-rail",
Name: "Workspace Agent Rail",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
root := t.TempDir()
repo := filepath.Join(root, "repo")
repoSubdir := filepath.Join(repo, "packages", "api")
repoSimilarPrefix := filepath.Join(root, "repo-a")
nestedRepo := filepath.Join(repo, "packages", "web")
nestedRepoSubdir := filepath.Join(nestedRepo, "src")
for _, path := range []string{repoSubdir, repoSimilarPrefix, nestedRepoSubdir} {
if err := mkdirAll(path); err != nil {
t.Fatalf("mkdir %q error = %v", path, err)
}
}
repoCanonical := normalizeAgentSessionRailPath(repo)
nestedRepoCanonical := normalizeAgentSessionRailPath(nestedRepo)
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{
ID: "project-repo",
Path: repo,
Label: "repo",
}); err != nil {
t.Fatalf("PutUserProject(repo) error = %v", err)
}
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{
ID: "project-nested",
Path: nestedRepo,
Label: "web",
}); err != nil {
t.Fatalf("PutUserProject(nested) error = %v", err)
}
for _, tc := range []struct {
sessionID string
cwd string
wantKind string
wantProjectPath string
wantKey string
}{
{
sessionID: "session-root",
cwd: repo,
wantKind: agentSessionRailSectionKindProject,
wantProjectPath: repoCanonical,
wantKey: agentSessionRailSectionKeyForProject(repo),
},
{
sessionID: "session-subdir",
cwd: repoSubdir,
wantKind: agentSessionRailSectionKindProject,
wantProjectPath: repoCanonical,
wantKey: agentSessionRailSectionKeyForProject(repo),
},
{
sessionID: "session-similar-prefix",
cwd: repoSimilarPrefix,
wantKind: agentSessionRailSectionKindConversations,
wantProjectPath: "",
wantKey: agentSessionRailSectionKeyConversations,
},
{
sessionID: "session-nested",
cwd: nestedRepoSubdir,
wantKind: agentSessionRailSectionKindProject,
wantProjectPath: nestedRepoCanonical,
wantKey: agentSessionRailSectionKeyForProject(nestedRepo),
},
} {
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail",
AgentSessionID: tc.sessionID,
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: tc.cwd,
Status: "completed",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(%s) error = %v", tc.sessionID, err)
}
rail := getTestAgentSessionRailSection(t, store, "ws-agent-rail", tc.sessionID)
if rail.Kind != tc.wantKind || rail.ProjectPath != tc.wantProjectPath || rail.Key != tc.wantKey {
t.Fatalf("rail(%s) = %#v, want kind=%q projectPath=%q key=%q", tc.sessionID, rail, tc.wantKind, tc.wantProjectPath, tc.wantKey)
}
}
}
func TestSQLiteStoreAgentSessionRailProjectDeletionDoesNotRewriteHistory(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-rail-delete",
Name: "Workspace Agent Rail Delete",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
repo := filepath.Join(t.TempDir(), "repo")
repoSubdir := filepath.Join(repo, "pkg")
if err := mkdirAll(repoSubdir); err != nil {
t.Fatalf("mkdir repo error = %v", err)
}
repoCanonical := normalizeAgentSessionRailPath(repo)
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{
ID: "project-repo",
Path: repo,
Label: "repo",
}); err != nil {
t.Fatalf("PutUserProject(repo) error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-delete",
AgentSessionID: "session-before-delete",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: repoSubdir,
Status: "completed",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(before delete) error = %v", err)
}
beforeDelete := getTestAgentSessionRailSection(t, store, "ws-agent-rail-delete", "session-before-delete")
if beforeDelete.Key != agentSessionRailSectionKeyForProject(repoCanonical) {
t.Fatalf("before delete rail = %#v, want project key", beforeDelete)
}
if err := store.DeleteUserProject(ctx, "project-repo"); err != nil {
t.Fatalf("DeleteUserProject() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-delete",
AgentSessionID: "session-before-delete",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: repoSubdir,
Status: "completed",
OccurredAtUnixMS: 150,
}); err != nil {
t.Fatalf("ReportSessionState(after delete same cwd) error = %v", err)
}
afterDeleteUpsert := getTestAgentSessionRailSection(t, store, "ws-agent-rail-delete", "session-before-delete")
if afterDeleteUpsert != beforeDelete {
t.Fatalf("rail changed after same-cwd upsert with deleted user project: got %#v want %#v", afterDeleteUpsert, beforeDelete)
}
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{
ID: "project-repo-readded",
Path: repo,
Label: "repo",
}); err != nil {
t.Fatalf("PutUserProject(repo readded) error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-delete",
AgentSessionID: "session-after-readd",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: repoSubdir,
Status: "completed",
OccurredAtUnixMS: 200,
}); err != nil {
t.Fatalf("ReportSessionState(after readd) error = %v", err)
}
afterReadd := getTestAgentSessionRailSection(t, store, "ws-agent-rail-delete", "session-after-readd")
if afterReadd.Key != beforeDelete.Key {
t.Fatalf("rail key after readd = %q, want %q", afterReadd.Key, beforeDelete.Key)
}
}
func TestSQLiteStoreAgentSessionRailPreservesKeyAcrossCwdChangesAndRepairsMissingRail(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-rail-reclassify",
Name: "Workspace Agent Rail Reclassify",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
root := t.TempDir()
repo := filepath.Join(root, "repo")
repoSubdir := filepath.Join(repo, "pkg")
otherDir := filepath.Join(root, "other")
for _, path := range []string{repoSubdir, otherDir} {
if err := mkdirAll(path); err != nil {
t.Fatalf("mkdir %q error = %v", path, err)
}
}
repoCanonical := normalizeAgentSessionRailPath(repo)
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{
ID: "project-repo",
Path: repo,
Label: "repo",
}); err != nil {
t.Fatalf("PutUserProject(repo) error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-reclassify",
AgentSessionID: "session-cwd-change",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: repoSubdir,
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(project cwd) error = %v", err)
}
initial := getTestAgentSessionRailSection(t, store, "ws-agent-rail-reclassify", "session-cwd-change")
if initial.Kind != agentSessionRailSectionKindProject ||
initial.ProjectPath != repoCanonical ||
initial.Key != agentSessionRailSectionKeyForProject(repoCanonical) {
t.Fatalf("initial rail = %#v, want project %q", initial, repoCanonical)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-reclassify",
AgentSessionID: "session-cwd-change",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: otherDir,
Status: "completed",
OccurredAtUnixMS: 200,
}); err != nil {
t.Fatalf("ReportSessionState(other cwd) error = %v", err)
}
changed := getTestAgentSessionRailSection(t, store, "ws-agent-rail-reclassify", "session-cwd-change")
if changed != initial {
t.Fatalf("changed cwd rail = %#v, want immutable %#v", changed, initial)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-reclassify",
AgentSessionID: "session-empty-rail",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: repoSubdir,
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(empty rail initial) error = %v", err)
}
if _, err := store.writeDB.ExecContext(ctx, `
UPDATE workspace_agent_sessions
SET rail_section_kind = '',
rail_project_path = '',
rail_section_key = ''
WHERE workspace_id = ? AND agent_session_id = ?;
`, "ws-agent-rail-reclassify", "session-empty-rail"); err != nil {
t.Fatalf("clear rail fields error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-reclassify",
AgentSessionID: "session-empty-rail",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: repoSubdir,
Status: "completed",
OccurredAtUnixMS: 200,
}); err != nil {
t.Fatalf("ReportSessionState(empty rail repair) error = %v", err)
}
repaired := getTestAgentSessionRailSection(t, store, "ws-agent-rail-reclassify", "session-empty-rail")
if repaired.Key != agentSessionRailSectionKeyForProject(repoCanonical) {
t.Fatalf("repaired rail = %#v, want project key", repaired)
}
}
func TestSQLiteStoreAgentSessionRailMessageOnlySessionUsesConversations(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-rail-message-only",
Name: "Workspace Agent Rail Message Only",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-message-only", AgentSessionID: "session-message-only",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime, Provider: "codex", OccurredAtUnixMS: 90,
}); err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-rail-message-only", "session-message-only", "turn-1", "codex", 95)
if _, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-rail-message-only",
AgentSessionID: "session-message-only",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-1",
TurnID: "turn-1",
Role: "user",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "hello"},
OccurredAtUnixMS: 100,
}},
}); err != nil {
t.Fatalf("ReportSessionMessages() error = %v", err)
}
rail := getTestAgentSessionRailSection(t, store, "ws-agent-rail-message-only", "session-message-only")
if rail.Kind != agentSessionRailSectionKindConversations ||
rail.ProjectPath != "" ||
rail.Key != agentSessionRailSectionKeyConversations {
t.Fatalf("message-only rail = %#v, want conversations", rail)
}
}
func TestAgentSessionRailScratchDateDirectoriesUseConversations(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
for _, cwd := range []string{
filepath.Join(home, "Documents", "Codex", "2026-07-03", "codex-session"),
filepath.Join(home, "Documents", "Tutti", "2026-07-03", "tutti-session"),
} {
if err := mkdirAll(cwd); err != nil {
t.Fatalf("mkdir %q error = %v", cwd, err)
}
rail := classifyAgentSessionRailSection(cwd, nil, nil)
if rail.Kind != agentSessionRailSectionKindConversations ||
rail.ProjectPath != "" ||
rail.Key != agentSessionRailSectionKeyConversations {
t.Fatalf("scratch cwd %q rail = %#v, want conversations", cwd, rail)
}
}
}
func TestSQLiteStoreAgentSessionRailNoProjectPrecedesParentProject(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-rail-no-project",
Name: "Workspace Agent Rail No Project",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
scratchCwd := filepath.Join(home, "Documents", "Codex", "2026-07-03", "codex-session")
importedNoProjectCwd := filepath.Join(home, "external-import")
for _, path := range []string{scratchCwd, importedNoProjectCwd} {
if err := mkdirAll(path); err != nil {
t.Fatalf("mkdir %q error = %v", path, err)
}
}
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{
ID: "project-home",
Path: home,
Label: "home",
}); err != nil {
t.Fatalf("PutUserProject(home) error = %v", err)
}
for _, tc := range []struct {
sessionID string
cwd string
runtimeContext map[string]any
}{
{
sessionID: "session-scratch-under-home",
cwd: scratchCwd,
},
{
sessionID: "session-import-marker-under-home",
cwd: importedNoProjectCwd,
runtimeContext: map[string]any{"externalImportNoProject": true},
},
{
sessionID: "session-no-project-marker-under-home",
cwd: importedNoProjectCwd,
runtimeContext: map[string]any{"noProject": true},
},
} {
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-no-project",
AgentSessionID: tc.sessionID,
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
RuntimeContext: tc.runtimeContext,
Cwd: tc.cwd,
Status: "completed",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(%s) error = %v", tc.sessionID, err)
}
rail := getTestAgentSessionRailSection(t, store, "ws-agent-rail-no-project", tc.sessionID)
if rail.Kind != agentSessionRailSectionKindConversations ||
rail.ProjectPath != "" ||
rail.Key != agentSessionRailSectionKeyConversations {
t.Fatalf("rail(%s) = %#v, want conversations", tc.sessionID, rail)
}
}
}
func TestSQLiteStoreAgentSessionRailExactProjectPrecedesNoProject(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-rail-exact-project",
Name: "Workspace Agent Rail Exact Project",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
scratchCwd := filepath.Join(home, "Documents", "Codex", "2026-07-03", "codex-session")
if err := mkdirAll(scratchCwd); err != nil {
t.Fatalf("mkdir scratch cwd error = %v", err)
}
scratchCanonical := normalizeAgentSessionRailPath(scratchCwd)
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{
ID: "project-scratch",
Path: scratchCwd,
Label: "scratch",
}); err != nil {
t.Fatalf("PutUserProject(scratch) error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-rail-exact-project",
AgentSessionID: "session-exact-scratch",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
RuntimeContext: map[string]any{"externalImportNoProject": true},
Cwd: scratchCwd,
Status: "completed",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
rail := getTestAgentSessionRailSection(t, store, "ws-agent-rail-exact-project", "session-exact-scratch")
if rail.Kind != agentSessionRailSectionKindProject ||
rail.ProjectPath != scratchCanonical ||
rail.Key != agentSessionRailSectionKeyForProject(scratchCanonical) {
t.Fatalf("rail = %#v, want exact scratch project %q", rail, scratchCanonical)
}
}
func TestSQLiteStoreMigrationBackfillsAgentSessionRailSectionsFromUserProjects(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "tuttid.db")
store, err := OpenSQLiteStore(dbPath)
if err != nil {
t.Fatalf("OpenSQLiteStore() error = %v", err)
}
t.Cleanup(func() {
_ = store.Close()
})
repo := filepath.Join(t.TempDir(), "repo")
repoSubdir := filepath.Join(repo, "pkg")
importedNoProjectDir := filepath.Join(repo, "imported-no-project")
otherDir := filepath.Join(t.TempDir(), "other")
for _, path := range []string{repoSubdir, importedNoProjectDir, otherDir} {
if err := mkdirAll(path); err != nil {
t.Fatalf("mkdir %q error = %v", path, err)
}
}
repoCanonical := normalizeAgentSessionRailPath(repo)
ctx := context.Background()
if _, err := store.writeDB.ExecContext(ctx, `
CREATE TABLE tuttid_schema_migrations (
id TEXT PRIMARY KEY,
applied_at_unix_ms INTEGER NOT NULL
);
CREATE TABLE user_projects (
id TEXT PRIMARY KEY,
path TEXT NOT NULL UNIQUE,
label TEXT NOT NULL,
created_at_unix_ms INTEGER NOT NULL,
updated_at_unix_ms INTEGER NOT NULL,
last_used_at_unix_ms INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE workspace_agent_sessions (
workspace_id TEXT NOT NULL,
agent_session_id TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT '',
runtime_context_json TEXT NOT NULL DEFAULT '{}',
cwd TEXT NOT NULL DEFAULT '',
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
created_at_unix_ms INTEGER NOT NULL DEFAULT 0,
updated_at_unix_ms INTEGER NOT NULL,
PRIMARY KEY (workspace_id, agent_session_id)
);
`); err != nil {
t.Fatalf("create legacy rail schema error = %v", err)
}
if _, err := store.writeDB.ExecContext(ctx, `
INSERT INTO user_projects (id, path, label, created_at_unix_ms, updated_at_unix_ms, last_used_at_unix_ms)
VALUES ('project-repo', ?, 'repo', 1, 1, 1);
`, repo); err != nil {
t.Fatalf("insert legacy user project error = %v", err)
}
if _, err := store.writeDB.ExecContext(ctx, `
INSERT INTO workspace_agent_sessions (workspace_id, agent_session_id, runtime_context_json, cwd, deleted_at_unix_ms, updated_at_unix_ms)
VALUES
('ws-agent-rail-migration', 'session-project', '{}', ?, 0, 1),
('ws-agent-rail-migration', 'session-import-no-project', '{"externalImportNoProject":true}', ?, 0, 1),
('ws-agent-rail-migration', 'session-conversations', '{}', ?, 0, 1);
`, repoSubdir, importedNoProjectDir, otherDir); err != nil {
t.Fatalf("insert legacy agent sessions error = %v", err)
}
if err := store.Migrate(ctx); err != nil {
t.Fatalf("Migrate() error = %v", err)
}
projectRail := getTestAgentSessionRailSection(t, store, "ws-agent-rail-migration", "session-project")
if projectRail.Kind != agentSessionRailSectionKindProject ||
projectRail.ProjectPath != repoCanonical ||
projectRail.Key != agentSessionRailSectionKeyForProject(repoCanonical) {
t.Fatalf("project rail = %#v, want project %q", projectRail, repoCanonical)
}
importNoProjectRail := getTestAgentSessionRailSection(t, store, "ws-agent-rail-migration", "session-import-no-project")
if importNoProjectRail.Kind != agentSessionRailSectionKindConversations ||
importNoProjectRail.ProjectPath != "" ||
importNoProjectRail.Key != agentSessionRailSectionKeyConversations {
t.Fatalf("import no-project rail = %#v, want conversations", importNoProjectRail)
}
conversationRail := getTestAgentSessionRailSection(t, store, "ws-agent-rail-migration", "session-conversations")
if conversationRail.Kind != agentSessionRailSectionKindConversations ||
conversationRail.ProjectPath != "" ||
conversationRail.Key != agentSessionRailSectionKeyConversations {
t.Fatalf("conversation rail = %#v, want conversations", conversationRail)
}
}
func TestSQLiteStorePreservesAgentTargetIDAcrossStateReports(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-target-activity",
Name: "Workspace Agent Target Activity",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-target-activity",
AgentSessionID: "session-1",
AgentTargetID: "local:codex",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
ProviderSessionID: "provider-session-1",
Cwd: "/workspace",
Title: "target session",
Status: "running",
OccurredAtUnixMS: 100,
StartedAtUnixMS: 90,
}); err != nil {
t.Fatalf("ReportSessionState(with target) error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-target-activity",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: "/workspace",
Title: "target session",
Status: "ready",
OccurredAtUnixMS: 200,
StartedAtUnixMS: 90,
}); err != nil {
t.Fatalf("ReportSessionState(without target) error = %v", err)
}
session, ok, err := store.GetSession(ctx, "ws-agent-target-activity", "session-1")
if err != nil {
t.Fatalf("GetSession() error = %v", err)
}
if !ok {
t.Fatal("GetSession() ok = false, want true")
}
if session.AgentTargetID != "local:codex" {
t.Fatalf("GetSession() agent target id = %q, want local:codex", session.AgentTargetID)
}
sessions, ok, err := store.ListSessions(ctx, "ws-agent-target-activity")
if err != nil {
t.Fatalf("ListSessions() error = %v", err)
}
if !ok || len(sessions) != 1 {
t.Fatalf("ListSessions() ok=%v len=%d, want one session", ok, len(sessions))
}
if sessions[0].AgentTargetID != "local:codex" {
t.Fatalf("ListSessions() agent target id = %q, want local:codex", sessions[0].AgentTargetID)
}
}
func TestSQLiteStoreGeneratedFilesDoNotFallbackToMessages(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-generated-files",
Name: "Workspace Agent Generated Files",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
for _, session := range []struct {
id string
cwd string
}{
{id: "session-1", cwd: "/workspace"},
{id: "session-2", cwd: "/workspace/other"},
} {
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-generated-files",
AgentSessionID: session.id,
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: session.cwd,
Status: "completed",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(%s) error = %v", session.id, err)
}
}
seedTestAgentTurn(t, store, ctx, "ws-agent-generated-files", "session-1", "turn-1", "codex", 105)
seedTestAgentTurn(t, store, ctx, "ws-agent-generated-files", "session-2", "turn-2", "codex", 105)
if _, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-generated-files",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{
{
MessageID: "message-1",
TurnID: "turn-1",
Role: "assistant",
Kind: "tool_call",
Status: "completed",
Payload: map[string]any{
"toolName": "Write",
"fileChanges": map[string]any{
"files": []any{
map[string]any{"path": "report.md"},
},
},
},
OccurredAtUnixMS: 110,
},
{
MessageID: "message-1b",
TurnID: "turn-1",
Role: "assistant",
Kind: "tool_call",
Status: "completed",
Payload: map[string]any{
"toolName": "Edit",
"input": map[string]any{
"file_path": "assets/styles.css",
"changes": []any{
map[string]any{
"path": "slides/02-why-now.html",
"kind": map[string]any{"type": "add"},
"diff": "<section>Why now</section>\n",
},
map[string]any{
"path": "slides/01-cover.html",
"kind": map[string]any{"type": "update"},
"diff": "@@ -1 +1 @@\n-Old\n+New\n",
},
},
},
},
OccurredAtUnixMS: 115,
},
},
}); err != nil {
t.Fatalf("ReportSessionMessages(session-1) error = %v", err)
}
if _, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-generated-files",
AgentSessionID: "session-2",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-2",
TurnID: "turn-2",
Role: "assistant",
Kind: "tool_call",
Status: "completed",
Payload: map[string]any{
"output": map[string]any{
"path": "/workspace/other/notes.txt",
},
},
OccurredAtUnixMS: 120,
}},
}); err != nil {
t.Fatalf("ReportSessionMessages(session-2) error = %v", err)
}
result, ok, err := store.ListWorkspaceGeneratedFileTurns(ctx, agentactivitybiz.ListWorkspaceGeneratedFileTurnsInput{
WorkspaceID: "ws-agent-generated-files",
SectionKey: "conversations",
})
if err != nil {
t.Fatalf("ListWorkspaceGeneratedFileTurns() error = %v", err)
}
if !ok {
t.Fatal("ListWorkspaceGeneratedFileTurns() ok = false, want true")
}
if len(result.Turns) != 0 {
t.Fatalf("turns = %#v, want no legacy message fallback", result.Turns)
}
arrayResult, ok, err := store.ListWorkspaceGeneratedFileTurns(ctx, agentactivitybiz.ListWorkspaceGeneratedFileTurnsInput{
WorkspaceID: "ws-agent-generated-files",
SectionKey: "conversations",
})
if err != nil {
t.Fatalf("ListWorkspaceGeneratedFileTurns(array changes) error = %v", err)
}
if !ok {
t.Fatal("ListWorkspaceGeneratedFileTurns(array changes) ok = false, want true")
}
if len(arrayResult.Turns) != 0 {
t.Fatalf("array turns = %#v, want no legacy message fallback", arrayResult.Turns)
}
}
func TestSQLiteStoreGeneratedFilesIgnoreAllMessageToolPayloads(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-generated-files-failed",
Name: "Workspace Agent Generated Files Failed",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-generated-files-failed",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: "/workspace",
Status: "completed",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-generated-files-failed", "session-1", "turn-1", "codex", 105)
if _, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-generated-files-failed",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{
{
MessageID: "failed-status",
TurnID: "turn-1",
Role: "assistant",
Kind: "tool_call",
Status: "failed",
Payload: map[string]any{
"toolName": "Write",
"fileChanges": map[string]any{
"files": []any{
map[string]any{"path": "failed-status.md"},
},
},
},
OccurredAtUnixMS: 110,
},
{
MessageID: "failed-output",
TurnID: "turn-1",
Role: "assistant",
Kind: "tool_call",
Status: "completed",
Payload: map[string]any{
"toolName": "Write",
"output": map[string]any{
"path": "failed-output.md",
"status": "failed",
"success": false,
},
},
OccurredAtUnixMS: 120,
},
{
MessageID: "read-tool",
TurnID: "turn-1",
Role: "assistant",
Kind: "tool_call",
Status: "completed",
Payload: map[string]any{
"toolName": "Bash",
"fileChanges": map[string]any{
"files": []any{
map[string]any{"path": "read-filechanges.md"},
},
},
"input": map[string]any{
"command": "nl -ba readme.md",
"changes": map[string]any{"read-input-changes.md": "read"},
"file_path": "readme.md",
},
"output": map[string]any{
"status": "completed",
"success": true,
},
},
OccurredAtUnixMS: 130,
},
{
MessageID: "successful-write",
TurnID: "turn-1",
Role: "assistant",
Kind: "tool_call",
Status: "completed",
Payload: map[string]any{
"toolName": "Write",
"output": map[string]any{
"filePath": "ok.md",
"status": "completed",
"success": true,
},
},
OccurredAtUnixMS: 140,
},
},
}); err != nil {
t.Fatalf("ReportSessionMessages() error = %v", err)
}
result, ok, err := store.ListWorkspaceGeneratedFileTurns(ctx, agentactivitybiz.ListWorkspaceGeneratedFileTurnsInput{
WorkspaceID: "ws-agent-generated-files-failed",
SectionKey: "conversations",
})
if err != nil {
t.Fatalf("ListWorkspaceGeneratedFileTurns() error = %v", err)
}
if !ok {
t.Fatal("ListWorkspaceGeneratedFileTurns() ok = false, want true")
}
if len(result.Turns) != 0 {
t.Fatalf("turns = %#v, want no message-derived turns", result.Turns)
}
}
func TestSQLiteStoreReportsProviderSessionMessagesToCanonicalAgentSession(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-provider-message",
Name: "Workspace Agent Provider Message",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-provider-message",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
ProviderSessionID: "provider-session-1",
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-provider-message", "session-1", "turn-approval-1", "codex", 105)
result, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-provider-message",
AgentSessionID: "provider-session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "approval-1",
TurnID: "turn-approval-1",
Role: "assistant",
Kind: "tool_call",
Status: "waiting",
Payload: map[string]any{"toolName": "Bash"},
OccurredAtUnixMS: 110,
}},
})
if err != nil {
t.Fatalf("ReportSessionMessages() error = %v", err)
}
if result.AcceptedCount != 1 || result.LatestVersion != 1 {
t.Fatalf("result = %#v, want one canonical message", result)
}
if result.Messages[0].AgentSessionID != "session-1" {
t.Fatalf("accepted message session id = %q, want session-1", result.Messages[0].AgentSessionID)
}
page, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-provider-message",
AgentSessionID: "session-1",
Limit: 10,
})
if err != nil {
t.Fatalf("ListSessionMessages(canonical) error = %v", err)
}
if !ok || len(page.Messages) != 1 || page.Messages[0].AgentSessionID != "session-1" {
t.Fatalf("canonical page = %#v ok=%v, want message under session-1", page, ok)
}
if _, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-provider-message",
AgentSessionID: "provider-session-1",
Limit: 10,
}); err != nil || ok {
t.Fatalf("ListSessionMessages(provider) ok=%v error=%v, want no alias session", ok, err)
}
}
func TestSQLiteStoreReportsProviderSessionMessagesToSameOriginSession(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-provider-origin-message",
Name: "Workspace Agent Provider Origin Message",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-provider-origin-message",
AgentSessionID: "runtime-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
ProviderSessionID: "shared-provider-session",
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(runtime) error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-provider-origin-message",
AgentSessionID: "runtime-2",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
ProviderSessionID: "shared-provider-session",
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(runtime-2) error = %v", err)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-provider-origin-message", "shared-provider-session", "turn-runtime-1", "codex", 105)
result, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-provider-origin-message",
AgentSessionID: "shared-provider-session",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "runtime-message-1",
TurnID: "turn-runtime-1",
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "runtime"},
OccurredAtUnixMS: 110,
}},
})
if err != nil {
t.Fatalf("ReportSessionMessages() error = %v", err)
}
if result.AcceptedCount != 1 || result.Messages[0].AgentSessionID != "shared-provider-session" {
t.Fatalf("result = %#v, want ambiguous message under provider session", result)
}
providerPage, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-provider-origin-message",
AgentSessionID: "shared-provider-session",
Limit: 10,
})
if err != nil {
t.Fatalf("ListSessionMessages(provider) error = %v", err)
}
if !ok || len(providerPage.Messages) != 1 || providerPage.Messages[0].AgentSessionID != "shared-provider-session" {
t.Fatalf("provider page = %#v ok=%v, want ambiguous provider message", providerPage, ok)
}
runtimePage, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-provider-origin-message",
AgentSessionID: "runtime-1",
Limit: 10,
})
if err != nil {
t.Fatalf("ListSessionMessages(runtime) error = %v", err)
}
if !ok || len(runtimePage.Messages) != 0 {
t.Fatalf("runtime page = %#v ok=%v, want no misrouted message", runtimePage, ok)
}
}
func TestSQLiteStoreDoesNotResolveProviderSessionMessagesAcrossProviders(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-provider-collision-message",
Name: "Workspace Agent Provider Collision Message",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-provider-collision-message",
AgentSessionID: "codex-session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
ProviderSessionID: "shared-provider-session",
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(codex) error = %v", err)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-provider-collision-message", "shared-provider-session", "turn-claude-1", "claude", 105)
result, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-provider-collision-message",
AgentSessionID: "shared-provider-session",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "claude",
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "claude-message-1",
TurnID: "turn-claude-1",
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "claude"},
OccurredAtUnixMS: 110,
}},
})
if err != nil {
t.Fatalf("ReportSessionMessages() error = %v", err)
}
if result.AcceptedCount != 1 || result.Messages[0].AgentSessionID != "shared-provider-session" {
t.Fatalf("result = %#v, want message under raw claude provider session", result)
}
codexPage, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-provider-collision-message",
AgentSessionID: "codex-session-1",
Limit: 10,
})
if err != nil {
t.Fatalf("ListSessionMessages(codex) error = %v", err)
}
if !ok || len(codexPage.Messages) != 0 {
t.Fatalf("codex page = %#v ok=%v, want no provider-mismatched message", codexPage, ok)
}
}
func TestSQLiteStorePersistsLargeAgentActivityMessagePayload(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
largeContent := strings.Repeat("完整回答", 24000)
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-large-message",
Name: "Workspace Agent Large Message",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-large-message", "session-large", "turn-large", "codex", 105)
result, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-large-message",
AgentSessionID: "session-large",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-large",
TurnID: "turn-large",
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"content": largeContent, "text": largeContent},
OccurredAtUnixMS: 110,
}},
})
if err != nil {
t.Fatalf("ReportSessionMessages() error = %v", err)
}
if result.AcceptedCount != 1 || result.LatestVersion != 1 {
t.Fatalf("result = %#v, want one accepted message", result)
}
page, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-large-message",
AgentSessionID: "session-large",
Limit: 10,
})
if err != nil {
t.Fatalf("ListSessionMessages() error = %v", err)
}
if !ok || len(page.Messages) != 1 {
t.Fatalf("page = %#v ok=%v, want one message", page, ok)
}
message := page.Messages[0]
content, _ := message.Payload["content"].(string)
text, _ := message.Payload["text"].(string)
if content != largeContent || text != largeContent {
t.Fatalf(
"message payload lengths content=%d text=%d, want %d",
len(content),
len(text),
len(largeContent),
)
}
}
func TestSQLiteStoreReportSessionStateReturnsCurrentLastEventForStalePatch(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-state-order",
Name: "Workspace Agent State Order",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-state-order",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Status: "completed",
CurrentPhase: "idle",
OccurredAtUnixMS: 200,
EndedAtUnixMS: 200,
}); err != nil {
t.Fatalf("ReportSessionState(completed) error = %v", err)
}
state, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-state-order",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Status: "active",
CurrentPhase: "working",
OccurredAtUnixMS: 150,
})
if err != nil {
t.Fatalf("ReportSessionState(stale) error = %v", err)
}
if !state.Accepted || state.LastEventUnixMS != 200 {
t.Fatalf("stale state result = %#v, want accepted with current last event 200", state)
}
if state.StateApplied {
t.Fatalf("stale state applied = true, want false")
}
if state.Session.LastEventUnixMS != 200 {
t.Fatalf("projected session last event = %d, want 200", state.Session.LastEventUnixMS)
}
session, ok, err := store.GetSession(ctx, "ws-agent-state-order", "session-1")
if err != nil {
t.Fatalf("GetSession() error = %v", err)
}
if !ok {
t.Fatal("GetSession() ok = false, want true")
}
if session.LastEventUnixMS != 200 {
t.Fatalf("session last event = %d, want 200", session.LastEventUnixMS)
}
}
func TestSQLiteStoreListAgentSessionsByUpdatedAtDescending(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-session-order",
Name: "Workspace Agent Session Order",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-session-order",
AgentSessionID: "session-older",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: "/workspace",
Status: "completed",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(session-older) error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-session-order",
AgentSessionID: "session-newer",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: "/workspace",
Status: "running",
OccurredAtUnixMS: 200,
}); err != nil {
t.Fatalf("ReportSessionState(session-newer) error = %v", err)
}
sessions, ok, err := store.ListSessions(ctx, "ws-agent-session-order")
if err != nil {
t.Fatalf("ListSessions() error = %v", err)
}
if !ok {
t.Fatal("ListSessions() ok = false, want true")
}
if len(sessions) != 2 {
t.Fatalf("ListSessions() len = %d, want 2", len(sessions))
}
if sessions[0].ID != "session-newer" || sessions[1].ID != "session-older" {
t.Fatalf("ListSessions() order = [%s %s], want [session-newer session-older]", sessions[0].ID, sessions[1].ID)
}
}
func TestSQLiteStoreListSessionSectionPagesByRailSectionKey(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-section-page",
Name: "Workspace Agent Section Page",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{
ID: "project-app",
Path: "/workspace/app",
Label: "App",
}); err != nil {
t.Fatalf("PutUserProject() error = %v", err)
}
for _, input := range []agentactivitybiz.SessionStateReport{
{
WorkspaceID: "ws-agent-section-page",
AgentSessionID: "project-newer",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: "/workspace/app",
Status: "completed",
OccurredAtUnixMS: 300,
},
{
WorkspaceID: "ws-agent-section-page",
AgentSessionID: "project-older",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: "/workspace/app/pkg",
Status: "completed",
OccurredAtUnixMS: 200,
},
{
WorkspaceID: "ws-agent-section-page",
AgentSessionID: "chat",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: "/scratch/session",
RuntimeContext: map[string]any{"externalImportNoProject": true},
Status: "completed",
OccurredAtUnixMS: 100,
},
} {
if err := reportSessionStateWithStableUpdatedAt(ctx, store, input); err != nil {
t.Fatalf("ReportSessionState(%s) error = %v", input.AgentSessionID, err)
}
}
projectPage, ok, err := store.ListSessionSection(ctx, agentactivitybiz.ListSessionSectionInput{
WorkspaceID: "ws-agent-section-page",
SectionKey: "project:/workspace/app",
Limit: 1,
})
if err != nil {
t.Fatalf("ListSessionSection(project) error = %v", err)
}
if !ok {
t.Fatal("ListSessionSection(project) ok=false, want true")
}
if got, want := activitySessionIDs(projectPage.Sessions), []string{"project-older"}; !slices.Equal(got, want) {
t.Fatalf("project page sessions = %#v, want %#v", got, want)
}
cursorParts := strings.SplitN(projectPage.NextCursor, "|", 2)
if !projectPage.HasMore || len(cursorParts) != 2 || cursorParts[1] != "project-older" {
t.Fatalf("project page cursor = %q hasMore=%v, want cursor for project-older true", projectPage.NextCursor, projectPage.HasMore)
}
nextProjectPage, ok, err := store.ListSessionSection(ctx, agentactivitybiz.ListSessionSectionInput{
WorkspaceID: "ws-agent-section-page",
SectionKey: "project:/workspace/app",
CursorSortTimeUnixMS: projectPage.Sessions[0].CreatedAtUnixMS,
CursorSessionID: "project-older",
Limit: 2,
})
if err != nil {
t.Fatalf("ListSessionSection(project next) error = %v", err)
}
if !ok {
t.Fatal("ListSessionSection(project next) ok=false, want true")
}
if got, want := activitySessionIDs(nextProjectPage.Sessions), []string{"project-newer"}; !slices.Equal(got, want) {
t.Fatalf("project next sessions = %#v, want %#v", got, want)
}
conversationsPage, ok, err := store.ListSessionSection(ctx, agentactivitybiz.ListSessionSectionInput{
WorkspaceID: "ws-agent-section-page",
SectionKey: "conversations",
Limit: 5,
})
if err != nil {
t.Fatalf("ListSessionSection(conversations) error = %v", err)
}
if !ok {
t.Fatal("ListSessionSection(conversations) ok=false, want true")
}
if got, want := activitySessionIDs(conversationsPage.Sessions), []string{"chat"}; !slices.Equal(got, want) {
t.Fatalf("conversations sessions = %#v, want %#v", got, want)
}
}
func TestSQLiteStoreListSessionSectionFiltersAgentTargetBeforePagination(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-section-target-filter",
Name: "Workspace Agent Section Target Filter",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{
ID: "project-app",
Path: "/workspace/app",
Label: "App",
}); err != nil {
t.Fatalf("PutUserProject() error = %v", err)
}
reports := []agentactivitybiz.SessionStateReport{
{
WorkspaceID: "ws-agent-section-target-filter",
AgentSessionID: "claude-1",
AgentTargetID: "claude-target",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "claude-code",
Cwd: "/workspace/app",
Status: "completed",
OccurredAtUnixMS: 600,
},
{
WorkspaceID: "ws-agent-section-target-filter",
AgentSessionID: "claude-2",
AgentTargetID: "claude-target",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "claude-code",
Cwd: "/workspace/app/pkg",
Status: "completed",
OccurredAtUnixMS: 500,
},
}
for index := range 4 {
reports = append(reports, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-section-target-filter",
AgentSessionID: fmt.Sprintf("codex-%d", index+1),
AgentTargetID: "codex-target",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Cwd: "/workspace/app",
Status: "completed",
OccurredAtUnixMS: int64(400 - index),
})
}
for _, input := range reports {
if err := reportSessionStateWithStableUpdatedAt(ctx, store, input); err != nil {
t.Fatalf("ReportSessionState(%s) error = %v", input.AgentSessionID, err)
}
}
allTargetsPage, ok, err := store.ListSessionSection(ctx, agentactivitybiz.ListSessionSectionInput{
WorkspaceID: "ws-agent-section-target-filter",
SectionKey: "project:/workspace/app",
Limit: 5,
})
if err != nil {
t.Fatalf("ListSessionSection(all targets) error = %v", err)
}
if !ok {
t.Fatal("ListSessionSection(all targets) ok=false, want true")
}
if !allTargetsPage.HasMore {
t.Fatalf("all targets hasMore = false, want true")
}
claudePage, ok, err := store.ListSessionSection(ctx, agentactivitybiz.ListSessionSectionInput{
WorkspaceID: "ws-agent-section-target-filter",
SectionKey: "project:/workspace/app",
AgentTargetID: "claude-target",
Limit: 5,
})
if err != nil {
t.Fatalf("ListSessionSection(claude target) error = %v", err)
}
if !ok {
t.Fatal("ListSessionSection(claude target) ok=false, want true")
}
if got, want := activitySessionIDs(claudePage.Sessions), []string{"claude-2", "claude-1"}; !slices.Equal(got, want) {
t.Fatalf("claude sessions = %#v, want %#v", got, want)
}
if claudePage.HasMore || claudePage.NextCursor != "" {
t.Fatalf("claude page state = hasMore %v cursor %q, want false empty", claudePage.HasMore, claudePage.NextCursor)
}
}
func TestSQLiteStoreSessionSectionSeparatesPinnedAndDeletionCandidates(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
const workspaceID = "ws-agent-section-pinned"
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Pinned Sections"}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.PutUserProject(ctx, userprojectbiz.Project{ID: "project-app", Path: "/workspace/app", Label: "App"}); err != nil {
t.Fatalf("PutUserProject() error = %v", err)
}
for index, sessionID := range []string{"ordinary", "pinned"} {
if err := reportSessionStateWithStableUpdatedAt(ctx, store, agentactivitybiz.SessionStateReport{
WorkspaceID: workspaceID, AgentSessionID: sessionID,
AgentTargetID: "codex-target", Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex", Cwd: "/workspace/app", Status: "completed", OccurredAtUnixMS: int64(100 + index),
}); err != nil {
t.Fatalf("ReportSessionState(%s) error = %v", sessionID, err)
}
}
if _, ok, err := store.UpdateSessionPinned(ctx, workspaceID, "pinned", true); err != nil || !ok {
t.Fatalf("UpdateSessionPinned() ok=%v error=%v, want true nil", ok, err)
}
ordinaryPage, ok, err := store.ListSessionSection(ctx, agentactivitybiz.ListSessionSectionInput{
WorkspaceID: workspaceID, SectionKey: "project:/workspace/app", Limit: 1,
})
if err != nil || !ok {
t.Fatalf("ListSessionSection() ok=%v error=%v", ok, err)
}
if got, want := activitySessionIDs(ordinaryPage.Sessions), []string{"ordinary"}; !slices.Equal(got, want) || ordinaryPage.HasMore {
t.Fatalf("ordinary page = %#v hasMore=%v, want %#v false", got, ordinaryPage.HasMore, want)
}
pinnedPage, ok, err := store.ListSessionSection(ctx, agentactivitybiz.ListSessionSectionInput{
WorkspaceID: workspaceID, SectionKey: agentactivitybiz.PinnedSessionPageKey, Limit: 5,
})
if err != nil || !ok {
t.Fatalf("ListSessionSection(pinned) ok=%v error=%v", ok, err)
}
if got, want := activitySessionIDs(pinnedPage.Sessions), []string{"pinned"}; !slices.Equal(got, want) {
t.Fatalf("pinned page = %#v, want %#v", got, want)
}
excluded, ok, err := store.ListSessionSectionDeletionCandidates(ctx, agentactivitybiz.ListSessionSectionDeletionCandidatesInput{
WorkspaceID: workspaceID, SectionKey: "project:/workspace/app", AgentTargetID: "codex-target", ExcludePinned: true,
})
if err != nil || !ok {
t.Fatalf("ListSessionSectionDeletionCandidates(excluded) ok=%v error=%v", ok, err)
}
if got, want := excluded.SessionIDs, []string{"ordinary"}; !slices.Equal(got, want) {
t.Fatalf("excluded candidates = %#v, want %#v", got, want)
}
included, ok, err := store.ListSessionSectionDeletionCandidates(ctx, agentactivitybiz.ListSessionSectionDeletionCandidatesInput{
WorkspaceID: workspaceID, SectionKey: "project:/workspace/app",
})
if err != nil || !ok {
t.Fatalf("ListSessionSectionDeletionCandidates(included) ok=%v error=%v", ok, err)
}
if got, want := included.SessionIDs, []string{"pinned", "ordinary"}; !slices.Equal(got, want) {
t.Fatalf("included candidates = %#v, want %#v", got, want)
}
}
func TestSQLiteStoreDeleteSessionsBatchUsesExactIdempotentSnapshot(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
for _, workspaceID := range []string{"ws-agent-batch-delete", "ws-agent-batch-other"} {
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: workspaceID}); err != nil {
t.Fatalf("Create(%s) error = %v", workspaceID, err)
}
}
for _, sessionID := range []string{"snapshot", "new-session"} {
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-batch-delete", AgentSessionID: sessionID,
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime, Provider: "codex",
Cwd: "/workspace/app", Status: "completed", OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(%s) error = %v", sessionID, err)
}
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-batch-other", AgentSessionID: "snapshot",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime, Provider: "codex", Status: "completed", OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(other) error = %v", err)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-batch-delete", "snapshot", "turn-snapshot", "codex", 105)
if _, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-batch-delete", AgentSessionID: "snapshot",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{MessageID: "message-snapshot", TurnID: "turn-snapshot", Role: "assistant", Kind: "text", Status: "completed", Payload: map[string]any{"text": "done"}}},
}); err != nil {
t.Fatalf("ReportSessionMessages() error = %v", err)
}
if _, ok, err := store.UpdateSessionPinned(ctx, "ws-agent-batch-delete", "snapshot", true); err != nil || !ok {
t.Fatalf("UpdateSessionPinned() ok=%v error=%v", ok, err)
}
result, err := store.DeleteSessionsBatch(ctx, agentactivitybiz.DeleteSessionsBatchInput{
WorkspaceID: "ws-agent-batch-delete", SessionIDs: []string{"snapshot", "missing"},
})
if err != nil {
t.Fatalf("DeleteSessionsBatch() error = %v", err)
}
if result.RemovedSessions != 1 || result.RemovedMessages != 1 || !slices.Equal(result.RemovedSessionIDs, []string{"snapshot"}) {
t.Fatalf("DeleteSessionsBatch() = %#v", result)
}
if _, ok, _ := store.GetSession(ctx, "ws-agent-batch-delete", "new-session"); !ok {
t.Fatal("new-session was deleted, want retained")
}
if _, ok, _ := store.GetSession(ctx, "ws-agent-batch-other", "snapshot"); !ok {
t.Fatal("other workspace session was deleted, want retained")
}
retry, err := store.DeleteSessionsBatch(ctx, agentactivitybiz.DeleteSessionsBatchInput{
WorkspaceID: "ws-agent-batch-delete", SessionIDs: []string{"snapshot"},
})
if err != nil || retry.RemovedSessions != 0 || len(retry.RemovedSessionIDs) != 0 {
t.Fatalf("DeleteSessionsBatch(retry) = %#v error=%v, want no-op", retry, err)
}
}
func TestSQLiteStoreDeleteAgentActivitySessionSoftDeletesMessages(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-delete",
Name: "Workspace Agent Delete",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-delete",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
seedTestAgentTurn(t, store, ctx, "ws-agent-delete", "session-1", "turn-1", "codex", 105)
if _, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-delete",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-1",
TurnID: "turn-1",
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "done"},
}},
}); err != nil {
t.Fatalf("ReportSessionMessages() error = %v", err)
}
deleteResult, err := store.DeleteSessionsBatch(ctx, agentactivitybiz.DeleteSessionsBatchInput{
WorkspaceID: "ws-agent-delete", SessionIDs: []string{"session-1"},
})
if err != nil {
t.Fatalf("DeleteSessionsBatch() error = %v", err)
}
if deleteResult.RemovedSessions != 1 {
t.Fatalf("DeleteSessionsBatch() = %#v, want one removed session", deleteResult)
}
if _, ok, err := store.GetSession(ctx, "ws-agent-delete", "session-1"); err != nil || ok {
t.Fatalf("GetSession() after delete ok=%v error=%v, want ok=false", ok, err)
}
if _, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-agent-delete",
AgentSessionID: "session-1",
Limit: 10,
}); err != nil || ok {
t.Fatalf("ListSessionMessages() after delete ok=%v error=%v, want ok=false", ok, err)
}
}
func activitySessionIDs(sessions []agentactivitybiz.Session) []string {
ids := make([]string, 0, len(sessions))
for _, session := range sessions {
ids = append(ids, session.ID)
}
return ids
}
func reportSessionStateWithStableUpdatedAt(
ctx context.Context,
store *SQLiteStore,
input agentactivitybiz.SessionStateReport,
) error {
if _, err := store.ReportSessionState(ctx, input); err != nil {
return err
}
time.Sleep(2 * time.Millisecond)
return nil
}
func TestSQLiteStoreClearAgentActivitySessionsHardDeletesTombstones(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
const workspaceID = "ws-agent-clear"
if err := store.Create(ctx, workspacebiz.Summary{
ID: workspaceID,
Name: "Workspace Agent Clear",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
for _, sessionID := range []string{"session-1", "session-2"} {
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: workspaceID,
AgentSessionID: sessionID,
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Status: "completed",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState(%s) error = %v", sessionID, err)
}
seedTestAgentTurn(t, store, ctx, workspaceID, sessionID, "turn-"+sessionID, "codex", 105)
if _, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: workspaceID,
AgentSessionID: sessionID,
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-" + sessionID,
TurnID: "turn-" + sessionID,
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "done"},
}},
}); err != nil {
t.Fatalf("ReportSessionMessages(%s) error = %v", sessionID, err)
}
}
if removed, err := store.DeleteSessionsBatch(ctx, agentactivitybiz.DeleteSessionsBatchInput{WorkspaceID: workspaceID, SessionIDs: []string{"session-1"}}); err != nil || removed.RemovedSessions != 1 {
t.Fatalf("DeleteSessionsBatch() result=%#v error=%v, want one removed session", removed, err)
}
result, err := store.ClearSessions(ctx, workspaceID)
if err != nil {
t.Fatalf("ClearSessions() error = %v", err)
}
if result.RemovedSessions != 2 || result.RemovedMessages != 2 {
t.Fatalf("ClearSessions() = %#v, want 2 sessions and 2 messages", result)
}
removedIDs := map[string]bool{}
for _, sessionID := range result.RemovedSessionIDs {
removedIDs[sessionID] = true
}
if !removedIDs["session-1"] || !removedIDs["session-2"] {
t.Fatalf("ClearSessions() removed session IDs = %#v, want session-1 and session-2", result.RemovedSessionIDs)
}
recreated, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: workspaceID,
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Status: "completed",
OccurredAtUnixMS: 200,
})
if err != nil {
t.Fatalf("ReportSessionState() after clear error = %v", err)
}
if !recreated.Accepted {
t.Fatal("ReportSessionState() after clear accepted = false, want true")
}
seedTestAgentTurn(t, store, ctx, workspaceID, "session-1", "turn-reimported", "codex", 205)
messageResult, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: workspaceID,
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-reimported",
TurnID: "turn-reimported",
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "reimported"},
}},
})
if err != nil {
t.Fatalf("ReportSessionMessages() after clear error = %v", err)
}
if messageResult.AcceptedCount != 1 {
t.Fatalf("ReportSessionMessages() after clear accepted count = %d, want 1", messageResult.AcceptedCount)
}
}
func TestSQLiteStoreUpdateAgentActivitySessionPinned(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-pin",
Name: "Workspace Agent Pin",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-pin",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
session, ok, err := store.GetSession(ctx, "ws-agent-pin", "session-1")
if err != nil {
t.Fatalf("GetSession() error = %v", err)
}
if !ok {
t.Fatal("GetSession() ok = false, want true")
}
if session.PinnedAtUnixMS != 0 {
t.Fatalf("default pinnedAtUnixMS = %d, want 0", session.PinnedAtUnixMS)
}
pinned, ok, err := store.UpdateSessionPinned(ctx, "ws-agent-pin", "session-1", true)
if err != nil {
t.Fatalf("UpdateSessionPinned(pin) error = %v", err)
}
if !ok {
t.Fatal("UpdateSessionPinned(pin) ok = false, want true")
}
if pinned.PinnedAtUnixMS <= 0 {
t.Fatalf("pinnedAtUnixMS after pin = %d, want > 0", pinned.PinnedAtUnixMS)
}
unpinned, ok, err := store.UpdateSessionPinned(ctx, "ws-agent-pin", "session-1", false)
if err != nil {
t.Fatalf("UpdateSessionPinned(unpin) error = %v", err)
}
if !ok {
t.Fatal("UpdateSessionPinned(unpin) ok = false, want true")
}
if unpinned.PinnedAtUnixMS != 0 {
t.Fatalf("pinnedAtUnixMS after unpin = %d, want 0", unpinned.PinnedAtUnixMS)
}
if removed, err := store.DeleteSessionsBatch(ctx, agentactivitybiz.DeleteSessionsBatchInput{WorkspaceID: "ws-agent-pin", SessionIDs: []string{"session-1"}}); err != nil || removed.RemovedSessions != 1 {
t.Fatalf("DeleteSessionsBatch() result=%#v error=%v, want one removed session", removed, err)
}
if _, ok, err := store.UpdateSessionPinned(ctx, "ws-agent-pin", "session-1", true); err != nil || ok {
t.Fatalf("UpdateSessionPinned(deleted) ok=%v error=%v, want ok=false", ok, err)
}
}
func TestSQLiteStoreListAgentActivityMessagesForHeadlessSessionWithoutMessages(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-headless-agent-activity",
Name: "Workspace Headless Agent Activity",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-headless-agent-activity",
AgentSessionID: "headless-session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
ProviderSessionID: "provider-session-1",
RuntimeContext: map[string]any{"visible": false},
Cwd: "/workspace",
Title: "headless",
Status: "running",
OccurredAtUnixMS: 100,
StartedAtUnixMS: 90,
}); err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
page, ok, err := store.ListSessionMessages(ctx, agentactivitybiz.ListSessionMessagesInput{
WorkspaceID: "ws-headless-agent-activity",
AgentSessionID: "headless-session-1",
AfterVersion: 5,
Limit: 10,
})
if err != nil {
t.Fatalf("ListSessionMessages() error = %v", err)
}
if !ok {
t.Fatal("ListSessionMessages() ok = false, want true")
}
if page.AgentSessionID != "headless-session-1" || len(page.Messages) != 0 || page.LatestVersion != 5 || page.HasMore {
t.Fatalf("page = %#v, want empty page for headless session", page)
}
}
func TestSQLiteStoreDeleteAgentActivitySessionIgnoresLateReports(t *testing.T) {
t.Parallel()
store := openTestSQLiteStore(t)
ctx := context.Background()
if err := store.Create(ctx, workspacebiz.Summary{
ID: "ws-agent-delete-late",
Name: "Workspace Agent Delete Late Reports",
}); err != nil {
t.Fatalf("Create() error = %v", err)
}
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-delete-late",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Status: "running",
OccurredAtUnixMS: 100,
}); err != nil {
t.Fatalf("ReportSessionState() error = %v", err)
}
if removed, err := store.DeleteSessionsBatch(ctx, agentactivitybiz.DeleteSessionsBatchInput{WorkspaceID: "ws-agent-delete-late", SessionIDs: []string{"session-1"}}); err != nil || removed.RemovedSessions != 1 {
t.Fatalf("DeleteSessionsBatch() result=%#v error=%v, want one removed session", removed, err)
}
lateState, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: "ws-agent-delete-late",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Provider: "codex",
Status: "completed",
OccurredAtUnixMS: 200,
EndedAtUnixMS: 200,
})
if err != nil {
t.Fatalf("late ReportSessionState() error = %v", err)
}
if lateState.Accepted {
t.Fatal("late ReportSessionState() accepted = true, want false")
}
lateMessages, err := store.ReportSessionMessages(ctx, agentactivitybiz.SessionMessageReport{
WorkspaceID: "ws-agent-delete-late",
AgentSessionID: "session-1",
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
Messages: []agentactivitybiz.MessageUpdate{{
MessageID: "message-late",
TurnID: "turn-late",
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "late"},
}},
})
if err != nil {
t.Fatalf("late ReportSessionMessages() error = %v", err)
}
if lateMessages.AcceptedCount != 0 {
t.Fatalf("late ReportSessionMessages() accepted count = %d, want 0", lateMessages.AcceptedCount)
}
if _, ok, err := store.GetSession(ctx, "ws-agent-delete-late", "session-1"); err != nil || ok {
t.Fatalf("GetSession() after late reports ok=%v error=%v, want ok=false", ok, err)
}
}
func seedTestAgentTurn(t *testing.T, store *SQLiteStore, ctx context.Context, workspaceID, agentSessionID, turnID, provider string, occurredAtUnixMS int64) {
t.Helper()
if _, ok, err := store.GetSession(ctx, workspaceID, agentSessionID); err != nil {
t.Fatalf("GetSession(%s) before turn seed error = %v", agentSessionID, err)
} else if !ok {
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
WorkspaceID: workspaceID, AgentSessionID: agentSessionID,
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime, Provider: provider,
Status: "running", OccurredAtUnixMS: occurredAtUnixMS - 1,
}); err != nil {
t.Fatalf("ReportSessionState(%s) before turn seed error = %v", agentSessionID, err)
}
}
if _, accepted, err := store.agentStore().RecordTurnTransition(ctx, agentactivitybiz.TurnTransition{
WorkspaceID: workspaceID, AgentSessionID: agentSessionID, TurnID: turnID,
Phase: agentactivitybiz.TurnPhaseSubmitted, Origin: agentactivitybiz.TurnOriginLegacyUnknown,
OccurredAtUnixMS: occurredAtUnixMS,
}); err != nil || !accepted {
t.Fatalf("RecordTurnTransition(%s) accepted=%v error=%v", turnID, accepted, err)
}
}
func openTestSQLiteStore(t *testing.T) *SQLiteStore {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "tuttid.db")
store, err := OpenSQLiteStore(dbPath)
if err != nil {
t.Fatalf("OpenSQLiteStore() error = %v", err)
}
t.Cleanup(func() {
_ = store.Close()
})
if err := store.Migrate(context.Background()); err != nil {
t.Fatalf("Migrate() error = %v", err)
}
return store
}
type testAgentSessionRailSection struct {
Kind string
ProjectPath string
Key string
}
func getTestAgentSessionRailSection(
t *testing.T,
store *SQLiteStore,
workspaceID string,
agentSessionID string,
) testAgentSessionRailSection {
t.Helper()
row := store.writeDB.QueryRowContext(context.Background(), `
SELECT rail_section_kind, rail_project_path, rail_section_key
FROM workspace_agent_sessions
WHERE workspace_id = ? AND agent_session_id = ?
`, workspaceID, agentSessionID)
var rail testAgentSessionRailSection
if err := row.Scan(&rail.Kind, &rail.ProjectPath, &rail.Key); err != nil {
t.Fatalf("scan agent session rail section: %v", err)
}
return rail
}
func mkdirAll(path string) error {
return os.MkdirAll(path, 0o755)
}
func createLegacyAgentActivityV4MarkedDatabaseWithoutAgentTargetID(t *testing.T, dbPath string) {
t.Helper()
db, err := sql.Open("sqlite", dbPath)
if err != nil {
t.Fatalf("open legacy db: %v", err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE tuttid_schema_migrations (
id TEXT PRIMARY KEY,
applied_at_unix_ms INTEGER NOT NULL
);
INSERT INTO tuttid_schema_migrations (id, applied_at_unix_ms) VALUES
('workspaces_v1', 1),
('workspaces_v2', 1),
('workspaces_v3', 1),
('workspaces_v4', 1),
('workspace_agent_activity_v1', 1),
('workspace_agent_activity_v2', 1),
('workspace_agent_activity_v3', 1),
('workspace_agent_activity_v4', 1);
CREATE TABLE workspaces (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at_unix_ms INTEGER NOT NULL,
updated_at_unix_ms INTEGER NOT NULL,
last_opened_at_unix_ms INTEGER
);
INSERT INTO workspaces (id, name, created_at_unix_ms, updated_at_unix_ms, last_opened_at_unix_ms)
VALUES ('ws-legacy-agent-target-id', 'Legacy Agent Target ID', 1, 1, 1);
CREATE TABLE workspace_workbench_snapshots (
workspace_id TEXT PRIMARY KEY,
schema_version INTEGER NOT NULL,
snapshot_json TEXT NOT NULL,
created_at_unix_ms INTEGER NOT NULL,
updated_at_unix_ms INTEGER NOT NULL,
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
);
CREATE TABLE workspace_agent_sessions (
workspace_id TEXT NOT NULL,
agent_session_id TEXT NOT NULL,
origin TEXT NOT NULL DEFAULT '',
provider TEXT NOT NULL DEFAULT '',
provider_session_id TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL DEFAULT '',
settings_json TEXT NOT NULL DEFAULT '{}',
runtime_context_json TEXT NOT NULL DEFAULT '{}',
cwd TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT '',
current_phase TEXT NOT NULL DEFAULT '',
last_error TEXT NOT NULL DEFAULT '',
message_version INTEGER NOT NULL DEFAULT 0,
last_event_at_unix_ms INTEGER NOT NULL DEFAULT 0,
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
ended_at_unix_ms INTEGER NOT NULL DEFAULT 0,
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
created_at_unix_ms INTEGER NOT NULL,
updated_at_unix_ms INTEGER NOT NULL,
pinned_at_unix_ms INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (workspace_id, agent_session_id),
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
);
INSERT INTO workspace_agent_sessions (
workspace_id, agent_session_id, origin, provider, provider_session_id, model,
settings_json, runtime_context_json, cwd, title, status, current_phase,
last_error, message_version, last_event_at_unix_ms, started_at_unix_ms,
ended_at_unix_ms, deleted_at_unix_ms, created_at_unix_ms, updated_at_unix_ms,
pinned_at_unix_ms
) VALUES (
'ws-legacy-agent-target-id', 'session-legacy', 'runtime', 'codex', 'provider-session-legacy', '',
'{}', '{}', '/', 'Legacy Session', 'running', 'idle',
'', 0, 1, 1,
0, 0, 1, 1,
0
),
(
'ws-legacy-agent-target-id', 'session-legacy-claude', 'runtime', 'claude-code', 'provider-session-legacy-claude', '',
'{}', '{}', '/', 'Legacy Claude Session', 'running', 'idle',
'', 0, 1, 1,
0, 0, 1, 1,
0
);
CREATE TABLE workspace_agent_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workspace_id TEXT NOT NULL,
agent_session_id TEXT NOT NULL,
message_id TEXT NOT NULL,
version INTEGER NOT NULL,
turn_id TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT '',
payload_json TEXT NOT NULL DEFAULT '{}',
occurred_at_unix_ms INTEGER NOT NULL DEFAULT 0,
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
completed_at_unix_ms INTEGER NOT NULL DEFAULT 0,
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
created_at_unix_ms INTEGER NOT NULL,
updated_at_unix_ms INTEGER NOT NULL,
UNIQUE (workspace_id, agent_session_id, message_id),
FOREIGN KEY (workspace_id, agent_session_id)
REFERENCES workspace_agent_sessions(workspace_id, agent_session_id)
ON DELETE CASCADE
);
`)
if err != nil {
t.Fatalf("create legacy agent activity database: %v", err)
}
}