Files
correx/apps/tui-go/internal/protocol/golden_test.go
T
kami 3600ec6897 feat(tui): surface plane-2 rationale in the approval band
The plane-2 tool-call assessor records verified preconditions
("[PATH_OUTSIDE_WORKSPACE] …") and the server already ships them in
RiskSummaryDto.rationale, but the Go TUI's RiskSummaryDto had no
Rationale field — so the justification was silently dropped at decode
and the gate showed only an opaque tier.

Decode the rationale, carry it on Approval, and render it under the
header in the approval band (warn-marked, above the diff). This closes
the last deferred item of the plane-2 slice-1 plan: the assessment
surfaced to the approval UX. Golden test pins the rationale decode.
2026-06-03 00:30:26 +04:00

103 lines
3.6 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)
}
},
},
{
// Plane-2 rationale (verified preconditions) must survive decode — it is the
// justification shown in the approval band, not the opaque tier.
name: "approval.required carries plane-2 rationale",
json: `{"type":"approval.required","sessionId":"s1","requestId":"req-1","tier":"T3","riskSummary":{"level":"MEDIUM","factors":[],"recommendedAction":"PROMPT_USER","rationale":["[PATH_OUTSIDE_WORKSPACE] outside: /tmp/x"]},"toolName":"file_write","preview":"--- a/x\n+++ b/x","sequence":12,"sessionSequence":3}`,
wantType: TypeApprovalRequired,
eventBear: true,
check: func(t *testing.T, m ServerMessage) {
if m.RiskSummary == nil {
t.Fatalf("approval.required: riskSummary decoded nil")
}
if len(m.RiskSummary.Rationale) != 1 || m.RiskSummary.Rationale[0] != "[PATH_OUTSIDE_WORKSPACE] outside: /tmp/x" {
t.Fatalf("rationale not decoded: %+v", m.RiskSummary)
}
},
},
{
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)
}
})
}
}