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
64 lines
2.7 KiB
Go
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", ¤t, ""); 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")
|
|
}
|
|
}
|