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
63 lines
2.1 KiB
Go
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)
|
|
}
|
|
}
|