Files
tutti/packages/agent/store-sqlite/transaction_participant_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

452 lines
19 KiB
Go

package storesqlite
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"testing"
)
type testTransactionParticipant struct {
fail bool
deltas []TransactionDelta
}
func (p *testTransactionParticipant) Participate(ctx context.Context, writer TransactionWriter, delta TransactionDelta) error {
p.deltas = append(p.deltas, delta)
if _, err := writer.ExecContext(ctx, `INSERT INTO test_transaction_markers (transaction_id, mutation_count) VALUES (?, ?)`, delta.TransactionID, len(delta.Mutations)); err != nil {
return err
}
if p.fail {
return errors.New("participant failure")
}
return nil
}
func openParticipantTestStore(t *testing.T, participant *testTransactionParticipant) *Store {
t.Helper()
opts := testOptions(&staticProjectPaths{})
opts.TransactionParticipant = participant
store := openTestStore(t, opts)
if _, err := store.db.Exec(`CREATE TABLE test_transaction_markers (transaction_id TEXT PRIMARY KEY, mutation_count INTEGER NOT NULL)`); err != nil {
t.Fatalf("create participant marker table: %v", err)
}
return store
}
func TestTransactionParticipantCommitsWithSessionTurnAndInteraction(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
result, err := store.ReportActivityState(context.Background(), ActivityStateReport{
Session: SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "runtime",
Provider: "codex", OccurredAtUnixMS: 100,
},
Turn: &TurnTransition{
WorkspaceID: "ws-1", AgentSessionID: "session-1", TurnID: "turn-1",
Phase: TurnPhaseWaiting, Origin: TurnOriginProviderInitiated, OccurredAtUnixMS: 100,
},
Interaction: &InteractionUpsert{
WorkspaceID: "ws-1", AgentSessionID: "session-1", RequestID: "request-1",
TurnID: "turn-1", Kind: InteractionKindQuestion, Status: InteractionStatusPending,
OccurredAtUnixMS: 100,
},
})
if err != nil {
t.Fatal(err)
}
if result.TransactionID == "" || len(result.CommitDelta.Mutations) != 4 {
t.Fatalf("commit delta = %#v", result.CommitDelta)
}
assertParticipantMutationKinds(t, result.CommitDelta,
MutationEntitySession, MutationEntityTurn, MutationEntityInteraction, MutationEntityInteractionTree)
assertParticipantMutationEntityID(t, result.CommitDelta, MutationEntityInteraction, "turn-1\x00request-1")
for _, mutation := range result.CommitDelta.Mutations {
if mutation.EntityKind == MutationEntityInteractionTree &&
(mutation.RootAgentSessionID != "session-1" || mutation.RootTurnID != "turn-1") {
t.Fatalf("interaction tree mutation identity = %#v", mutation)
}
}
encodedSession, err := json.Marshal(result.State.Session)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(encodedSession), "CommitTransactionID") || strings.Contains(string(encodedSession), "CommitDelta") || strings.Contains(string(encodedSession), result.TransactionID) {
t.Fatalf("transient commit metadata leaked into session JSON: %s", encodedSession)
}
var mutationCount int
if err := store.db.QueryRow(`SELECT mutation_count FROM test_transaction_markers WHERE transaction_id = ?`, result.TransactionID).Scan(&mutationCount); err != nil || mutationCount != 4 {
t.Fatalf("participant marker mutation_count=%d error=%v", mutationCount, err)
}
}
func TestTransactionParticipantAddsChildInteractionRootIdentity(t *testing.T) {
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-child-root", AgentSessionID: "root", Kind: SessionKindRoot,
Provider: "claude-code", OccurredAtUnixMS: 1,
}, "root-turn", 2)
reportSessionWithTurn(t, store, SessionStateReport{
WorkspaceID: "ws-child-root", 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)
_, result, err := store.UpsertInteraction(context.Background(), InteractionUpsert{
WorkspaceID: "ws-child-root", AgentSessionID: "child", TurnID: "child-turn",
RequestID: "request-child", Kind: InteractionKindQuestion, Status: InteractionStatusPending,
OccurredAtUnixMS: 5,
})
if err != nil || result != InteractionTransitionApplied {
t.Fatalf("UpsertInteraction() result=%v err=%v", result, err)
}
got := participant.deltas[len(participant.deltas)-1].Mutations
if len(got) != 2 || got[1].EntityKind != MutationEntityInteractionTree ||
got[1].RootAgentSessionID != "root" || got[1].RootTurnID != "root-turn" {
t.Fatalf("child interaction mutation = %#v", got)
}
}
func TestRuntimeGoalProjectionParticipatesOnlyOnSemanticChange(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
ctx := context.Background()
reportGoal := func(occurredAt int64, status string) TransactionDelta {
t.Helper()
result, err := store.ReportActivityState(ctx, ActivityStateReport{Session: SessionStateReport{
WorkspaceID: "ws-goal", AgentSessionID: "session-goal", Origin: "runtime",
Provider: "codex", OccurredAtUnixMS: occurredAt,
RuntimeContext: map[string]any{
"goal": map[string]any{"objective": "ship it", "status": status},
},
}})
if err != nil {
t.Fatalf("report Goal %q: %v", status, err)
}
return result.CommitDelta
}
active := reportGoal(100, "active")
assertParticipantMutationKinds(t, active, MutationEntitySession, MutationEntityGoalState)
completed := reportGoal(200, "complete")
assertParticipantMutationKinds(t, completed, MutationEntitySession, MutationEntityGoalState)
replayed := reportGoal(300, "complete")
assertParticipantMutationKinds(t, replayed, MutationEntitySession)
stale := reportGoal(250, "active")
assertParticipantMutationKinds(t, stale, MutationEntitySession)
metadataOnly, err := store.ReportActivityState(ctx, ActivityStateReport{Session: SessionStateReport{
WorkspaceID: "ws-goal", AgentSessionID: "session-without-goal", Origin: "runtime",
Provider: "codex", OccurredAtUnixMS: 100,
RuntimeContext: map[string]any{"providerResumeCheckpoint": "checkpoint"},
}})
if err != nil {
t.Fatalf("report metadata-only Session: %v", err)
}
assertParticipantMutationKinds(t, metadataOnly.CommitDelta, MutationEntitySession)
}
func TestTransactionParticipantFailureRollsBackCanonicalAndMarkerFacts(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{fail: true}
store := openParticipantTestStore(t, participant)
_, err := store.ReportActivityState(context.Background(), ActivityStateReport{
Session: SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "runtime",
Provider: "codex", OccurredAtUnixMS: 100,
},
Turn: &TurnTransition{
WorkspaceID: "ws-1", AgentSessionID: "session-1", TurnID: "turn-1",
Phase: TurnPhaseWaiting, Origin: TurnOriginProviderInitiated, OccurredAtUnixMS: 100,
},
Interaction: &InteractionUpsert{
WorkspaceID: "ws-1", AgentSessionID: "session-1", RequestID: "request-1",
TurnID: "turn-1", Kind: InteractionKindQuestion, Status: InteractionStatusPending,
OccurredAtUnixMS: 100,
},
})
if err == nil || !strings.Contains(err.Error(), "participant failure") {
t.Fatalf("ReportActivityState() error = %v", err)
}
if _, found, getErr := store.GetSession(context.Background(), "ws-1", "session-1"); getErr != nil || found {
t.Fatalf("session leaked after rollback found=%v error=%v", found, getErr)
}
var markerCount int
if err := store.db.QueryRow(`SELECT COUNT(*) FROM test_transaction_markers`).Scan(&markerCount); err != nil || markerCount != 0 {
t.Fatalf("participant markers after rollback=%d error=%v", markerCount, err)
}
if len(participant.deltas) != 1 {
t.Fatalf("participant deltas=%d", len(participant.deltas))
}
}
func TestTransactionParticipantReceivesMessageVersions(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
seedTurnTestSession(t, store, "ws-1", "session-1")
if _, accepted, err := store.RecordTurnTransition(context.Background(), TurnTransition{
WorkspaceID: "ws-1", AgentSessionID: "session-1", TurnID: "turn-1",
Phase: TurnPhaseRunning, OccurredAtUnixMS: 1,
}); err != nil || !accepted {
t.Fatalf("seed turn accepted=%v error=%v", accepted, err)
}
for index, report := range []SessionMessageReport{
{
WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "runtime",
Messages: []MessageUpdate{{MessageID: "message-1", TurnID: "turn-1", Role: "assistant", Kind: "text", Status: "running", OccurredAtUnixMS: 2}},
},
{
WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "runtime",
Messages: []MessageUpdate{{MessageID: "message-1", Status: "completed", ContentDelta: "done", CompletedAtUnixMS: 3}},
},
} {
result, err := store.ReportSessionMessages(context.Background(), report)
if err != nil {
t.Fatalf("message report %d: %v", index+1, err)
}
if len(result.CommitDelta.Mutations) != 1 || result.CommitDelta.Mutations[0].Version != int64(index+1) {
t.Fatalf("message report %d delta=%#v", index+1, result.CommitDelta)
}
}
}
func TestRuntimeOperationCompletionParticipatesWithCanonicalAndOutboxFacts(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
seedRuntimeInteractiveSubject(t, store, "session-1", "turn-1", "request-1")
prepareRuntimeInteractive(t, store, "operation-1", "session-1", "turn-1", "request-1")
claimRuntimeOperation(t, store, "operation-1", "worker-a")
completion, changed, err := store.CompleteInteractiveRuntimeOperation(context.Background(), CompleteInteractiveRuntimeOperationInput{
WorkspaceID: "ws-1", OperationID: "operation-1", LeaseOwner: "worker-a",
Disposition: InteractionStatusAnswered, NowUnixMS: 30,
})
if err != nil || !changed {
t.Fatalf("completion changed=%v error=%v", changed, err)
}
if completion.TransactionID == "" || completion.Event.ID == 0 {
t.Fatalf("completion=%#v", completion)
}
assertParticipantMutationKinds(t, completion.CommitDelta,
MutationEntityRuntimeOperation, MutationEntityRuntimeEvent,
MutationEntityInteraction, MutationEntityInteractionTree)
assertParticipantMutationEntityID(t, completion.CommitDelta, MutationEntityInteraction, "turn-1\x00request-1")
}
func TestRuntimeOperationCompletionRollsBackWhenParticipantFails(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
seedRuntimeInteractiveSubject(t, store, "session-1", "turn-1", "request-1")
prepareRuntimeInteractive(t, store, "operation-1", "session-1", "turn-1", "request-1")
claimRuntimeOperation(t, store, "operation-1", "worker-a")
var markerCountBefore int
if err := store.db.QueryRow(`SELECT COUNT(*) FROM test_transaction_markers`).Scan(&markerCountBefore); err != nil {
t.Fatal(err)
}
participant.fail = true
_, _, err := store.CompleteInteractiveRuntimeOperation(context.Background(), CompleteInteractiveRuntimeOperationInput{
WorkspaceID: "ws-1", OperationID: "operation-1", LeaseOwner: "worker-a",
Disposition: InteractionStatusAnswered, NowUnixMS: 30,
})
if err == nil || !strings.Contains(err.Error(), "participant failure") {
t.Fatalf("completion error=%v", err)
}
interactions, listErr := store.ListSessionInteractions(context.Background(), ListSessionInteractionsInput{
WorkspaceID: "ws-1", AgentSessionID: "session-1",
})
operation, found, getErr := store.GetRuntimeOperation(context.Background(), "ws-1", "operation-1")
if listErr != nil || len(interactions) != 1 || interactions[0].Status != InteractionStatusPending {
t.Fatalf("interactions after rollback=%#v error=%v", interactions, listErr)
}
if getErr != nil || !found || operation.Status != RuntimeOperationStatusLeased {
t.Fatalf("operation after rollback=%#v found=%v error=%v", operation, found, getErr)
}
var markerCountAfter int
if err := store.db.QueryRow(`SELECT COUNT(*) FROM test_transaction_markers`).Scan(&markerCountAfter); err != nil || markerCountAfter != markerCountBefore {
t.Fatalf("marker count before=%d after=%d error=%v", markerCountBefore, markerCountAfter, err)
}
}
func TestGoalPrepareParticipatesWithIntentStateAndAudit(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
if _, err := store.ReportSessionState(context.Background(), SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "session-1", Provider: "codex", OccurredAtUnixMS: 1,
}); err != nil {
t.Fatal(err)
}
operation, state, created, err := store.PrepareGoalControlOperation(context.Background(), GoalControlOperationPrepare{
OperationID: "goal-op-1", WorkspaceID: "ws-1", AgentSessionID: "session-1",
Action: "set", Objective: "ship it", ClientSubmitID: "submit-1", OccurredAtUnixMS: 2,
})
if err != nil || !created {
t.Fatalf("goal prepare created=%v error=%v", created, err)
}
if operation.CommitTransactionID == "" || state.CommitTransactionID != operation.CommitTransactionID {
t.Fatalf("operation=%#v state=%#v", operation, state)
}
assertParticipantMutationKinds(t, operation.CommitDelta,
MutationEntityGoalOperation, MutationEntityGoalState, MutationEntityMessage)
for _, mutation := range operation.CommitDelta.Mutations {
if mutation.EntityKind == MutationEntityMessage && (mutation.EntityID != "goal-control:goal-op-1" || mutation.Version != 1) {
t.Fatalf("goal audit mutation=%#v", mutation)
}
}
}
func TestStaleSettlementRollsBackWhenParticipantFails(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
seedTurnTestSession(t, store, "ws-1", "session-1")
if _, accepted, err := store.RecordTurnTransition(context.Background(), TurnTransition{
WorkspaceID: "ws-1", AgentSessionID: "session-1", TurnID: "turn-1",
Phase: TurnPhaseRunning, OccurredAtUnixMS: 10,
}); err != nil || !accepted {
t.Fatalf("seed stale turn accepted=%v error=%v", accepted, err)
}
participant.fail = true
if _, err := store.SettleStaleTurns(context.Background()); err == nil || !strings.Contains(err.Error(), "participant failure") {
t.Fatalf("SettleStaleTurns() error=%v", err)
}
turn, found, err := store.GetTurn(context.Background(), "ws-1", "session-1", "turn-1")
if err != nil || !found || turn.Phase != TurnPhaseRunning {
t.Fatalf("turn after rollback=%#v found=%v error=%v", turn, found, err)
}
session, found, err := store.GetSession(context.Background(), "ws-1", "session-1")
if err != nil || !found || session.ActiveTurnID != "turn-1" {
t.Fatalf("session after rollback=%#v found=%v error=%v", session, found, err)
}
}
func TestSessionTitleUpdateRollsBackWhenParticipantFails(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
if _, err := store.ReportSessionState(context.Background(), SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "session-1", Provider: "codex",
Title: "before", OccurredAtUnixMS: 1,
}); err != nil {
t.Fatal(err)
}
participant.fail = true
if _, _, err := store.UpdateSessionTitle(context.Background(), "ws-1", "session-1", "after"); err == nil {
t.Fatal("UpdateSessionTitle() error=nil, want participant failure")
}
session, found, err := store.GetSession(context.Background(), "ws-1", "session-1")
if err != nil || !found || session.Title != "before" {
t.Fatalf("session after rollback=%#v found=%v error=%v", session, found, err)
}
}
func TestSessionDeleteCommitsDurableMarkerWithTombstone(t *testing.T) {
t.Parallel()
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
if _, err := store.ReportSessionState(context.Background(), SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME",
Provider: "codex", OccurredAtUnixMS: 1,
}); err != nil {
t.Fatal(err)
}
result, err := store.DeleteSessionWithCommit(context.Background(), "ws-1", "session-1")
if err != nil {
t.Fatal(err)
}
if result.RemovedSessions != 1 || result.TransactionID == "" {
t.Fatalf("delete result=%#v", result)
}
assertParticipantMutationEntityID(t, result.CommitDelta, MutationEntitySession, "session-1")
if len(result.CommitDelta.Mutations) != 2 || result.CommitDelta.Mutations[0].Operation != "delete" ||
result.CommitDelta.Mutations[1].EntityKind != MutationEntityInteractionTree ||
result.CommitDelta.Mutations[1].RootAgentSessionID != "session-1" ||
result.CommitDelta.Mutations[1].RootTurnID != "" {
t.Fatalf("delete delta=%#v", result.CommitDelta)
}
}
func TestSessionDeleteAndInitializationRollbackPreserveCanonicalFactWhenParticipantFails(t *testing.T) {
t.Parallel()
for _, testCase := range []struct {
name string
run func(*Store) error
}{
{name: "user deletion", run: func(store *Store) error {
_, err := store.DeleteSessionWithCommit(context.Background(), "ws-1", "session-1")
return err
}},
{name: "initialization compensation", run: func(store *Store) error {
_, err := store.RollbackRuntimeSessionInitialization(context.Background(), "ws-1", "session-1")
return err
}},
} {
t.Run(testCase.name, func(t *testing.T) {
participant := &testTransactionParticipant{}
store := openParticipantTestStore(t, participant)
if _, err := store.ReportSessionState(context.Background(), SessionStateReport{
WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME",
Provider: "codex", OccurredAtUnixMS: 1,
}); err != nil {
t.Fatal(err)
}
participant.fail = true
if err := testCase.run(store); err == nil || !strings.Contains(err.Error(), "participant failure") {
t.Fatalf("mutation error=%v", err)
}
if _, found, err := store.GetSession(context.Background(), "ws-1", "session-1"); err != nil || !found {
t.Fatalf("session after participant rollback found=%v error=%v", found, err)
}
})
}
}
func assertParticipantMutationKinds(t *testing.T, delta TransactionDelta, want ...string) {
t.Helper()
got := make([]string, 0, len(delta.Mutations))
for index, mutation := range delta.Mutations {
if mutation.MutationID != fmt.Sprintf("%s:%d", delta.TransactionID, index+1) {
t.Fatalf("mutation %d id=%q transaction=%q", index, mutation.MutationID, delta.TransactionID)
}
got = append(got, mutation.EntityKind)
}
sort.Strings(got)
sort.Strings(want)
if fmt.Sprint(got) != fmt.Sprint(want) {
t.Fatalf("mutation kinds=%v want=%v", got, want)
}
}
func assertParticipantMutationEntityID(t *testing.T, delta TransactionDelta, entityKind, want string) {
t.Helper()
for _, mutation := range delta.Mutations {
if mutation.EntityKind == entityKind {
if mutation.EntityID != want {
t.Fatalf("%s mutation entity id=%q, want %q", entityKind, mutation.EntityID, want)
}
return
}
}
t.Fatalf("%s mutation not found in %#v", entityKind, delta.Mutations)
}