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

379 lines
14 KiB
Go

package storesqlite
import (
"context"
"errors"
"strings"
"testing"
)
func TestChildSessionsKeepImmutableRootAndParentRelations(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
ctx := context.Background()
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "root", Kind: SessionKindRoot,
Provider: "codex", OccurredAtUnixMS: 10,
}, "root-turn", 10)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "child-1", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn", ParentToolCallID: "call-1",
Provider: "codex", OccurredAtUnixMS: 20,
}, "child-turn-1", 20)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "child-2", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "child-1", ParentTurnID: "child-turn-1", ParentToolCallID: "call-2",
Provider: "codex", OccurredAtUnixMS: 30,
}, "child-turn-2", 30)
roots, ok, err := store.ListSessions(ctx, "ws-1")
if err != nil || !ok || len(roots) != 1 || roots[0].ID != "root" {
t.Fatalf("ListSessions() = %#v ok=%v err=%v", roots, ok, err)
}
children, err := store.ListChildSessions(ctx, "ws-1", "root")
if err != nil {
t.Fatal(err)
}
if len(children) != 2 || children[0].ID != "child-1" || children[1].ID != "child-2" {
t.Fatalf("ListChildSessions() = %#v", children)
}
if children[1].ParentAgentSessionID != "child-1" || children[1].RootAgentSessionID != "root" {
t.Fatalf("nested child relation = %#v", children[1])
}
_, err = store.ReportSessionState(ctx, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "child-1", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn", ParentToolCallID: "different-call",
OccurredAtUnixMS: 40,
})
if err == nil || !strings.Contains(err.Error(), "parent tool call id is immutable") {
t.Fatalf("changed creator relation error = %v", err)
}
}
func TestChildSessionReportReturnsCanonicalRelations(t *testing.T) {
store := openTestStore(t, testOptions(&staticProjectPaths{}))
ctx := context.Background()
if _, err := store.ReportSessionState(ctx, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "root",
Kind: SessionKindRoot, Provider: "codex",
OccurredAtUnixMS: 1,
}); err != nil {
t.Fatal(err)
}
if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{
WorkspaceID: "ws-1", AgentSessionID: "root",
TurnID: "root-turn", Phase: TurnPhaseRunning,
OccurredAtUnixMS: 2,
}); err != nil || !accepted {
t.Fatalf("root turn accepted=%v error=%v", accepted, err)
}
for index, childID := range []string{"child-b", "child-a"} {
result, err := store.ReportSessionState(ctx, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: childID,
Kind: SessionKindChild, Provider: "codex",
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn",
ParentToolCallID: "call-" + childID,
OccurredAtUnixMS: int64(3 + index),
CreatedAtUnixMS: 10,
})
if err != nil {
t.Fatal(err)
}
if result.Session.ID != childID ||
result.Session.ParentAgentSessionID != "root" {
t.Fatalf("%s canonical Session=%#v", childID, result.Session)
}
}
refreshed, err := store.ReportSessionState(ctx, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "child-b",
Kind: SessionKindChild, Provider: "codex",
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn",
ParentToolCallID: "call-child-b",
OccurredAtUnixMS: 6,
})
if err != nil || refreshed.Session.ID != "child-b" {
t.Fatalf("refreshed child-b=%#v error=%v", refreshed.Session, err)
}
if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{
WorkspaceID: "ws-1", AgentSessionID: "child-b",
TurnID: "child-turn", Phase: TurnPhaseRunning,
OccurredAtUnixMS: 7,
}); err != nil || !accepted {
t.Fatalf("child turn accepted=%v error=%v", accepted, err)
}
for index, nestedID := range []string{"nested-z", "nested-a"} {
result, err := store.ReportSessionState(ctx, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: nestedID,
Kind: SessionKindChild, Provider: "codex",
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "child-b", ParentTurnID: "child-turn",
ParentToolCallID: "call-" + nestedID,
OccurredAtUnixMS: int64(8 + index),
CreatedAtUnixMS: 20,
})
if err != nil {
t.Fatal(err)
}
if result.Session.ID != nestedID ||
result.Session.ParentAgentSessionID != "child-b" {
t.Fatalf("%s canonical Session=%#v", nestedID, result.Session)
}
}
graph, err := store.CaptureHistoricalSessionGraph(ctx, "ws-1", "root")
if err != nil {
t.Fatal(err)
}
if len(graph.Sessions) != 5 {
t.Fatalf("captured Sessions=%#v", graph.Sessions)
}
if err := store.RestoreHistoricalSessionGraph(ctx, HistoricalSessionGraphRestoreInput{
WorkspaceID: "ws-restore",
UserID: "user-restore",
Graph: graph,
}); err != nil {
t.Fatal(err)
}
restored, err := store.ListChildSessions(ctx, "ws-restore", "root")
if err != nil {
t.Fatal(err)
}
if len(restored) != 4 ||
restored[0].ID != "child-a" ||
restored[1].ID != "child-b" ||
restored[2].ID != "nested-a" ||
restored[3].ID != "nested-z" {
t.Fatalf("restored children=%#v", restored)
}
}
func TestChildSessionReportKeepsCreatorRelationImmutable(t *testing.T) {
store := openTestStore(t, testOptions(&staticProjectPaths{}))
ctx := context.Background()
if _, err := store.ReportSessionState(ctx, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "root",
Kind: SessionKindRoot, Provider: "codex", OccurredAtUnixMS: 1,
}); err != nil {
t.Fatal(err)
}
if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{
WorkspaceID: "ws-1", AgentSessionID: "root",
TurnID: "root-turn", Phase: TurnPhaseRunning, OccurredAtUnixMS: 2,
}); err != nil || !accepted {
t.Fatalf("root turn accepted=%v error=%v", accepted, err)
}
report := SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "child",
Kind: SessionKindChild, Provider: "codex",
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn",
ParentToolCallID: "call-child", OccurredAtUnixMS: 3,
}
if _, err := store.ReportSessionState(ctx, report); err != nil {
t.Fatal(err)
}
report.OccurredAtUnixMS = 4
report.ParentToolCallID = "different-call"
if _, err := store.ReportSessionState(ctx, report); err == nil ||
!strings.Contains(err.Error(), "parent tool call id is immutable") {
t.Fatalf("changed creator relation error=%v", err)
}
}
func TestChildSessionRequiresLiveRootTurnAndExistingParentTurn(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
ctx := context.Background()
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "root", Provider: "claude-code", OccurredAtUnixMS: 10,
}, "root-turn", 10)
_, err := store.ReportSessionState(ctx, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "child", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "missing-turn", ParentToolCallID: "call-1",
OccurredAtUnixMS: 20,
})
if err == nil || !strings.Contains(err.Error(), "root parent must use the root session and turn") {
t.Fatalf("missing parent turn error = %v", err)
}
if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{
WorkspaceID: "ws-1", AgentSessionID: "root", TurnID: "root-turn",
Phase: TurnPhaseSettled, Outcome: TurnOutcomeCompleted, OccurredAtUnixMS: 30,
}); err != nil || !accepted {
t.Fatalf("settle root turn accepted=%v err=%v", accepted, err)
}
_, err = store.ReportSessionState(ctx, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "late-child", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn", ParentToolCallID: "call-late",
OccurredAtUnixMS: 40,
})
if err == nil || !strings.Contains(err.Error(), "after its root turn settled") {
t.Fatalf("late child error = %v", err)
}
}
func TestDeleteSessionTombstonesEntireChildSessionTree(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
seedChildSessionTree(t, store)
removed, err := store.DeleteSession(context.Background(), "ws-1", "root")
if err != nil || !removed {
t.Fatalf("DeleteSession() removed=%v err=%v", removed, err)
}
for _, sessionID := range []string{"root", "child-1", "child-2"} {
deleted, err := store.SessionDeleted(context.Background(), "ws-1", sessionID)
if err != nil || !deleted {
t.Fatalf("SessionDeleted(%s)=%v err=%v", sessionID, deleted, err)
}
}
for sessionID, turnID := range map[string]string{
"root": "root-turn", "child-1": "child-turn-1", "child-2": "child-turn-2",
} {
if turn, found, err := store.GetTurn(context.Background(), "ws-1", sessionID, turnID); err != nil || !found || turn.Phase != TurnPhaseSettled || turn.Outcome != TurnOutcomeInterrupted {
t.Fatalf("GetTurn(%s)=%#v found=%v err=%v, want preserved interrupted history", sessionID, turn, found, err)
}
}
}
func TestDeleteSessionsBatchExpandsChildSessionTree(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
seedChildSessionTree(t, store)
result, err := store.DeleteSessionsBatch(context.Background(), DeleteSessionsBatchInput{
WorkspaceID: "ws-1",
SessionIDs: []string{"root"},
})
if err != nil {
t.Fatal(err)
}
if result.RemovedSessions != 3 || len(result.RemovedSessionIDs) != 3 {
t.Fatalf("DeleteSessionsBatch()=%#v", result)
}
for _, sessionID := range []string{"root", "child-1", "child-2"} {
if !containsString(result.RemovedSessionIDs, sessionID) {
t.Fatalf("removed session ids=%#v, want %s", result.RemovedSessionIDs, sessionID)
}
}
}
func TestDeleteSessionsBatchRejectsChangedDeletionPlan(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
seedChildSessionTree(t, store)
_, err := store.DeleteSessionsBatch(context.Background(), DeleteSessionsBatchInput{
WorkspaceID: "ws-1",
SessionIDs: []string{"root"},
ExpectedSessionIDs: []string{"root"},
})
if !errors.Is(err, ErrDeleteSessionsPlanChanged) {
t.Fatalf("DeleteSessionsBatch() error = %v, want plan changed", err)
}
for _, sessionID := range []string{"root", "child-1", "child-2"} {
if deleted, lookupErr := store.SessionDeleted(context.Background(), "ws-1", sessionID); lookupErr != nil || deleted {
t.Fatalf("SessionDeleted(%s)=%v err=%v after rejected plan", sessionID, deleted, lookupErr)
}
}
}
func TestDeleteSessionsBatchPreservesRootPinnedAfterConditionalPlan(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
seedChildSessionTree(t, store)
ctx := context.Background()
sectionKey := RailSectionKeyForProject("/workspace/project")
if _, err := store.db.ExecContext(ctx, `
UPDATE workspace_agent_sessions
SET rail_section_key = ?
WHERE workspace_id = 'ws-1' AND agent_session_id = 'root'
`, sectionKey); err != nil {
t.Fatal(err)
}
input := DeleteSessionsBatchInput{
WorkspaceID: "ws-1",
SessionIDs: []string{"root"},
RequiredRootRailSectionKey: sectionKey,
ExcludePinnedRoots: true,
}
plan, err := store.PlanDeleteSessions(ctx, input)
if err != nil || len(plan.SessionIDs) != 3 {
t.Fatalf("PlanDeleteSessions() = %#v, error = %v", plan, err)
}
if _, err := store.db.ExecContext(ctx, `
UPDATE workspace_agent_sessions
SET pinned_at_unix_ms = 100
WHERE workspace_id = 'ws-1' AND agent_session_id = 'root'
`); err != nil {
t.Fatal(err)
}
input.ExpectedSessionIDs = plan.SessionIDs
if _, err := store.DeleteSessionsBatch(ctx, input); !errors.Is(err, ErrDeleteSessionsPlanChanged) {
t.Fatalf("DeleteSessionsBatch() error = %v, want plan changed", err)
}
for _, sessionID := range []string{"root", "child-1", "child-2"} {
if deleted, lookupErr := store.SessionDeleted(ctx, "ws-1", sessionID); lookupErr != nil || deleted {
t.Fatalf("SessionDeleted(%s)=%v err=%v after root was pinned", sessionID, deleted, lookupErr)
}
}
}
func seedChildSessionTree(t *testing.T, store *Store) {
t.Helper()
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "root", Kind: SessionKindRoot,
Provider: "codex", OccurredAtUnixMS: 10,
}, "root-turn", 10)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "child-1", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn", ParentToolCallID: "call-1",
Provider: "codex", OccurredAtUnixMS: 20,
}, "child-turn-1", 20)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "child-2", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "child-1", ParentTurnID: "child-turn-1", ParentToolCallID: "call-2",
Provider: "codex", OccurredAtUnixMS: 30,
}, "child-turn-2", 30)
}
func containsString(values []string, expected string) bool {
for _, value := range values {
if value == expected {
return true
}
}
return false
}
func reportSessionWithTurn(
t *testing.T,
store *Store,
session SessionStateReport,
turnID string,
occurredAtUnixMS int64,
) {
t.Helper()
result, err := store.ReportActivityState(context.Background(), ActivityStateReport{
Session: session,
Turn: &TurnTransition{
WorkspaceID: session.WorkspaceID, AgentSessionID: session.AgentSessionID,
TurnID: turnID, Phase: TurnPhaseRunning, OccurredAtUnixMS: occurredAtUnixMS,
},
})
if err != nil || !result.TurnAccepted {
t.Fatalf("ReportActivityState(%s) accepted=%v err=%v", session.AgentSessionID, result.TurnAccepted, err)
}
}