3600ec6897
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.
103 lines
3.6 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|