package counterfactual import ( "context" "testing" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/evolve" ) func sampleDeliverable() *Deliverable { return &Deliverable{ HiddenAssumptions: []string{"assumes single tenant"}, FailureScenarios: []string{"fails under multi-tenant"}, Verdict: VerdictBWins, VerdictReason: "alternative survives multi-tenant", ToolName: "DatabaseWrite", Step: StepReverse, DecisionID: "rec-7", OccurredAt: time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC), } } func TestAsReplayEventFieldsPropagate(t *testing.T) { d := sampleDeliverable() ev := d.AsReplayEvent() if !ev.Log.Timestamp.Equal(d.OccurredAt) { t.Errorf("Timestamp: got %v want %v", ev.Log.Timestamp, d.OccurredAt) } if ev.Log.DecisionID != d.DecisionID { t.Errorf("DecisionID: got %q want %q", ev.Log.DecisionID, d.DecisionID) } if ev.Log.Entity != d.ToolName { t.Errorf("Entity should be ToolName: got %q want %q", ev.Log.Entity, d.ToolName) } } func TestAsReplayEventPayloadIsClonedDeliverable(t *testing.T) { d := sampleDeliverable() ev := d.AsReplayEvent() payload, ok := ev.Log.Payload.(*Deliverable) if !ok { t.Fatalf("Payload should be *Deliverable, got %T", ev.Log.Payload) } if payload == d { t.Errorf("Payload should be a clone, not the original pointer") } if payload.Verdict != d.Verdict { t.Errorf("Clone Verdict mismatch: got %q want %q", payload.Verdict, d.Verdict) } // Mutating the payload must not affect the original. // // 修改 payload 不应影响原 Deliverable. payload.HiddenAssumptions[0] = "MUTATED" if d.HiddenAssumptions[0] == "MUTATED" { t.Errorf("Payload mutation leaked into source Deliverable") } } func TestAsReplayEventFeedbackIsNil(t *testing.T) { ev := sampleDeliverable().AsReplayEvent() if ev.Feedback != nil { t.Errorf("Feedback should be nil at reverse-think time, got %+v", ev.Feedback) } } func TestAsReplayEventMetaTags(t *testing.T) { d := sampleDeliverable() ev := d.AsReplayEvent() gotStep, ok := ev.Log.Meta[MetaKeyStep].(string) if !ok || gotStep != d.Step { t.Errorf("Log.Meta[MetaKeyStep]: got %v want %q", ev.Log.Meta[MetaKeyStep], d.Step) } gotVerdict, ok := ev.Log.Meta[MetaKeyVerdict].(string) if !ok || gotVerdict != string(d.Verdict) { t.Errorf("Log.Meta[MetaKeyVerdict]: got %v want %q", ev.Log.Meta[MetaKeyVerdict], d.Verdict) } gotSource, ok := ev.Meta[MetaKeySource].(string) if !ok || gotSource != SourceReverseThink { t.Errorf("Meta[MetaKeySource]: got %v want %q", ev.Meta[MetaKeySource], SourceReverseThink) } } func TestAsReplayEventNilReceiver(t *testing.T) { var d *Deliverable ev := d.AsReplayEvent() // All fields should be zero; the test is that no panic occurs and // the zero ReplayEvent is consistent with evolve's documented zero // semantics (Feedback nil, Log fields zero). // // 所有字段应为零值; 测试是不 panic 且零值 ReplayEvent 与 evolve godoc // 零值语义一致 (Feedback nil, Log 字段零). if ev.Feedback != nil { t.Errorf("nil receiver should return zero ReplayEvent, got Feedback %+v", ev.Feedback) } if ev.Log.DecisionID != "" { t.Errorf("nil receiver Log.DecisionID should be empty, got %q", ev.Log.DecisionID) } if ev.Log.Payload != nil { t.Errorf("nil receiver Log.Payload should be nil, got %v", ev.Log.Payload) } } // captureReflector is a minimal evolve.Reflector implementation used to // prove the adapter's output flows through evolve's interface unmodified. // Records every event for later assertion. // // captureReflector 是最小 evolve.Reflector 实现, 证 adapter 输出经 evolve // 接口未变形传播. 记录每个事件供断言. type captureReflector struct { events []evolve.ReplayEvent } func (c *captureReflector) OnEvent(ctx context.Context, event evolve.ReplayEvent) error { c.events = append(c.events, event) return nil } func TestAsReplayEventConsumableByReflector(t *testing.T) { d := sampleDeliverable() ev := d.AsReplayEvent() cap := &captureReflector{} if err := cap.OnEvent(context.Background(), ev); err != nil { t.Fatalf("Reflector.OnEvent: %v", err) } if len(cap.events) != 1 { t.Fatalf("Reflector should have captured 1 event, got %d", len(cap.events)) } got := cap.events[0] if got.Log.DecisionID != "rec-7" { t.Errorf("captured event lost DecisionID: got %q", got.Log.DecisionID) } }