Files
weichen adcd8fefd2 Fence U3 account writes and stabilize directory cursors
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.
2026-10-14 07:33:54 +00:00

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