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
690 lines
27 KiB
Go
690 lines
27 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
|
"github.com/tutti-os/tutti/packages/agent/daemon/liveprotocol"
|
|
)
|
|
|
|
func TestClaudeCodeSDKAdapterMapsSessionTitleUpdated(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
session.Title = "inspect repo"
|
|
|
|
events, terminal, err := adapter.sidecarTurnEvents(&claudeSDKAdapterSession{}, session, "turn-1", claudeSDKSidecarEvent{
|
|
Type: "session_title_updated",
|
|
Payload: map[string]any{
|
|
"title": " Inspect repository structure ",
|
|
},
|
|
})
|
|
if err != nil || terminal {
|
|
t.Fatalf("session_title_updated terminal=%v err=%v", terminal, err)
|
|
}
|
|
if len(events) != 1 || events[0].Type != activityshared.EventSessionUpdated {
|
|
t.Fatalf("events = %#v, want session.updated", events)
|
|
}
|
|
if events[0].Payload.Title != "Inspect repository structure" {
|
|
t.Fatalf("title = %q, want provider title", events[0].Payload.Title)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterProjectsSDKRuntimeActivityWithoutTurnIdentity(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{liveState: newClaudeSDKLiveState()}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
for _, state := range []string{"running", "idle"} {
|
|
events, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "", claudeSDKSidecarEvent{
|
|
Type: "sdk_lifecycle_observed",
|
|
Payload: map[string]any{
|
|
"sdkMessageType": "system",
|
|
"sdkMessageSubtype": "session_state_changed",
|
|
"state": state,
|
|
},
|
|
})
|
|
if err != nil || terminal {
|
|
t.Fatalf("state=%q terminal=%v err=%v", state, terminal, err)
|
|
}
|
|
if len(events) != 1 || events[0].Type != activityshared.EventSessionUpdated {
|
|
t.Fatalf("state=%q events=%#v, want session.updated", state, events)
|
|
}
|
|
if got := string(events[0].Payload.RuntimeActivity); got != state {
|
|
t.Fatalf("state=%q runtime activity=%q", state, got)
|
|
}
|
|
if _, persisted := claudeSDKRuntimeContext(session, adapterSession)["runtimeActivityState"]; persisted {
|
|
t.Fatalf("state=%q leaked into persistent runtime context", state)
|
|
}
|
|
report := reportActivityInput(session, events)
|
|
if len(report.StatePatches) != 1 || report.StatePatches[0].RuntimeActivity == nil ||
|
|
report.StatePatches[0].RuntimeActivity.State != state ||
|
|
report.StatePatches[0].RuntimeActivity.OccurredAtUnixMS <= 0 {
|
|
t.Fatalf("state=%q report runtime activity=%#v", state, report.StatePatches)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterPreservesResolvedModelDescriptor(t *testing.T) {
|
|
adapterSession := &claudeSDKAdapterSession{
|
|
liveState: newClaudeSDKLiveState(),
|
|
}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
adapterSession.applySessionPayload(&session, map[string]any{
|
|
"model": "default",
|
|
"configOptions": []any{map[string]any{
|
|
"id": "model",
|
|
"currentValue": "default",
|
|
"effectiveValue": "claude-opus-4-8",
|
|
"options": []any{
|
|
map[string]any{"name": "Default", "value": "default"},
|
|
map[string]any{"name": "Opus 4.8", "value": "opus"},
|
|
},
|
|
}},
|
|
})
|
|
|
|
configOptions, ok := claudeSDKRuntimeContext(
|
|
session,
|
|
adapterSession,
|
|
)["configOptions"].([]map[string]any)
|
|
if !ok || len(configOptions) == 0 {
|
|
t.Fatal("runtime config options empty")
|
|
}
|
|
if got := asString(configOptions[0]["effectiveValue"]); got != "claude-opus-4-8" {
|
|
t.Fatalf("effective model = %q, want claude-opus-4-8", got)
|
|
}
|
|
if got := asString(configOptions[0]["currentValue"]); got != "default" {
|
|
t.Fatalf("current model = %q, want inherited default", got)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterMirrorsGoalSlashPromptIntoRuntimeContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapterSession := &claudeSDKAdapterSession{
|
|
liveState: newClaudeSDKLiveState(),
|
|
}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
if event, ok := adapterSession.mirrorGoalSlashPrompt(session, "/goal ship native goal"); !ok {
|
|
t.Fatal("mirrorGoalSlashPrompt ok=false, want goal mirror")
|
|
} else if event.Type != activityshared.EventSessionUpdated {
|
|
t.Fatalf("event type = %q, want session.updated", event.Type)
|
|
}
|
|
goal := payloadObject(claudeSDKRuntimeContext(session, adapterSession)["goal"])
|
|
if asString(goal["objective"]) != "ship native goal" || asString(goal["status"]) != "active" {
|
|
t.Fatalf("runtime goal = %#v, want active objective", goal)
|
|
}
|
|
|
|
if _, ok := adapterSession.mirrorGoalSlashPrompt(session, "/goal clear"); !ok {
|
|
t.Fatal("mirrorGoalSlashPrompt clear ok=false")
|
|
}
|
|
if goal := payloadObject(claudeSDKRuntimeContext(session, adapterSession)["goal"]); len(goal) != 0 {
|
|
t.Fatalf("runtime goal after clear = %#v, want empty", goal)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterMapsGoalObservedSidecarEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{
|
|
liveState: newClaudeSDKLiveState(),
|
|
}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
events, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-goal", claudeSDKSidecarEvent{
|
|
Type: "goal_observed",
|
|
Payload: map[string]any{
|
|
"turnId": "turn-goal",
|
|
"updateType": "thread_goal_update",
|
|
"goal": map[string]any{
|
|
"objective": "ship native goal",
|
|
"status": "active",
|
|
"sentinel": true,
|
|
},
|
|
},
|
|
})
|
|
if err != nil || terminal {
|
|
t.Fatalf("goal_observed terminal=%v err=%v", terminal, err)
|
|
}
|
|
if len(activityEventsWithType(events, activityshared.EventSessionUpdated)) < 2 {
|
|
t.Fatalf("events = %#v, want goal session.updated events", events)
|
|
}
|
|
goal := payloadObject(claudeSDKRuntimeContext(session, adapterSession)["goal"])
|
|
if asString(goal["objective"]) != "ship native goal" || asString(goal["status"]) != "active" {
|
|
t.Fatalf("runtime goal = %#v, want active SDK goal status", goal)
|
|
}
|
|
|
|
events, terminal, err = adapter.sidecarTurnEvents(adapterSession, session, "turn-goal", claudeSDKSidecarEvent{
|
|
Type: "goal_observed",
|
|
Payload: map[string]any{
|
|
"turnId": "turn-goal",
|
|
"updateType": "thread_goal_cleared",
|
|
},
|
|
})
|
|
if err != nil || terminal {
|
|
t.Fatalf("goal cleared terminal=%v err=%v", terminal, err)
|
|
}
|
|
if len(events) == 0 {
|
|
t.Fatal("events empty, want goal cleared session.updated")
|
|
}
|
|
if goal := payloadObject(claudeSDKRuntimeContext(session, adapterSession)["goal"]); len(goal) != 0 {
|
|
t.Fatalf("runtime goal after clear = %#v, want empty", goal)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterExecLeavesInitialTitleToController(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
conn := &scriptedClaudeSDKConnection{
|
|
frames: []ProcessFrame{{
|
|
Stdout: []byte(`{"type":"provider_turn_identity_resolved","payload":{"turnId":"turn-1","providerTurnId":"provider-turn-1"}}` + "\n" + `{"type":"turn_completed","payload":{"turnId":"turn-1","providerTurnId":"provider-turn-1","stopReason":"end_turn"}}` + "\n"),
|
|
}},
|
|
}
|
|
adapterSession := &claudeSDKAdapterSession{
|
|
conn: conn,
|
|
reader: &claudeSDKLineReader{conn: conn},
|
|
providerSessionID: session.ProviderSessionID,
|
|
pendingRequests: make(map[string]*pendingInteractiveRequest),
|
|
liveState: newClaudeSDKLiveState(),
|
|
}
|
|
adapter.storeSession(session.AgentSessionID, adapterSession)
|
|
|
|
var emitted []activityshared.Event
|
|
events, err := adapter.Exec(context.Background(), session, textPrompt(" inspect repo "), "", "turn-1", func(next []activityshared.Event) {
|
|
emitted = append(emitted, next...)
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("Exec: %v", err)
|
|
}
|
|
titleEvents := activityEventsWithType(emitted, activityshared.EventSessionUpdated)
|
|
if len(titleEvents) != 0 {
|
|
t.Fatalf("title events = %#v, want adapter to leave initial title to controller", titleEvents)
|
|
}
|
|
if !hasActivityMessage(events, activityshared.MessageRoleUser, "inspect repo") {
|
|
t.Fatalf("events = %#v, missing user prompt", events)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterIgnoresCanceledTurnOrphanBeforeCompactResult(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
adapterSession := &claudeSDKAdapterSession{liveState: newClaudeSDKLiveState()}
|
|
|
|
_, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-canceled", claudeSDKSidecarEvent{
|
|
Type: "turn_canceled",
|
|
Payload: map[string]any{"turnId": "turn-canceled"},
|
|
})
|
|
if err != nil || !terminal {
|
|
t.Fatalf("turn_canceled terminal=%v err=%v, want terminal cancel", terminal, err)
|
|
}
|
|
adapter.beginClaudeSDKRootTurn(adapterSession, "turn-compact", "turn-compact")
|
|
orphan, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "turn_completed",
|
|
Payload: map[string]any{"turnId": "turn-canceled"},
|
|
})
|
|
if err != nil || terminal || len(orphan) != 0 {
|
|
t.Fatalf("orphan events=%#v terminal=%v err=%v, want ignored stale turn result", orphan, terminal, err)
|
|
}
|
|
compact, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "turn_completed",
|
|
Payload: map[string]any{"turnId": "turn-compact"},
|
|
})
|
|
if err != nil || !terminal || len(compact) != 1 || compact[0].Type != activityshared.EventRootProviderTurnCompleted ||
|
|
compact[0].Payload.TurnID != "turn-compact" || compact[0].Payload.ProviderTurnID != "turn-compact" {
|
|
t.Fatalf("compact result events=%#v terminal=%v err=%v, want root provider completion", compact, terminal, err)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterMapsCompactLifecycleAsSystemNotice(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{liveState: newClaudeSDKLiveState()}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
started, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "compact_started",
|
|
Payload: map[string]any{"turnId": "turn-compact"},
|
|
})
|
|
if err != nil || terminal || len(started) != 1 {
|
|
t.Fatalf("compact_started events=%#v terminal=%v err=%v", started, terminal, err)
|
|
}
|
|
failed, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "compact_failed",
|
|
Payload: map[string]any{
|
|
"turnId": "turn-compact",
|
|
"content": "Compacting failed: Not enough messages to compact.",
|
|
},
|
|
})
|
|
if err != nil || terminal || len(failed) != 1 {
|
|
t.Fatalf("compact_failed events=%#v terminal=%v err=%v", failed, terminal, err)
|
|
}
|
|
if started[0].EventID != failed[0].EventID {
|
|
t.Fatalf("compact event IDs = %q and %q, want one stable notice", started[0].EventID, failed[0].EventID)
|
|
}
|
|
if started[0].Payload.Content != appServerCompactingContextTitle ||
|
|
started[0].Payload.Metadata["kind"] != "agent_system_notice" ||
|
|
started[0].Payload.Metadata["noticeCommand"] != "compact" ||
|
|
started[0].Payload.Metadata["noticeCommandStatus"] != "running" {
|
|
t.Fatalf("compact_started = %#v, want running compact system notice", started[0])
|
|
}
|
|
if failed[0].Payload.Content != appServerCompactionInterruptedTitle ||
|
|
failed[0].Payload.Metadata["noticeCommandStatus"] != "failed" ||
|
|
failed[0].Payload.Metadata["detail"] != "Not enough messages to compact." {
|
|
t.Fatalf("compact_failed = %#v, want failed compact system notice", failed[0])
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterMapsContextOverflowAsHandoffRequired(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{liveState: newClaudeSDKLiveState()}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
_, _, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "compact_started",
|
|
Payload: map[string]any{"turnId": "turn-compact"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
failed, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "compact_failed",
|
|
Payload: map[string]any{
|
|
"turnId": "turn-compact",
|
|
"reason": "Maximum context length exceeded.",
|
|
"contextHandoffRequired": true,
|
|
},
|
|
})
|
|
if err != nil || terminal || len(failed) != 1 {
|
|
t.Fatalf("compact_failed events=%#v terminal=%v err=%v", failed, terminal, err)
|
|
}
|
|
metadata := failed[0].Payload.Metadata
|
|
if metadata["noticeKind"] != "context_handoff_required" ||
|
|
metadata["severity"] != "error" ||
|
|
metadata["retryable"] != false {
|
|
t.Fatalf("compact_failed metadata=%#v, want terminal handoff error", metadata)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterSettlesActiveCompactWithTurn(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
turnEvent string
|
|
wantNoticeStatus string
|
|
}{
|
|
{name: "canceled", turnEvent: "turn_canceled", wantNoticeStatus: "canceled"},
|
|
{name: "failed", turnEvent: "turn_failed", wantNoticeStatus: "failed"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{liveState: newClaudeSDKLiveState()}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
started, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "compact_started",
|
|
Payload: map[string]any{"turnId": "turn-compact"},
|
|
})
|
|
if err != nil || terminal || len(started) != 1 {
|
|
t.Fatalf("compact_started events=%#v terminal=%v err=%v", started, terminal, err)
|
|
}
|
|
|
|
settled, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: test.turnEvent,
|
|
Payload: map[string]any{"turnId": "turn-compact", "error": "provider stopped"},
|
|
})
|
|
if err != nil || !terminal {
|
|
t.Fatalf("%s events=%#v terminal=%v err=%v", test.turnEvent, settled, terminal, err)
|
|
}
|
|
var compact *activityshared.Event
|
|
for index := range settled {
|
|
if settled[index].Payload.Metadata["noticeCommand"] == "compact" {
|
|
compact = &settled[index]
|
|
break
|
|
}
|
|
}
|
|
if compact == nil {
|
|
t.Fatalf("%s events=%#v, want terminal compact notice", test.turnEvent, settled)
|
|
}
|
|
if compact.EventID != started[0].EventID ||
|
|
compact.Payload.Content != appServerCompactionInterruptedTitle ||
|
|
compact.Payload.Metadata["noticeCommandStatus"] != test.wantNoticeStatus {
|
|
t.Fatalf("terminal compact = %#v, want stable %s notice", compact, test.wantNoticeStatus)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterDoesNotResettleTerminalCompactWithTurn(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{liveState: newClaudeSDKLiveState()}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
if _, _, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "compact_started",
|
|
Payload: map[string]any{"turnId": "turn-compact"},
|
|
}); err != nil {
|
|
t.Fatalf("compact_started: %v", err)
|
|
}
|
|
if _, _, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "compact_failed",
|
|
Payload: map[string]any{"turnId": "turn-compact", "reason": "not enough context"},
|
|
}); err != nil {
|
|
t.Fatalf("compact_failed: %v", err)
|
|
}
|
|
settled, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "turn_failed",
|
|
Payload: map[string]any{"turnId": "turn-compact", "error": "provider stopped"},
|
|
})
|
|
if err != nil || !terminal {
|
|
t.Fatalf("turn_failed events=%#v terminal=%v err=%v", settled, terminal, err)
|
|
}
|
|
for _, event := range settled {
|
|
if event.Payload.Metadata["noticeCommand"] == "compact" {
|
|
t.Fatalf("turn_failed events=%#v, terminal compact must not be emitted twice", settled)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterIgnoresCompactTerminalAfterSynthesizedCancel(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{liveState: newClaudeSDKLiveState()}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
started, _, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "compact_started",
|
|
Payload: map[string]any{"turnId": "turn-compact"},
|
|
})
|
|
if err != nil || len(started) != 1 {
|
|
t.Fatalf("compact_started events=%#v err=%v", started, err)
|
|
}
|
|
settled, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", claudeSDKSidecarEvent{
|
|
Type: "turn_canceled",
|
|
Payload: map[string]any{"turnId": "turn-compact"},
|
|
})
|
|
if err != nil || !terminal {
|
|
t.Fatalf("turn_canceled events=%#v terminal=%v err=%v", settled, terminal, err)
|
|
}
|
|
var canceledCompact *activityshared.Event
|
|
for index := range settled {
|
|
if settled[index].Payload.Metadata["noticeCommandStatus"] == "canceled" {
|
|
canceledCompact = &settled[index]
|
|
break
|
|
}
|
|
}
|
|
if canceledCompact == nil || canceledCompact.EventID != started[0].EventID {
|
|
t.Fatalf("turn_canceled events=%#v, want stable canceled compact notice", settled)
|
|
}
|
|
|
|
lateEvents := []claudeSDKSidecarEvent{
|
|
{Type: "compact_completed", Payload: map[string]any{"turnId": "turn-compact"}},
|
|
{Type: "compact_failed", Payload: map[string]any{"turnId": "turn-compact", "reason": "late failure"}},
|
|
}
|
|
for _, event := range lateEvents {
|
|
late, lateTerminal, lateErr := adapter.sidecarTurnEvents(adapterSession, session, "turn-compact", event)
|
|
if lateErr != nil || lateTerminal || len(late) != 0 {
|
|
t.Fatalf("late %s events=%#v terminal=%v err=%v, want ignored terminal", event.Type, late, lateTerminal, lateErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterMapsThinkingEvents(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
streaming, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-1", claudeSDKSidecarEvent{
|
|
Type: "thinking_delta",
|
|
Payload: map[string]any{
|
|
"snapshot": "Need context.",
|
|
},
|
|
})
|
|
if err != nil || terminal {
|
|
t.Fatalf("thinking_delta err=%v terminal=%v", err, terminal)
|
|
}
|
|
completed, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-1", claudeSDKSidecarEvent{
|
|
Type: "thinking_completed",
|
|
Payload: map[string]any{
|
|
"content": "Need context.",
|
|
},
|
|
})
|
|
if err != nil || terminal {
|
|
t.Fatalf("thinking_completed err=%v terminal=%v", err, terminal)
|
|
}
|
|
if len(streaming) != 1 || len(completed) != 1 {
|
|
t.Fatalf("events = %#v %#v, want one streaming and one completed thinking event", streaming, completed)
|
|
}
|
|
if streaming[0].Type != activityshared.EventMessageAppended ||
|
|
streaming[0].Payload.Role != activityshared.MessageRoleAssistantThinking ||
|
|
streaming[0].Payload.Content != "Need context." {
|
|
t.Fatalf("streaming thinking = %#v", streaming[0])
|
|
}
|
|
if completed[0].EventID != streaming[0].EventID {
|
|
t.Fatalf("thinking event IDs = %q and %q, want stable ID", streaming[0].EventID, completed[0].EventID)
|
|
}
|
|
if completed[0].Payload.Metadata["streamState"] != messageStreamStateCompleted {
|
|
t.Fatalf("completed metadata = %#v, want completed stream state", completed[0].Payload.Metadata)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterProjectsCumulativeAssistantSnapshotsAsSuffixDeltas(t *testing.T) {
|
|
t.Parallel()
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
project := func(snapshot string) liveprotocol.MessageDeltaData {
|
|
t.Helper()
|
|
events, terminal, err := adapter.sidecarTurnEvents(
|
|
adapterSession,
|
|
session,
|
|
"turn-1",
|
|
claudeSDKSidecarEvent{
|
|
Type: "assistant_delta",
|
|
Payload: map[string]any{
|
|
"turnId": "turn-1",
|
|
"snapshot": snapshot,
|
|
},
|
|
},
|
|
)
|
|
if err != nil || terminal || len(events) != 1 {
|
|
t.Fatalf("assistant_delta events=%#v terminal=%v err=%v", events, terminal, err)
|
|
}
|
|
stream := ProjectActivityEventsToStreamEvents(session, events)
|
|
if len(stream) != 1 || stream[0].EventType != StreamEventMessageDelta {
|
|
t.Fatalf("stream = %#v, want one message_delta", stream)
|
|
}
|
|
event, ok := stream[0].Data.(liveprotocol.Event)
|
|
if !ok {
|
|
t.Fatalf("stream data = %T, want liveprotocol.Event", stream[0].Data)
|
|
}
|
|
var delta liveprotocol.MessageDeltaData
|
|
if err := json.Unmarshal(event.Data, &delta); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return delta
|
|
}
|
|
|
|
first := project("Hel")
|
|
if first.Content == nil ||
|
|
first.Content.Operation != "set" ||
|
|
string(first.Content.Value) != `"Hel"` {
|
|
t.Fatalf("first delta = %#v, want set Hel", first)
|
|
}
|
|
second := project("Hello")
|
|
if second.MessageID != first.MessageID ||
|
|
second.Content == nil ||
|
|
second.Content.Operation != "append_text" ||
|
|
second.Content.Text != "lo" {
|
|
t.Fatalf("second delta = %#v, want same-message append lo", second)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterSuppressesGoalClearControlTranscript(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{
|
|
liveState: newClaudeSDKLiveState(),
|
|
goalClearControlTurns: map[string]struct{}{
|
|
"turn-clear": {},
|
|
},
|
|
}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
for _, event := range []claudeSDKSidecarEvent{
|
|
{Type: "turn_started", Payload: map[string]any{"turnId": "turn-clear"}},
|
|
{Type: "assistant_delta", Payload: map[string]any{"turnId": "turn-clear", "snapshot": "Goal cleared: ship it"}},
|
|
{Type: "assistant_completed", Payload: map[string]any{"turnId": "turn-clear", "content": "Goal cleared: ship it"}},
|
|
{Type: "thinking_delta", Payload: map[string]any{"turnId": "turn-clear", "snapshot": "Clearing goal"}},
|
|
{Type: "thinking_completed", Payload: map[string]any{"turnId": "turn-clear", "content": "Clearing goal"}},
|
|
} {
|
|
events, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "", event)
|
|
if err != nil || terminal || len(events) != 0 {
|
|
t.Fatalf("%s events=%#v terminal=%v err=%v, want suppressed transcript", event.Type, events, terminal, err)
|
|
}
|
|
}
|
|
|
|
ordinary, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-normal", claudeSDKSidecarEvent{
|
|
Type: "assistant_completed",
|
|
Payload: map[string]any{
|
|
"turnId": "turn-normal",
|
|
"content": "Goal cleared: ship it",
|
|
},
|
|
})
|
|
if err != nil || terminal || !hasActivityMessage(ordinary, activityshared.MessageRoleAssistant, "Goal cleared: ship it") {
|
|
t.Fatalf("ordinary assistant events=%#v terminal=%v err=%v, want visible matching text", ordinary, terminal, err)
|
|
}
|
|
|
|
for index, terminalType := range []string{"turn_completed", "turn_canceled", "turn_failed"} {
|
|
turnID := "turn-clear"
|
|
if index > 0 {
|
|
turnID += "-" + terminalType
|
|
}
|
|
adapterSession.goalClearControlTurns[turnID] = struct{}{}
|
|
terminalEvents, terminal, terminalErr := adapter.sidecarTurnEvents(adapterSession, session, "", claudeSDKSidecarEvent{
|
|
Type: terminalType,
|
|
Payload: map[string]any{"turnId": turnID, "error": "failed"},
|
|
})
|
|
if terminalErr != nil || !terminal {
|
|
t.Fatalf("%s terminal=%v err=%v, want terminal", terminalType, terminal, terminalErr)
|
|
}
|
|
if len(terminalEvents) != 0 {
|
|
t.Fatalf("%s events=%#v, want internal control terminal suppressed", terminalType, terminalEvents)
|
|
}
|
|
if adapter.isGoalClearControlTurn(adapterSession, turnID) {
|
|
t.Fatalf("%s clear control turn remained registered", terminalType)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeSDKAdapterMapsToolLifecycleAndFileMetadata(t *testing.T) {
|
|
adapter := NewClaudeCodeSDKAdapter(nil)
|
|
adapterSession := &claudeSDKAdapterSession{}
|
|
session := standardTestSession(ProviderClaudeCode)
|
|
|
|
started, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-1", claudeSDKSidecarEvent{
|
|
Type: "tool_started",
|
|
Payload: map[string]any{
|
|
"toolCallId": "toolu-1",
|
|
"toolName": "Edit",
|
|
"callType": "file_change",
|
|
"name": "Edit",
|
|
"status": "streaming",
|
|
"input": map[string]any{
|
|
"file_path": "/tmp/a.txt",
|
|
"old_string": "old",
|
|
"new_string": "new",
|
|
},
|
|
"locations": []any{map[string]any{"path": "/tmp/a.txt"}},
|
|
"metadata": map[string]any{
|
|
"fileChange": map[string]any{
|
|
"paths": []any{"/tmp/a.txt"},
|
|
"oldText": "old",
|
|
"newText": "new",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil || terminal {
|
|
t.Fatalf("tool_started err=%v terminal=%v", err, terminal)
|
|
}
|
|
completed, terminal, err := adapter.sidecarTurnEvents(adapterSession, session, "turn-1", claudeSDKSidecarEvent{
|
|
Type: "tool_completed",
|
|
Payload: map[string]any{
|
|
"toolCallId": "toolu-1",
|
|
"toolName": "Edit",
|
|
"callType": "file_change",
|
|
"name": "Edit",
|
|
"status": "completed",
|
|
"output": map[string]any{
|
|
"text": "updated",
|
|
"structuredPatch": []any{
|
|
map[string]any{"path": "/tmp/a.txt", "oldString": "old", "newString": "new"},
|
|
},
|
|
},
|
|
"metadata": map[string]any{
|
|
"claudeToolResponse": map[string]any{
|
|
"agentId": "agent-1",
|
|
"totalDurationMs": 123,
|
|
"originalFile": "large provider snapshot",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil || terminal {
|
|
t.Fatalf("tool_completed err=%v terminal=%v", err, terminal)
|
|
}
|
|
if len(started) != 1 || started[0].Type != activityshared.EventCallStarted {
|
|
t.Fatalf("started = %#v, want call.started", started)
|
|
}
|
|
if len(completed) != 2 || completed[0].Type != activityshared.EventCallCompleted || completed[1].Type != activityshared.EventTurnUpdated {
|
|
t.Fatalf("completed = %#v, want call.completed followed by turn.updated", completed)
|
|
}
|
|
if started[0].EventID != "claude-sdk:tool:toolu-1" || completed[0].EventID != started[0].EventID {
|
|
t.Fatalf("event IDs = %q %q, want stable tool ID", started[0].EventID, completed[0].EventID)
|
|
}
|
|
if started[0].Payload.CallID != "toolu-1" || started[0].Payload.CallType != "file_change" {
|
|
t.Fatalf("started payload = %#v", started[0].Payload)
|
|
}
|
|
if locations, ok := started[0].Payload.Metadata["locations"].([]any); !ok || len(locations) != 1 {
|
|
t.Fatalf("locations metadata = %#v, want one file location", started[0].Payload.Metadata["locations"])
|
|
}
|
|
sidecarMetadata := payloadMap(started[0].Payload.Metadata, "metadata")
|
|
fileChange := payloadMap(sidecarMetadata, "fileChange")
|
|
if fileChange["oldText"] != "old" || fileChange["newText"] != "new" {
|
|
t.Fatalf("fileChange metadata = %#v, want edit diff text", fileChange)
|
|
}
|
|
completedMetadata := payloadMap(completed[0].Payload.Metadata, "metadata")
|
|
if completedMetadata["agentId"] != "agent-1" || completedMetadata["durationMs"] != 123 {
|
|
t.Fatalf("completed metadata = %#v, want canonical Claude metadata", completed[0].Payload.Metadata)
|
|
}
|
|
if completedMetadata["claudeToolResponse"] != nil {
|
|
t.Fatalf("completed metadata retained Claude response envelope: %#v", completed[0].Payload.Metadata)
|
|
}
|
|
if patches, ok := completed[0].Payload.Output["structuredPatch"].([]any); !ok || len(patches) != 1 {
|
|
t.Fatalf("completed output = %#v, want canonical structured patch", completed[0].Payload.Output)
|
|
}
|
|
turnFiles := payloadArray(payloadMap(completed[1].Payload.Metadata, "fileChanges")["files"])
|
|
if len(turnFiles) != 1 || turnFiles[0]["path"] != "/tmp/a.txt" || turnFiles[0]["change"] != "modified" {
|
|
t.Fatalf("turn file changes = %#v, want canonical modified file", turnFiles)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeClaudeSDKToolPayloadCanonicalizesNoNewlineMarkers(t *testing.T) {
|
|
payload := normalizeClaudeSDKToolPayload(map[string]any{
|
|
"output": map[string]any{
|
|
"changes": []any{map[string]any{
|
|
"path": "/tmp/a.txt",
|
|
"diff": "@@ -1 +1 @@\n-old\n \\ No newline at end of file\n+new\n \\ No newline at end of file",
|
|
}},
|
|
},
|
|
})
|
|
output := payloadMap(payload, "output")
|
|
changes, _ := output["changes"].([]any)
|
|
change := payloadObject(changes[0])
|
|
want := "@@ -1 +1 @@\n-old\n\\ No newline at end of file\n+new\n\\ No newline at end of file"
|
|
if change["diff"] != want {
|
|
t.Fatalf("normalized diff = %q, want %q", change["diff"], want)
|
|
}
|
|
}
|