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 TestVerdictKind(t *testing.T) { tests := []struct { name string verdict Verdict want VerdictKind }{ {"A_holds exact", VerdictAHolds, VerdictKindAHolds}, {"B_wins exact", VerdictBWins, VerdictKindBWins}, {"depends placeholder", VerdictDepends, VerdictKindDepends}, {"depends with named dimension", Verdict("depends_on_engine_requirements"), VerdictKindDepends}, {"depends bare prefix", Verdict("depends_on_"), VerdictKindDepends}, {"unknown forward-compat label", Verdict("future_label"), VerdictKindUnknown}, {"empty", Verdict(""), VerdictKindUnknown}, // "depends_on" without the trailing underscore is NOT the family -- // guards against a sloppy Contains-based match. // 不带尾下划线的 "depends_on" 不属于该族 -- 防 Contains 式松散匹配. {"near-miss no underscore", Verdict("depends_on"), VerdictKindUnknown}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.verdict.Kind(); got != tt.want { t.Errorf("Kind(%q) = %v, want %v", tt.verdict, got, tt.want) } }) } } func TestVerdictDependsDimension(t *testing.T) { tests := []struct { name string verdict Verdict want string }{ // The real-MiniMax case that motivated this API: the model named the // deciding dimension instead of echoing the placeholder X. // 催生本 API 的真 MiniMax 案例: 模型指明了决定性维度而非回显占位 X. {"named dimension", Verdict("depends_on_engine_requirements"), "engine_requirements"}, {"bare placeholder yields X", VerdictDepends, "X"}, {"bare prefix yields empty", Verdict("depends_on_"), ""}, {"non-depends yields empty", VerdictAHolds, ""}, {"unknown yields empty", Verdict("future_label"), ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.verdict.DependsDimension(); got != tt.want { t.Errorf("DependsDimension(%q) = %q, want %q", tt.verdict, got, tt.want) } }) } } 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) } }