Files
correx/apps/tui-go/internal/protocol/golden_test.go
T
kami 1017bfffef feat(tui): event-derived ordering + retire Kotlin TUI
Rebuild the Go TUI transcript from the event stream instead of
optimistic local echoes. ChatTurnEvent now maps to ServerMessage.ChatTurn
(chat.turn); the TUI auto-vivifies sessions and renders user+router turns
from events. SessionStarted is de-special-cased into SessionAnnounced
(still carries workflowId), dropping the router.response shadow and
optimistic echoes.

Delete the unused Kotlin TUI (apps/tui) and add a kotlinx<->Go
golden-fixture test to lock the wire protocol. Gitignore server runtime
logs.
2026-06-02 23:38:20 +04:00

87 lines
2.7 KiB
Go

package protocol
import "testing"
// Golden wire frames as emitted by the Kotlin server (kotlinx.serialization). These
// strings are the cross-language contract: the Kotlin side pins the encode in
// ServerMessageSerializationTest; this test pins the Go decode of the exact same bytes,
// so a discriminator (@SerialName) or field-name drift fails loudly on one side.
func TestDecodeGoldenServerFrames(t *testing.T) {
cases := []struct {
name string
json string
wantType string
eventBear bool
check func(t *testing.T, m ServerMessage)
}{
{
name: "session.announced",
json: `{"type":"session.announced","sessionId":"s1","workflowId":"healthcheck","sequence":6,"sessionSequence":1}`,
wantType: TypeSessionAnnounced,
eventBear: true,
check: func(t *testing.T, m ServerMessage) {
if m.SessionID != "s1" || m.WorkflowID != "healthcheck" {
t.Fatalf("session.announced fields: %+v", m)
}
},
},
{
name: "chat.turn",
json: `{"type":"chat.turn","sessionId":"s1","turnId":"t9","role":"ROUTER","content":"hi there","sequence":7,"sessionSequence":2}`,
wantType: TypeChatTurn,
eventBear: true,
check: func(t *testing.T, m ServerMessage) {
if m.Role != "ROUTER" || m.Content != "hi there" || m.TurnID != "t9" {
t.Fatalf("chat.turn fields: %+v", m)
}
},
},
{
name: "stage.started",
json: `{"type":"stage.started","sessionId":"s1","stageId":"write_script","occurredAt":123,"sequence":8,"sessionSequence":3}`,
wantType: TypeStageStarted,
eventBear: true,
check: func(t *testing.T, m ServerMessage) {
if m.StageID != "write_script" {
t.Fatalf("stage.started fields: %+v", m)
}
},
},
{
name: "tool.completed",
json: `{"type":"tool.completed","sessionId":"s1","toolName":"shell","outputSummary":"ok","occurredAt":1,"diff":null,"sequence":9,"sessionSequence":4}`,
wantType: TypeToolCompleted,
eventBear: true,
check: func(t *testing.T, m ServerMessage) {
if m.ToolName != "shell" || m.Summary != "ok" {
t.Fatalf("tool.completed fields: %+v", m)
}
},
},
{
name: "snapshot_complete (non-event control frame)",
json: `{"type":"snapshot_complete"}`,
wantType: TypeSnapshotComplete,
eventBear: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
m, err := Decode([]byte(c.json))
if err != nil {
t.Fatalf("decode failed: %v", err)
}
if m.Type != c.wantType {
t.Fatalf("type = %q, want %q", m.Type, c.wantType)
}
if m.IsEventBearing() != c.eventBear {
t.Fatalf("IsEventBearing = %v, want %v", m.IsEventBearing(), c.eventBear)
}
if c.check != nil {
c.check(t, m)
}
})
}
}