Files
weichenandCursor c36d891b05 feat(intentflow): emit plan facts onto Track E outbox
Append awaiting/step/completed envelopes in the command UoW and project them without changing dispatcher leases.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 00:46:09 +00:00

107 lines
3.3 KiB
Go

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)
}
}