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
172 lines
6.3 KiB
Go
172 lines
6.3 KiB
Go
package liveprotocol
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestMessageDeltaContractRejectsSchemaDrift(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := map[string]string{
|
|
"missing turn identity": `{
|
|
"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"message_delta",
|
|
"data":{"workspaceId":"workspace-1","agentSessionId":"session-1","messageId":"message-1",
|
|
"role":"assistant","kind":"text","occurredAtUnixMs":1,
|
|
"content":{"operation":"set","value":"hello"}}
|
|
}`,
|
|
"append without text": `{
|
|
"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"message_delta",
|
|
"data":{"workspaceId":"workspace-1","agentSessionId":"session-1","messageId":"message-1",
|
|
"turnId":"turn-1","role":"assistant","kind":"text","occurredAtUnixMs":1,
|
|
"content":{"operation":"append_text"}}
|
|
}`,
|
|
"scalar semantics": `{
|
|
"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"message_delta",
|
|
"data":{"workspaceId":"workspace-1","agentSessionId":"session-1","messageId":"message-1",
|
|
"turnId":"turn-1","role":"assistant","kind":"text","occurredAtUnixMs":1,
|
|
"semantics":"not-an-object"}
|
|
}`,
|
|
"duplicate payload unset": `{
|
|
"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"message_delta",
|
|
"data":{"workspaceId":"workspace-1","agentSessionId":"session-1","messageId":"message-1",
|
|
"turnId":"turn-1","role":"assistant","kind":"text","occurredAtUnixMs":1,
|
|
"payloadUnset":["text","text"]}
|
|
}`,
|
|
"negative lifecycle timestamp": `{
|
|
"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"message_delta",
|
|
"data":{"workspaceId":"workspace-1","agentSessionId":"session-1","messageId":"message-1",
|
|
"turnId":"turn-1","role":"assistant","kind":"text","occurredAtUnixMs":1,
|
|
"completedAtUnixMs":-1}
|
|
}`,
|
|
}
|
|
|
|
for name, raw := range tests {
|
|
name, raw := name, raw
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if _, err := DecodeEvent([]byte(raw)); !errors.Is(err, ErrInvalidLiveEvent) {
|
|
t.Fatalf("DecodeEvent error = %v, want ErrInvalidLiveEvent", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntimeActivityUpdateContract(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
event, err := NewRuntimeActivityUpdateEvent(RuntimeActivityUpdateData{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
EventType: EventTypeRuntimeActivityUpdate, State: "running", OccurredAtUnixMS: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := MarshalEvent(event)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := DecodeEvent(raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
event.Data = []byte(`{"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"runtime_activity_update","state":"busy","occurredAtUnixMs":10}`)
|
|
if _, err := MarshalEvent(event); !errors.Is(err, ErrInvalidLiveEvent) {
|
|
t.Fatalf("invalid runtime activity error = %v, want ErrInvalidLiveEvent", err)
|
|
}
|
|
}
|
|
|
|
func TestTurnUpdateContractClosesTerminalAndActiveStates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
terminal := mustTerminalTurnUpdate(t)
|
|
var terminalData TurnUpdateData
|
|
if err := json.Unmarshal(terminal.Data, &terminalData); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
activeTurnID := terminalData.Turn.TurnID
|
|
terminalData.ActiveTurnID = &activeTurnID
|
|
if _, err := NewTurnUpdateEvent(terminalData); !errors.Is(err, ErrInvalidLiveEvent) {
|
|
t.Fatalf("settled turn with activeTurnId error = %v, want ErrInvalidLiveEvent", err)
|
|
}
|
|
|
|
terminalData.ActiveTurnID = nil
|
|
terminalData.Turn.Outcome = nil
|
|
if _, err := NewTurnUpdateEvent(terminalData); !errors.Is(err, ErrInvalidLiveEvent) {
|
|
t.Fatalf("settled turn without outcome error = %v, want ErrInvalidLiveEvent", err)
|
|
}
|
|
|
|
terminalData.Turn.Phase = "running"
|
|
terminalData.Turn.SettledAtUnixMS = nil
|
|
terminalData.ActiveTurnID = &activeTurnID
|
|
if _, err := NewTurnUpdateEvent(terminalData); err != nil {
|
|
t.Fatalf("valid active turn error = %v", err)
|
|
}
|
|
|
|
outcome := "completed"
|
|
terminalData.Turn.Outcome = &outcome
|
|
if _, err := NewTurnUpdateEvent(terminalData); !errors.Is(err, ErrInvalidLiveEvent) {
|
|
t.Fatalf("active turn with outcome error = %v, want ErrInvalidLiveEvent", err)
|
|
}
|
|
}
|
|
|
|
func TestCanonicalVariantsRejectNonObjectPayloads(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
auditRaw := []byte(`{
|
|
"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"session_audit",
|
|
"data":{"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"session_audit",
|
|
"audit":{"auditId":"audit-1","role":"system","payload":"bad",
|
|
"occurredAtUnixMs":1,"version":1}}
|
|
}`)
|
|
if _, err := DecodeEvent(auditRaw); !errors.Is(err, ErrInvalidLiveEvent) {
|
|
t.Fatalf("audit payload error = %v, want ErrInvalidLiveEvent", err)
|
|
}
|
|
|
|
interactionRaw := []byte(`{
|
|
"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"interaction_update",
|
|
"data":{"workspaceId":"workspace-1","agentSessionId":"session-1","eventType":"interaction_update",
|
|
"occurredAtUnixMs":1,
|
|
"interaction":{"requestId":"request-1","agentSessionId":"session-1","turnId":"turn-1",
|
|
"kind":"approval","status":"pending","toolName":null,"input":[],
|
|
"output":null,"metadata":null,"createdAtUnixMs":1,"updatedAtUnixMs":1}}
|
|
}`)
|
|
if _, err := DecodeEvent(interactionRaw); !errors.Is(err, ErrInvalidLiveEvent) {
|
|
t.Fatalf("interaction input error = %v, want ErrInvalidLiveEvent", err)
|
|
}
|
|
}
|
|
|
|
func TestInteractionSnapshotContractTreatsEmptyArrayAsExplicitClear(t *testing.T) {
|
|
t.Parallel()
|
|
event, err := NewInteractionSnapshotEvent(InteractionSnapshotData{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
EventType: EventTypeInteractionSnapshot, OccurredAtUnixMS: 0, RootTurnID: "turn-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var data InteractionSnapshotData
|
|
if err := json.Unmarshal(event.Data, &data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data.Interactions == nil || len(data.Interactions) != 0 {
|
|
t.Fatalf("interactions = %#v, want explicit empty array", data.Interactions)
|
|
}
|
|
if data.RootTurnID != "turn-1" {
|
|
t.Fatalf("root turn = %q, want turn-1", data.RootTurnID)
|
|
}
|
|
}
|
|
|
|
func TestInteractionSnapshotContractRequiresRootForEmptyCollection(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := NewInteractionSnapshotEvent(InteractionSnapshotData{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
EventType: EventTypeInteractionSnapshot, OccurredAtUnixMS: 0,
|
|
})
|
|
if !errors.Is(err, ErrInvalidLiveEvent) {
|
|
t.Fatalf("missing root error = %v, want ErrInvalidLiveEvent", err)
|
|
}
|
|
}
|