package counterfactual import ( "encoding/json" "errors" "testing" "time" ) func TestDeliverableJSONRoundtrip(t *testing.T) { original := &Deliverable{ HiddenAssumptions: []string{"assumes single-tenant", "assumes UTC clock"}, FailureScenarios: []string{"clock skew breaks ordering"}, Verdict: VerdictBWins, VerdictReason: "alternative tolerates clock skew", ToolName: "DatabaseWrite", Step: StepReverse, DecisionID: "rec-42", OccurredAt: time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC), } raw, err := json.Marshal(original) if err != nil { t.Fatalf("Marshal: %v", err) } var roundtripped Deliverable if err := json.Unmarshal(raw, &roundtripped); err != nil { t.Fatalf("Unmarshal: %v", err) } if roundtripped.Verdict != original.Verdict { t.Errorf("Verdict mismatch: got %q want %q", roundtripped.Verdict, original.Verdict) } if roundtripped.VerdictReason != original.VerdictReason { t.Errorf("VerdictReason mismatch") } if len(roundtripped.HiddenAssumptions) != 2 { t.Errorf("HiddenAssumptions length: got %d want 2", len(roundtripped.HiddenAssumptions)) } if len(roundtripped.FailureScenarios) != 1 { t.Errorf("FailureScenarios length: got %d want 1", len(roundtripped.FailureScenarios)) } if !roundtripped.OccurredAt.Equal(original.OccurredAt) { t.Errorf("OccurredAt mismatch") } } func TestDeliverableJSONRespectsSkillSchema(t *testing.T) { // Raw MiniMax response shape per ~/.claude/skills/reverse-think/SKILL.md // must unmarshal cleanly into the four core fields. Metadata fields // (tool_name / step / decision_id / occurred_at) are absent in raw // response and stay zero -- consumer wraps them after. // // 原始 MiniMax 响应 (见 SKILL.md prompt 模板) 必须能干净 unmarshal 入 // 4 个核心字段. 元数据字段在原始响应中缺失保持零值, 消费方包装时再填. raw := []byte(`{ "hidden_assumptions": ["assumes mutex is uncontended"], "failure_scenarios": ["high contention deadlocks Stage()"], "verdict": "depends_on_X", "verdict_reason": "depends on observed contention rate" }`) var d Deliverable if err := json.Unmarshal(raw, &d); err != nil { t.Fatalf("Unmarshal raw skill response: %v", err) } if d.Verdict != VerdictDepends { t.Errorf("Verdict: got %q want %q", d.Verdict, VerdictDepends) } if d.ToolName != "" { t.Errorf("ToolName should be zero in raw response, got %q", d.ToolName) } if !d.OccurredAt.IsZero() { t.Errorf("OccurredAt should be zero in raw response") } } func TestDeliverableValidate(t *testing.T) { stamp := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC) tests := []struct { name string d Deliverable wantErr error }{ { name: "valid minimum", d: Deliverable{ Verdict: VerdictAHolds, VerdictReason: "no concerns surfaced", OccurredAt: stamp, }, wantErr: nil, }, { name: "empty verdict", d: Deliverable{ VerdictReason: "x", OccurredAt: stamp, }, wantErr: ErrVerdictRequired, }, { name: "empty reason", d: Deliverable{ Verdict: VerdictAHolds, OccurredAt: stamp, }, wantErr: ErrVerdictReasonRequired, }, { name: "zero timestamp", d: Deliverable{ Verdict: VerdictAHolds, VerdictReason: "x", }, wantErr: ErrOccurredAtZero, }, { name: "unknown verdict allowed (forward compat)", d: Deliverable{ Verdict: Verdict("future_label"), VerdictReason: "x", OccurredAt: stamp, }, wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.d.Validate() if !errors.Is(err, tt.wantErr) { t.Errorf("Validate: got err=%v want %v", err, tt.wantErr) } }) } } func TestDeliverableClone(t *testing.T) { original := &Deliverable{ HiddenAssumptions: []string{"a", "b"}, FailureScenarios: []string{"x"}, Verdict: VerdictAHolds, VerdictReason: "fine", OccurredAt: time.Now().UTC(), } clone := original.Clone() clone.HiddenAssumptions[0] = "MUTATED" if original.HiddenAssumptions[0] == "MUTATED" { t.Errorf("Clone shares slice with original") } clone.Verdict = VerdictBWins if original.Verdict == VerdictBWins { t.Errorf("Clone shares value semantics broken") } } func TestCloneNilSafe(t *testing.T) { var d *Deliverable if d.Clone() != nil { t.Errorf("Clone of nil should return nil") } } func TestMetadataKey(t *testing.T) { // Lock the canonical key; cross-package consumers depend on this // literal not changing without coordinated update. // // 锁定规范 key; 跨包消费方依赖本字面量, 协调升级前不应改. if MetadataKey != "flyto.counterfactual.deliverable" { t.Errorf("MetadataKey changed unexpectedly: got %q", MetadataKey) } }