package intentflow_test import ( "context" "encoding/json" "errors" "os" "path/filepath" "runtime" "testing" "chat-agent-system/internal/app/intentflow" "chat-agent-system/internal/events" ) func TestCreatePlanEmitsAwaitingInput(t *testing.T) { sink := &intentflow.MemorySink{} proj := intentflow.NewProjector() now, opts, _ := testClock() opts.Events = sink p := intentflow.NewPlans(&intentflow.SerialUoW{}, intentflow.NewMemoryStore(), opts) ctx := context.Background() view, err := p.CreatePlan(ctx, draft("digest-a")) if err != nil { t.Fatal(err) } envs := sink.All() if len(envs) != 1 || envs[0].EventName != events.EventIntentPlanAwaiting { t.Fatalf("want one awaiting_input, got %#v", envs) } if envs[0].Producer != "app/intentflow" { t.Fatalf("producer %s", envs[0].Producer) } var payload map[string]any if err := json.Unmarshal(envs[0].Payload, &payload); err != nil { t.Fatal(err) } for _, bad := range []string{"prompt", "slots", "permissions", "result", "input"} { if _, ok := payload[bad]; ok { t.Fatalf("sensitive field %s leaked", bad) } } if err := proj.ApplyEnvelope(envs[0]); err != nil { t.Fatal(err) } if err := proj.ApplyEnvelope(envs[0]); err != nil { t.Fatal(err) } sh, ok := proj.VisibleShadow(principal(), view.PlanID) if !ok || sh.Revision != 1 { t.Fatalf("shadow %+v ok=%v", sh, ok) } other := principal() other.UserID = 99 if _, ok := proj.VisibleShadow(other, view.PlanID); ok { t.Fatal("cross-user shadow must be hidden") } got, err := p.GetPlan(ctx, principal(), view.PlanID) if err != nil || got == nil { t.Fatalf("SSOT GET must work without card success: %v", err) } _ = now } func TestProjectionOnlyForwardAndForgedRejected(t *testing.T) { proj := intentflow.NewProjector() newer := events.Envelope{ EventID: "e2", EventName: events.EventIntentPlanAwaiting, SchemaVersion: events.EnvelopeSchema, Environment: "dev", ProjectID: 42, AggregateType: "intent_plan", AggregateID: "pln_x", AggregateVersion: 2, Producer: "app/intentflow", Payload: []byte(`{"plan_id":"pln_x","revision":2,"status":"executing","environment":"dev","project_id":42,"user_id":7,"session_id":"sess-u4","summary_ref":""}`), } newer.PayloadDigest = events.Digest(newer.Payload) older := newer older.EventID = "e1" older.AggregateVersion = 1 older.Payload = []byte(`{"plan_id":"pln_x","revision":1,"status":"awaiting_confirmation","environment":"dev","project_id":42,"user_id":7,"session_id":"sess-u4","summary_ref":""}`) older.PayloadDigest = events.Digest(older.Payload) if err := proj.ApplyEnvelope(newer); err != nil { t.Fatal(err) } if err := proj.ApplyEnvelope(older); err != nil { t.Fatal(err) } sh, _ := proj.Shadow("pln_x") if sh.Revision != 2 || sh.Status != "executing" { t.Fatalf("low revision must not overwrite: %+v", sh) } forged := newer forged.EventID = "e3" forged.Producer = "forged" forged.PayloadDigest = events.Digest(forged.Payload) if err := proj.ApplyEnvelope(forged); !errors.Is(err, events.ErrIntegrityFailure) { t.Fatalf("forged producer: %v", err) } } func TestAwaitingPayloadGolden(t *testing.T) { _, file, _, _ := runtime.Caller(0) raw, err := os.ReadFile(filepath.Join(filepath.Dir(file), "testdata", "intent-plan-awaiting-v1.json")) if err != nil { t.Fatal(err) } if err := intentflow.AssertPlanPayload(events.EventIntentPlanAwaiting, raw); err != nil { t.Fatal(err) } }