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
170 lines
5.2 KiB
Go
170 lines
5.2 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
|
)
|
|
|
|
func TestAppServerFileChangePreservesCwdInRawInput(t *testing.T) {
|
|
item := map[string]any{
|
|
"id": "item-file-change",
|
|
"type": "fileChange",
|
|
"status": "completed",
|
|
"cwd": "/workspace/project",
|
|
"changes": []any{
|
|
map[string]any{
|
|
"path": "/workspace/project/src/app.ts",
|
|
"kind": map[string]any{"type": "update"},
|
|
"diff": "@@ -1 +1 @@\n-old\n+new\n",
|
|
},
|
|
},
|
|
}
|
|
|
|
update, ok := appServerItemToolCallUpdate(item, true)
|
|
if !ok {
|
|
t.Fatalf("expected fileChange item to produce an update")
|
|
}
|
|
rawInput, ok := update["rawInput"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("rawInput = %#v, want map", update["rawInput"])
|
|
}
|
|
if got := asString(rawInput["cwd"]); got != "/workspace/project" {
|
|
t.Fatalf("rawInput.cwd = %q, want /workspace/project", got)
|
|
}
|
|
if rawInput["changes"] == nil {
|
|
t.Fatalf("rawInput.changes was not preserved")
|
|
}
|
|
}
|
|
|
|
func TestAppServerFileChangeCompletionUpdatesCanonicalTurn(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
session := standardTestSession(ProviderCodex)
|
|
normalizer := newACPTurnNormalizer()
|
|
update, ok := appServerItemToolCallUpdate(map[string]any{
|
|
"id": "item-delete",
|
|
"type": "fileChange",
|
|
"status": "completed",
|
|
"cwd": "/workspace/project",
|
|
"changes": []any{map[string]any{
|
|
"path": "/workspace/project/obsolete.ts",
|
|
"kind": map[string]any{"type": "delete"},
|
|
"diff": "@@ -1 +0,0 @@\n-obsolete",
|
|
}},
|
|
}, true)
|
|
if !ok {
|
|
t.Fatal("fileChange item did not produce a tool-call update")
|
|
}
|
|
events, ok := normalizer.ToolCallEvents(session, "turn-1", update)
|
|
if !ok || len(events) != 2 || events[1].Type != activityshared.EventTurnUpdated {
|
|
t.Fatalf("fileChange events = %#v, want completed call followed by turn.updated", events)
|
|
}
|
|
files := payloadArray(payloadMap(events[1].Payload.Metadata, "fileChanges")["files"])
|
|
if len(files) != 1 || files[0]["change"] != "deleted" {
|
|
t.Fatalf("turn file changes = %#v, want deleted", files)
|
|
}
|
|
}
|
|
|
|
func TestAppServerFileChangePatchUpdatePreservesAuthoritativeDiff(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
session := standardTestSession(ProviderCodex)
|
|
normalizer := newACPTurnNormalizer()
|
|
reduction := newCodexAppServerReducer(&CodexAppServerAdapter{}).ReduceNotification(
|
|
nil,
|
|
session,
|
|
"turn-1",
|
|
acpMessage{
|
|
Method: appServerNotifyFileChangePatchUpdated,
|
|
Params: mustJSONRawMessage(t, map[string]any{
|
|
"itemId": "item-file-change",
|
|
"changes": []any{map[string]any{
|
|
"path": "/workspace/project/src/app.ts",
|
|
"kind": map[string]any{"type": "update"},
|
|
"diff": "@@ -1 +1 @@\n-old\n+new\n",
|
|
}},
|
|
}),
|
|
},
|
|
normalizer,
|
|
nil,
|
|
)
|
|
if len(reduction.Events) < 2 {
|
|
t.Fatalf("events = %#v, want tool update and canonical turn update", reduction.Events)
|
|
}
|
|
last := reduction.Events[len(reduction.Events)-1]
|
|
if last.Type != activityshared.EventTurnUpdated {
|
|
t.Fatalf("last event type = %q, want turn updated", last.Type)
|
|
}
|
|
files := payloadArray(payloadMap(last.Payload.Metadata, "fileChanges")["files"])
|
|
if len(files) != 1 || files[0]["change"] != "modified" ||
|
|
files[0]["unifiedDiff"] != "@@ -1 +1 @@\n-old\n+new\n" {
|
|
t.Fatalf("file changes = %#v, want authoritative modified diff", files)
|
|
}
|
|
}
|
|
|
|
func TestAppServerFileChangeApprovalUsesStartedItemChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
session := Session{
|
|
Provider: ProviderCodex,
|
|
AgentSessionID: "session-file-change-approval",
|
|
}
|
|
normalizer := newACPTurnNormalizer()
|
|
item := map[string]any{
|
|
"id": "item-file-change",
|
|
"type": "fileChange",
|
|
"status": "inProgress",
|
|
"cwd": "/workspace/project",
|
|
"changes": []any{
|
|
map[string]any{
|
|
"path": "/workspace/project/src/app.ts",
|
|
"kind": map[string]any{"type": "update"},
|
|
},
|
|
},
|
|
}
|
|
update, ok := appServerItemToolCallUpdate(item, false)
|
|
if !ok {
|
|
t.Fatal("fileChange item did not produce a tool-call update")
|
|
}
|
|
if events, _ := normalizer.ToolCallEvents(session, "turn-1", update); len(events) == 0 {
|
|
t.Fatal("fileChange item did not populate the turn normalizer")
|
|
}
|
|
|
|
adapter := &CodexAppServerAdapter{}
|
|
events, pending, err := adapter.appServerApprovalRequested(
|
|
session,
|
|
"turn-1",
|
|
json.RawMessage(`1`),
|
|
appServerMethodFileChangeApproval,
|
|
map[string]any{
|
|
"itemId": "item-file-change",
|
|
"reason": nil,
|
|
"grantRoot": nil,
|
|
},
|
|
normalizer,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("appServerApprovalRequested: %v", err)
|
|
}
|
|
if pending == nil {
|
|
t.Fatal("pending approval is nil")
|
|
return
|
|
}
|
|
if pending.approvalPurpose != approvalPurposeEditFiles {
|
|
t.Fatalf("pending approval purpose = %q, want %q", pending.approvalPurpose, approvalPurposeEditFiles)
|
|
}
|
|
interaction := events[len(events)-1].Payload.Interaction
|
|
if interaction == nil || asString(interaction.Metadata["approvalPurpose"]) != approvalPurposeEditFiles {
|
|
t.Fatalf("interaction approval purpose = %#v, want %q", interaction, approvalPurposeEditFiles)
|
|
}
|
|
changes, ok := pending.input["changes"].([]any)
|
|
if !ok || len(changes) != 1 {
|
|
t.Fatalf("pending approval changes = %#v, want the started item changes", pending.input["changes"])
|
|
}
|
|
if got := asString(payloadObject(changes[0])["path"]); got != "/workspace/project/src/app.ts" {
|
|
t.Fatalf("pending approval change path = %q", got)
|
|
}
|
|
}
|