Files
Agent-System-Backend/internal/store/account_settle_test.go
T
weichen 7229abe50d Fence final run settlement against account disable
Why: A disable could commit after the run manager checked account state but before its final business message was stored.

Evidence: SELF receipts /tmp/iam-u3-final-settle-sqlite.json, /tmp/iam-u3-final-settle-pg.json, /tmp/iam-u3-backend-settle-all.json, /tmp/iam-u3-backend-settle-pg.json, and /tmp/iam-u3-http-two-instance-pg.json.

Impact: The final run transaction locks and rechecks the account, rejects post-disable business output, and permits only an account-denial failure record. Auth-off implicit local ownership remains explicit.

Refs: IAM U3 r1 stages 4-5; Dev feature/iam-u3-account-directory-recovery
2026-10-14 04:48:53 +00:00

63 lines
2.1 KiB
Go

package store
import (
"errors"
"testing"
"time"
core "agent-system-core"
)
func TestAccountCheckedFinalSettlementCannotWriteAfterDisable(t *testing.T) {
st := openTestStore(t)
t.Logf("account final settlement dialect=%s", st.Dialect())
if err := st.CreateAccount("admin", "", "hash", true); err != nil {
t.Fatal(err)
}
if err := st.EnsureUser(OwnerDefault, ""); err != nil {
t.Fatal(err)
}
conv := seedConversation(t, st)
run := seedRun(t, st, conv, core.RunRunning)
local, err := st.GetAccountByOwner(OwnerDefault)
if err != nil {
t.Fatal(err)
}
if _, err := st.SetAccountEnabled("admin", local.ID, local.StateVersion, false); err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
run.Status, run.Phase, run.EndedAt = core.RunCompleted, core.PhaseDone, &now
message := core.Message{ConversationID: conv.ID, RunID: run.ID, Role: core.RoleAssistant,
Parts: []core.Part{{Type: core.PartText, Text: "must not persist after disable"}}}
settle := func() error {
if st.Dialect() == "postgres" {
_, _, err := st.SettleRunFencedAccountChecked(&run, &message, "test-instance", 0, true)
return err
}
_, err := st.SettleRunAccountChecked(&run, &message, true)
return err
}
if err := settle(); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("disabled account completed business output: %v", err)
}
got, err := st.GetRunByID(run.ID)
if err != nil || got.Status != core.RunRunning {
t.Fatalf("failed transaction changed run state: %+v %v", got, err)
}
messages, err := st.ListMessages(conv.ID)
if err != nil || len(messages) != 0 {
t.Fatalf("failed transaction wrote assistant message: %+v %v", messages, err)
}
run.Status = core.RunFailed
run.Error = &core.ErrorPart{Code: "run.account_inactive", Message: "account disabled"}
message.Parts = []core.Part{{Type: core.PartError, Err: run.Error}}
if err := settle(); err != nil {
t.Fatalf("minimal account-denial failure could not settle: %v", err)
}
got, err = st.GetRunByID(run.ID)
if err != nil || got.Status != core.RunFailed {
t.Fatalf("denial state not persisted: %+v %v", got, err)
}
}