Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes. Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending. Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete. Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
110 lines
3.6 KiB
Go
110 lines
3.6 KiB
Go
package cardsvc
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"agent-system-backend/internal/store"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
func TestAccountCheckedCardActionsUseActorEventSinkAndRespectDisable(t *testing.T) {
|
|
st, err := store.OpenSQLite(t.TempDir() + "/cards.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
if err := st.CreateAccount("admin", "", "", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.CreateAccount("alice", "", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
project, err := st.CreateProject("alice", "cards")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conv, err := st.CreateConversation("alice", project.ID, "cards", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
legacyCalls := 0
|
|
svc := New(st, func(string, string, core.StreamEventType, any) error {
|
|
legacyCalls++
|
|
return nil
|
|
})
|
|
type emitted struct {
|
|
actor, channel string
|
|
typ core.StreamEventType
|
|
}
|
|
var guarded []emitted
|
|
svc.SetAccountCheckedEmitter(func(actor, channel, runID string, typ core.StreamEventType, payload any) error {
|
|
guarded = append(guarded, emitted{actor, channel, typ})
|
|
return nil
|
|
})
|
|
base := core.ActivityCard{ProjectID: project.ID, ConversationID: conv.ID, Title: "Action",
|
|
Status: core.CardActionRequired, Actions: []core.UIAction{{ID: "yes", Label: "Approve"}}}
|
|
action, err := svc.CreateAccountChecked("alice", base, "", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, label, err := svc.ActionAccountChecked("alice", action.ID, "yes", "", false); err != nil || label != "Approve" {
|
|
t.Fatalf("guarded action: label=%q err=%v", label, err)
|
|
}
|
|
pending, err := svc.CreateAccountChecked("alice", base, "", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pin, err := svc.CreateAccountChecked("alice", core.ActivityCard{ProjectID: project.ID, ConversationID: conv.ID, Title: "Pin"}, "", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := svc.SetPinnedAccountChecked("alice", pin.ID, true, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := svc.DismissAccountChecked("alice", pin.ID, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
archive, err := svc.CreateAccountChecked("alice", core.ActivityCard{ProjectID: project.ID, ConversationID: conv.ID, Title: "Archive"}, "", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
archived, err := svc.ToProjectAccountChecked("alice", archive.ID, false)
|
|
if err != nil || archived.ConversationID != "" {
|
|
t.Fatalf("guarded archive: %+v %v", archived, err)
|
|
}
|
|
if legacyCalls != 0 {
|
|
t.Fatalf("guarded card operations used unguarded emitter %d times", legacyCalls)
|
|
}
|
|
if len(guarded) == 0 {
|
|
t.Fatal("guarded operations published no events")
|
|
}
|
|
for _, event := range guarded {
|
|
if event.actor != "alice" {
|
|
t.Fatalf("event actor lost: %+v", event)
|
|
}
|
|
}
|
|
before := len(guarded)
|
|
account, err := st.GetAccountByOwner("alice")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.SetAccountEnabled("admin", account.ID, account.StateVersion, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, mutate := range []func() error{
|
|
func() error { _, err := svc.CreateAccountChecked("alice", base, "", false); return err },
|
|
func() error { _, _, err := svc.ActionAccountChecked("alice", pending.ID, "yes", "", false); return err },
|
|
func() error { _, err := svc.SetPinnedAccountChecked("alice", pin.ID, false, false); return err },
|
|
func() error { _, err := svc.DismissAccountChecked("alice", pin.ID, false); return err },
|
|
func() error { _, err := svc.ToProjectAccountChecked("alice", archive.ID, false); return err },
|
|
} {
|
|
if err := mutate(); !errors.Is(err, store.ErrAccountForbidden) {
|
|
t.Fatalf("disabled actor mutated card: %v", err)
|
|
}
|
|
}
|
|
if len(guarded) != before {
|
|
t.Fatalf("disabled actor published %d events", len(guarded)-before)
|
|
}
|
|
}
|