Files
tutti/packages/agent/daemon/runtime/controller_report_projection_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

346 lines
12 KiB
Go

package agentruntime
import (
"context"
"testing"
agentsessionstore "github.com/tutti-os/tutti/packages/agent/daemon/activity"
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
"github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
)
type captureGoalControlLifecycleObserver struct {
observations []GoalControlAppliedObservation
}
func (o *captureGoalControlLifecycleObserver) ObserveGoalControlApplied(
_ context.Context,
observation GoalControlAppliedObservation,
) error {
o.observations = append(o.observations, observation)
return nil
}
func TestControllerRoutesGoalControlAppliedOutsideSessionMetadata(t *testing.T) {
t.Parallel()
controller := NewController(nil, nil)
observer := &captureGoalControlLifecycleObserver{}
controller.SetGoalControlLifecycleObserver(observer)
session := Session{
RoomID: "room-1", AgentSessionID: "agent-session-1", Provider: ProviderClaudeCode,
ProviderSessionID: "provider-session-1", CWD: "/workspace",
}
controller.store(session)
ctx, ok := activityEventContext(session, "goal-applied-1", "")
if !ok {
t.Fatal("goal control event context unavailable")
}
event := activityshared.NewGoalControlApplied(ctx, map[string]any{
"operationId": "goal-op-1", "revision": int64(3), "repairEpoch": int64(1),
"action": "set", "providerTurnId": "provider-turn-1",
"goal": map[string]any{"objective": "ship it", "status": "active"},
})
controller.applySessionEventsByAgentSessionID(session.AgentSessionID, []activityshared.Event{event})
if len(observer.observations) != 1 {
t.Fatalf("goal observations=%#v", observer.observations)
}
observation := observer.observations[0]
if observation.WorkspaceID != "room-1" || observation.AgentSessionID != "agent-session-1" ||
observation.OperationID != "goal-op-1" || observation.Revision != 3 || observation.RepairEpoch != 1 ||
observation.Action != "set" || observation.ProviderTurnID != "provider-turn-1" ||
observation.Observed["objective"] != "ship it" {
t.Fatalf("goal observation=%#v", observation)
}
if report := reportActivityInput(session, []activityshared.Event{event}); len(report.StatePatches) != 0 {
t.Fatalf("internal goal lifecycle leaked into session report=%#v", report.StatePatches)
}
}
func TestEnrichReportStatePatchesWithSessionMetadataFillsSnapshotTitleAndIdentity(t *testing.T) {
t.Parallel()
report := &agentsessionstore.ReportActivityInput{
StatePatches: []agentsessionstore.WorkspaceAgentStatePatch{{
AgentSessionID: "agent-session-1",
CurrentPhase: "failed",
}},
}
enrichReportStatePatchesWithSessionMetadata(report, agentsessionstore.WorkspaceAgentStatePatch{
AgentSessionID: "agent-session-1",
Provider: "codex",
ProviderSessionID: "provider-session-1",
Model: "gpt-5",
PermissionModeID: "bypassPermissions",
CWD: "/workspace",
Title: "Automation Review",
Settings: map[string]any{"model": "gpt-5"},
RuntimeContext: map[string]any{
"title": "Automation Review",
"cwd": "/workspace",
},
})
patch := report.StatePatches[0]
if patch.Provider != "codex" ||
patch.ProviderSessionID != "provider-session-1" ||
patch.Model != "gpt-5" ||
patch.PermissionModeID != "bypassPermissions" ||
patch.CWD != "/workspace" ||
patch.Title != "Automation Review" {
t.Fatalf("enriched patch = %#v, want snapshot identity and title", patch)
}
if patch.RuntimeContext["title"] != "Automation Review" {
t.Fatalf("runtime context = %#v, want title fallback", patch.RuntimeContext)
}
}
func TestEnrichReportStatePatchesWithSessionMetadataKeepsIncomingTitle(t *testing.T) {
t.Parallel()
report := &agentsessionstore.ReportActivityInput{
StatePatches: []agentsessionstore.WorkspaceAgentStatePatch{{
AgentSessionID: "agent-session-1",
Title: "Provider title",
}},
}
enrichReportStatePatchesWithSessionMetadata(report, agentsessionstore.WorkspaceAgentStatePatch{
AgentSessionID: "agent-session-1",
Title: "Automation Review",
})
if got := report.StatePatches[0].Title; got != "Provider title" {
t.Fatalf("title = %q, want incoming provider title", got)
}
}
func TestEnrichReportStatePatchesWithSessionMetadataDoesNotAttachTurnLifecycle(t *testing.T) {
t.Parallel()
activeTurnID := "root-turn-1"
report := &agentsessionstore.ReportActivityInput{
StatePatches: []agentsessionstore.WorkspaceAgentStatePatch{{
AgentSessionID: "agent-session-1",
RootProviderTurn: &canonical.WorkspaceAgentRootProviderTurnTransition{
RootTurnID: "root-turn-1",
ProviderTurnID: "provider-turn-1",
Phase: agentsessionstore.RootProviderTurnPhaseCompleted,
},
}},
}
enrichReportStatePatchesWithSessionMetadata(report, agentsessionstore.WorkspaceAgentStatePatch{
AgentSessionID: "agent-session-1",
Provider: ProviderClaudeCode,
TurnLifecycle: &canonical.WorkspaceAgentTurnLifecycle{
ActiveTurnID: &activeTurnID,
Phase: "waiting",
},
SubmitAvailability: &canonical.WorkspaceAgentSubmitAvailability{
State: "blocked",
Reason: "active_turn",
},
})
patch := report.StatePatches[0]
if patch.Provider != ProviderClaudeCode {
t.Fatalf("provider = %q, want session metadata", patch.Provider)
}
if patch.TurnLifecycle != nil || patch.SubmitAvailability != nil {
t.Fatalf("persisted provider patch inherited runtime lifecycle: %#v", patch)
}
}
func TestEnrichStreamStateEventsWithSessionSnapshotFillsRuntimeContext(t *testing.T) {
t.Parallel()
adapter := &statefulInteractiveAdapter{
provider: ProviderClaudeCode,
snapshot: SessionStateSnapshot{
AgentSessionID: "agent-session-1",
Provider: ProviderClaudeCode,
TurnLifecycle: &TurnLifecycle{
ActiveTurnID: stringPtr("synthetic-turn-1"),
Phase: "running",
},
SubmitAvailability: &SubmitAvailability{
State: "blocked",
Reason: "active_turn",
},
PendingInteractive: &SessionInteractivePrompt{
Kind: "ask-user",
RequestID: "request-1",
ToolName: "AskUserQuestion",
Status: "waiting",
Input: map[string]any{
"questions": []any{map[string]any{
"id": "scope",
"question": "Scope?",
}},
},
},
RuntimeContext: map[string]any{
"usage": map[string]any{
"contextWindow": map[string]any{
"usedTokens": int64(38414),
"totalTokens": int64(200000),
},
},
},
},
}
controller := NewController([]Adapter{adapter}, nil)
session := Session{
RoomID: "room-1",
AgentSessionID: "agent-session-1",
Provider: ProviderClaudeCode,
}
controller.store(session)
events := []StreamEvent{{
EventType: StreamEventStatePatch,
Data: agentsessionstore.WorkspaceAgentStatePatch{
AgentSessionID: "agent-session-1",
CurrentPhase: "idle",
},
}}
controller.enrichStreamStateEventsWithSessionSnapshot(session, events)
patch, ok := events[0].Data.(agentsessionstore.WorkspaceAgentStatePatch)
if !ok {
t.Fatalf("stream patch type = %T, want WorkspaceAgentStatePatch", events[0].Data)
}
usage, _ := patch.RuntimeContext["usage"].(map[string]any)
contextWindow, _ := usage["contextWindow"].(map[string]any)
if got, _ := int64Value(contextWindow["totalTokens"]); got != 200000 {
t.Fatalf("runtime context usage = %#v, want totalTokens=200000", patch.RuntimeContext["usage"])
}
if patch.TurnLifecycle == nil ||
patch.TurnLifecycle.ActiveTurnID == nil ||
*patch.TurnLifecycle.ActiveTurnID != "synthetic-turn-1" ||
patch.TurnLifecycle.Phase != "running" {
t.Fatalf("turn lifecycle = %#v, want synthetic running", patch.TurnLifecycle)
}
if patch.SubmitAvailability == nil ||
patch.SubmitAvailability.State != "blocked" ||
patch.SubmitAvailability.Reason != "active_turn" {
t.Fatalf("submit availability = %#v, want active_turn blocked", patch.SubmitAvailability)
}
if patch.InteractionTransition != nil {
t.Fatalf("snapshot enrichment interaction transition = %#v, want nil", patch.InteractionTransition)
}
}
func TestSessionTitleEnrichmentDoesNotCrossSessionBoundaries(t *testing.T) {
t.Parallel()
controller := NewController(nil, nil)
session := Session{
RoomID: "room-1",
AgentSessionID: "root-session",
Provider: ProviderCodex,
Title: "Root title",
UserTitleSet: true,
}
controller.store(session)
report := &agentsessionstore.ReportActivityInput{
StatePatches: []agentsessionstore.WorkspaceAgentStatePatch{
{AgentSessionID: "root-session", Title: "Stale root title"},
{AgentSessionID: "child-session", Title: "Child title"},
},
}
controller.enrichReportStatePatchesWithSessionMetadata(session, report)
if got := report.StatePatches[0].Title; got != "Root title" {
t.Fatalf("root report title = %q, want Root title", got)
}
if got := report.StatePatches[1].Title; got != "Child title" {
t.Fatalf("child report title = %q, want Child title", got)
}
stream := []StreamEvent{
{
EventType: StreamEventStatePatch,
Data: agentsessionstore.WorkspaceAgentStatePatch{
AgentSessionID: "root-session",
Title: "Stale root title",
},
},
{
EventType: StreamEventStatePatch,
Data: agentsessionstore.WorkspaceAgentStatePatch{
AgentSessionID: "child-session",
Title: "Child title",
},
},
}
controller.enrichStreamStateEventsWithSessionSnapshot(session, stream)
rootPatch := stream[0].Data.(agentsessionstore.WorkspaceAgentStatePatch)
childPatch := stream[1].Data.(agentsessionstore.WorkspaceAgentStatePatch)
if rootPatch.Title != "Root title" {
t.Fatalf("root stream title = %q, want Root title", rootPatch.Title)
}
if childPatch.Title != "Child title" {
t.Fatalf("child stream title = %q, want Child title", childPatch.Title)
}
}
func TestDeriveSessionStatusFromEvents(t *testing.T) {
t.Parallel()
session := Session{AgentSessionID: "agent-session-1", Provider: ProviderCodex}
tests := []struct {
name string
events []activityshared.Event
fallback string
want string
}{
{
name: "keeps working without terminal event",
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnStarted, "turn-1", SessionStatusWorking, "", "", nil)},
fallback: SessionStatusReady,
want: SessionStatusWorking,
},
{
name: "turn completed makes conversation ready",
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnCompleted, "turn-1", SessionStatusCompleted, "", "", nil)},
fallback: SessionStatusWorking,
want: SessionStatusReady,
},
{
name: "interrupted turn marks session canceled",
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnCanceled, "turn-1", SessionStatusCanceled, "", "", nil)},
fallback: SessionStatusWorking,
want: SessionStatusCanceled,
},
{
name: "failed terminal event wins",
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnFailed, "turn-1", SessionStatusFailed, "", "", nil)},
fallback: SessionStatusWorking,
want: SessionStatusFailed,
},
{
name: "waiting turn update keeps session waiting",
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnUpdated, "turn-1", SessionStatusWaiting, "", "", map[string]any{"phase": string(activityshared.TurnPhaseWaitingApproval)})},
fallback: SessionStatusWorking,
want: SessionStatusWaiting,
},
{
name: "session completed ends session",
events: []activityshared.Event{newSessionActivityEvent(session, EventSessionCompleted, SessionStatusCompleted, nil)},
fallback: SessionStatusWorking,
want: SessionStatusCompleted,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := deriveSessionStatusFromEvents(tt.events, tt.fallback); got != tt.want {
t.Fatalf("status = %q, want %q", got, tt.want)
}
})
}
}