Files
tutti/packages/agent/store-sqlite/interaction_tree_read_test.go
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

267 lines
11 KiB
Go

package storesqlite
import (
"context"
"errors"
"strings"
"testing"
)
func TestSessionInteractionTreeSnapshotIncludesRootAndDescendantLatestTurns(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
ctx := context.Background()
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "root", Kind: SessionKindRoot,
Provider: "claude-code", OccurredAtUnixMS: 1,
}, "root-turn", 2)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "child", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn", ParentToolCallID: "call-child",
Provider: "claude-code", OccurredAtUnixMS: 3,
}, "child-old", 4)
seedTreeInteraction(t, store, "child", "child-old", "old-request", InteractionStatusPending, 5)
if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{
WorkspaceID: "ws-tree", AgentSessionID: "child", TurnID: "child-old",
Phase: TurnPhaseSettled, Outcome: TurnOutcomeCompleted, OccurredAtUnixMS: 6,
}); err != nil || !accepted {
t.Fatalf("settle child old turn: accepted=%v err=%v", accepted, err)
}
if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{
WorkspaceID: "ws-tree", AgentSessionID: "child", TurnID: "child-latest",
Phase: TurnPhaseRunning, OccurredAtUnixMS: 7,
}); err != nil || !accepted {
t.Fatalf("start child latest turn: accepted=%v err=%v", accepted, err)
}
seedTreeInteraction(t, store, "child", "child-latest", "child-pending", InteractionStatusPending, 8)
seedTreeInteraction(t, store, "root", "root-turn", "root-answered", InteractionStatusPending, 9)
seedTreeInteraction(t, store, "root", "root-turn", "root-answered", InteractionStatusAnswered, 10)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "nested", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "child", ParentTurnID: "child-latest", ParentToolCallID: "call-nested",
Provider: "claude-code", OccurredAtUnixMS: 11,
}, "nested-turn", 12)
seedTreeInteraction(t, store, "nested", "nested-turn", "nested-pending", InteractionStatusPending, 13)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "other-root", Kind: SessionKindRoot,
Provider: "claude-code", OccurredAtUnixMS: 14,
}, "other-turn", 15)
seedTreeInteraction(t, store, "other-root", "other-turn", "other-pending", InteractionStatusPending, 16)
snapshot, found, err := store.GetSessionInteractionTreeSnapshot(ctx, SessionInteractionTreeQuery{
WorkspaceID: " ws-tree ", RootAgentSessionID: " root ",
})
if err != nil || !found {
t.Fatalf("GetSessionInteractionTreeSnapshot() found=%v err=%v", found, err)
}
if snapshot.RootTurnID != "root-turn" {
t.Fatalf("RootTurnID=%q, want root-turn", snapshot.RootTurnID)
}
wantRequests := []string{"child-pending", "nested-pending", "root-answered"}
if len(snapshot.Interactions) != len(wantRequests) {
t.Fatalf("Interactions=%#v, want requests %v", snapshot.Interactions, wantRequests)
}
for index, want := range wantRequests {
if snapshot.Interactions[index].RequestID != want {
t.Fatalf("Interactions[%d]=%#v, want request %q", index, snapshot.Interactions[index], want)
}
}
if len(snapshot.PendingInteractions) != 2 ||
snapshot.PendingInteractions[0].RequestID != "child-pending" ||
snapshot.PendingInteractions[1].RequestID != "nested-pending" {
t.Fatalf("PendingInteractions=%#v", snapshot.PendingInteractions)
}
}
func TestSessionInteractionTreeSnapshotValidatesExplicitRootTurn(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "root", Kind: SessionKindRoot,
Provider: "claude-code", OccurredAtUnixMS: 1,
}, "root-turn", 2)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "other-root", Kind: SessionKindRoot,
Provider: "claude-code", OccurredAtUnixMS: 3,
}, "other-turn", 4)
for _, turnID := range []string{"missing", "other-turn"} {
_, _, err := store.GetSessionInteractionTreeSnapshot(context.Background(), SessionInteractionTreeQuery{
WorkspaceID: "ws-tree", RootAgentSessionID: "root", RootTurnID: turnID,
})
if !errors.Is(err, ErrInteractionTreeRootTurnNotFound) {
t.Fatalf("turn %q error=%v, want %v", turnID, err, ErrInteractionTreeRootTurnNotFound)
}
}
if _, err := store.db.Exec(`
UPDATE workspace_agent_turn_history
SET history_state = 'retracted', retracted_by_operation_id = 'operation-1',
updated_at_unix_ms = 5
WHERE workspace_id = 'ws-tree' AND agent_session_id = 'root' AND turn_id = 'root-turn'
`); err != nil {
t.Fatal(err)
}
_, _, err := store.GetSessionInteractionTreeSnapshot(context.Background(), SessionInteractionTreeQuery{
WorkspaceID: "ws-tree", RootAgentSessionID: "root", RootTurnID: "root-turn",
})
if !errors.Is(err, ErrInteractionTreeRootTurnNotFound) {
t.Fatalf("retracted turn error=%v, want %v", err, ErrInteractionTreeRootTurnNotFound)
}
}
func TestSessionInteractionTreeSnapshotPlanRanksTurnsBeforeJoiningInteractions(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
rows, err := store.db.Query(
"EXPLAIN QUERY PLAN "+sessionInteractionTreeSnapshotSQL,
"ws-tree", "root", "root-turn", "ws-tree", "root", "root-turn",
)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
var details []string
for rows.Next() {
var id, parent, unused int
var detail string
if err := rows.Scan(&id, &parent, &unused, &detail); err != nil {
t.Fatal(err)
}
details = append(details, detail)
}
plan := strings.Join(details, "\n")
if strings.Count(plan, "CORRELATED SCALAR SUBQUERY") != 1 ||
strings.Index(plan, "SCAN ranked_descendant_turns") > strings.Index(plan, "SEARCH interaction") {
t.Fatalf("interaction tree plan does not rank effective turns before joining interactions:\n%s", plan)
}
}
func TestInteractionTreeDirtyFactCoversLatestTurnSettleRetractAndDelete(t *testing.T) {
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
ctx := context.Background()
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "root", Kind: SessionKindRoot,
Provider: "claude-code", OccurredAtUnixMS: 1,
}, "root-turn", 2)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "child", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn", ParentToolCallID: "call-child",
Provider: "claude-code", OccurredAtUnixMS: 3,
}, "child-old", 4)
seedTreeInteraction(t, store, "child", "child-old", "old-request", InteractionStatusPending, 5)
if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{
WorkspaceID: "ws-tree", AgentSessionID: "child", TurnID: "child-old",
Phase: TurnPhaseSettled, Outcome: TurnOutcomeCompleted, OccurredAtUnixMS: 6,
}); err != nil || !accepted {
t.Fatalf("settle old child turn: accepted=%v err=%v", accepted, err)
}
assertLatestInteractionTreeDirty(t, participant, "root", "root-turn")
if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{
WorkspaceID: "ws-tree", AgentSessionID: "child", TurnID: "child-latest",
Phase: TurnPhaseRunning, OccurredAtUnixMS: 7,
}); err != nil || !accepted {
t.Fatalf("start latest child turn: accepted=%v err=%v", accepted, err)
}
assertLatestInteractionTreeDirty(t, participant, "root", "root-turn")
seedTreeInteraction(t, store, "child", "child-latest", "latest-request", InteractionStatusPending, 8)
tx, err := store.db.BeginTx(ctx, nil)
if err != nil {
t.Fatal(err)
}
if _, err := tx.ExecContext(ctx, `
UPDATE workspace_agent_turn_history
SET history_state = 'retracted', retracted_by_operation_id = 'operation-1', updated_at_unix_ms = 9
WHERE workspace_id = 'ws-tree' AND agent_session_id = 'child' AND turn_id = 'child-latest'
`); err != nil {
_ = tx.Rollback()
t.Fatal(err)
}
if _, err := store.commitTransaction(ctx, tx, "ws-tree", []TransactionMutation{
transactionMutation("ws-tree", "child", MutationEntityTurn, "child-latest", "retract", 9),
}); err != nil {
t.Fatal(err)
}
assertLatestInteractionTreeDirty(t, participant, "root", "root-turn")
snapshot, found, err := store.GetSessionInteractionTreeSnapshot(ctx, SessionInteractionTreeQuery{
WorkspaceID: "ws-tree", RootAgentSessionID: "root", RootTurnID: "root-turn",
})
if err != nil || !found || len(snapshot.Interactions) != 1 ||
snapshot.Interactions[0].RequestID != "old-request" {
t.Fatalf("snapshot after child retract = %#v found=%v err=%v", snapshot, found, err)
}
if _, err := store.DeleteSession(ctx, "ws-tree", "child"); err != nil {
t.Fatal(err)
}
assertLatestInteractionTreeDirty(t, participant, "root", "root-turn")
snapshot, found, err = store.GetSessionInteractionTreeSnapshot(ctx, SessionInteractionTreeQuery{
WorkspaceID: "ws-tree", RootAgentSessionID: "root", RootTurnID: "root-turn",
})
if err != nil || !found || len(snapshot.Interactions) != 0 {
t.Fatalf("snapshot after child delete = %#v found=%v err=%v", snapshot, found, err)
}
}
func assertLatestInteractionTreeDirty(
t *testing.T,
participant *testTransactionParticipant,
rootAgentSessionID string,
rootTurnID string,
) {
t.Helper()
if len(participant.deltas) == 0 {
t.Fatal("interaction tree mutation did not participate")
}
for _, mutation := range participant.deltas[len(participant.deltas)-1].Mutations {
if mutation.EntityKind == MutationEntityInteractionTree &&
mutation.RootAgentSessionID == rootAgentSessionID && mutation.RootTurnID == rootTurnID {
return
}
}
t.Fatalf("latest delta lacks interaction tree dirty fact: %#v", participant.deltas[len(participant.deltas)-1])
}
func TestSessionInteractionTreeSnapshotRejectsChildRoot(t *testing.T) {
t.Parallel()
store := openTestStore(t, testOptions(&staticProjectPaths{}))
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "root", Kind: SessionKindRoot,
Provider: "claude-code", OccurredAtUnixMS: 1,
}, "root-turn", 2)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-tree", AgentSessionID: "child", Kind: SessionKindChild,
RootAgentSessionID: "root", RootTurnID: "root-turn",
ParentAgentSessionID: "root", ParentTurnID: "root-turn", ParentToolCallID: "call-child",
Provider: "claude-code", OccurredAtUnixMS: 3,
}, "child-turn", 4)
_, _, err := store.GetSessionInteractionTreeSnapshot(context.Background(), SessionInteractionTreeQuery{
WorkspaceID: "ws-tree", RootAgentSessionID: "child",
})
if !errors.Is(err, ErrInteractionTreeRootRequired) {
t.Fatalf("error=%v, want %v", err, ErrInteractionTreeRootRequired)
}
}
func seedTreeInteraction(t *testing.T, store *Store, sessionID, turnID, requestID, status string, occurredAt int64) {
t.Helper()
_, result, err := store.UpsertInteraction(context.Background(), InteractionUpsert{
WorkspaceID: "ws-tree", AgentSessionID: sessionID, TurnID: turnID,
RequestID: requestID, Kind: InteractionKindQuestion, Status: status,
OccurredAtUnixMS: occurredAt,
})
if err != nil || result != InteractionTransitionApplied {
t.Fatalf("seed interaction %s: result=%v err=%v", requestID, result, err)
}
}