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
267 lines
11 KiB
Go
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)
|
|
}
|
|
}
|