Files
Agent-System-Backend/internal/cardsvc/task_projection_test.go
T
weichen dd0c3ce7d0 Persist task projections and replay cursors atomically
Why: Background work needs consistent cards, events, and user reading state.

Evidence: SELF make evidence records SQLite rollback, projection, pin, and regression checks; PostgreSQL replay is pending the composition gate.

Impact: Adds an optional atomic task path and a consistent card replay cursor without schema changes.

Refs: Agent-System-Dev fast-slow-u2-u3 plan Task 3
2026-10-13 01:31:46 +00:00

64 lines
2.7 KiB
Go

package cardsvc
import (
core "agent-system-core"
"testing"
)
func TestTaskProjectionPreservesUserReadingPreferences(t *testing.T) {
st, svc, _ := openTest(t)
c := core.ActivityCard{ID: "card-task", ProjectID: "p1", ConversationID: "c1", Title: "任务", Kind: core.CardKindProgress}
c.Task = &core.TaskSnapshot{SchemaVersion: 1, TaskID: "t1", AttemptID: "a1", CardID: c.ID, ProjectID: "p1", ConversationID: "c1", OwnerInstanceID: "i1", StreamScope: "conversation:c1", Revision: 1, ExecutionState: "queued", OutcomeCompleteness: "none", DeliveryState: "pending", Title: "任务"}
if _, e := svc.CreateTask("o", c, ""); e != nil {
t.Fatal(e)
}
current, e := st.GetCard("o", c.ID)
if e != nil {
t.Fatal("task card not saved", e)
}
current.Pinned = true
current.Status = core.CardDismissed
if e = st.SaveCard("o", &current, ""); e != nil {
t.Fatal(e)
}
next := *current.Task
next.Revision = 2
next.ExecutionState = "completed"
next.Conclusion = "有用成果"
if e = svc.CommitTask("o", next, 1); e != nil {
t.Fatal(e)
}
got, e := st.GetCard("o", c.ID)
if e != nil || !got.Pinned || got.Status != core.CardDismissed || got.Task.ExecutionState != "completed" {
t.Fatal("work update stole user state", got, e)
}
}
func TestTaskProjectionUserPinHasAtomicEventWatermark(t *testing.T) {
st, svc, _ := openTest(t)
c := core.ActivityCard{ID: "card-pin", ProjectID: "p1", ConversationID: "c1", Title: "任务", Kind: core.CardKindProgress}
c.Task = &core.TaskSnapshot{SchemaVersion: 1, TaskID: "t1", AttemptID: "a1", CardID: c.ID, ProjectID: "p1", ConversationID: "c1", OwnerInstanceID: "i1", StreamScope: "conversation:c1", Revision: 1, ExecutionState: "queued", OutcomeCompleteness: "none", DeliveryState: "pending", Title: "任务"}
if _, e := svc.CreateTask("o", c, ""); e != nil {
t.Fatal(e)
}
if _, e := svc.SetPinned("o", c.ID, true); e != nil {
t.Fatal(e)
}
snapshot, e := st.CardsAtCursor("o", "c1")
if e != nil || snapshot.ReplaySeq != 2 || len(snapshot.Cards) != 1 || !snapshot.Cards[0].Pinned {
t.Fatal("pin not atomically replayable", snapshot, e)
}
}
func TestTaskProjectionCannotCreateSecondExecutionOwner(t *testing.T) {
_, svc, _ := openTest(t)
c := core.ActivityCard{ID: "card-archive", ProjectID: "p1", ConversationID: "c1", Title: "任务", Kind: core.CardKindProgress}
c.Task = &core.TaskSnapshot{SchemaVersion: 1, TaskID: "t1", AttemptID: "a1", CardID: c.ID, ProjectID: "p1", ConversationID: "c1", OwnerInstanceID: "i1", StreamScope: "conversation:c1", Revision: 1, ExecutionState: "queued", OutcomeCompleteness: "none", DeliveryState: "pending", Title: "任务"}
if _, e := svc.CreateTask("o", c, ""); e != nil {
t.Fatal(e)
}
if _, e := svc.ToProject("o", c.ID); e == nil {
t.Fatal("task was copied with a second execution owner")
}
}