Append awaiting/step/completed envelopes in the command UoW and project them without changing dispatcher leases. Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
3.3 KiB
Go
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)
|
|
}
|
|
}
|