Why: Disabling an account must stop in-flight work as well as new HTTP requests. Evidence: SELF receipts /tmp/iam-u3-u2-runtime-regression.json and /tmp/iam-u3-u2-app-compile.json. Impact: Run start, tools, HITL, streaming output, child runs and final settlement recheck the account independently of project authorization. Refs: IAM U3 r1 stages 4-5; Dev feature/iam-u3-account-directory-recovery
146 lines
5.4 KiB
Go
146 lines
5.4 KiB
Go
package runmgr
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
|
|
"agent-system-backend/internal/store"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
// A missing domain provider must not make a registered account guard optional.
|
|
func TestAccountGuardBlocksRunToolAndQueuedHITL(t *testing.T) {
|
|
c, st := newExecEnv(t)
|
|
if err := st.CreateAccount("alice", "Alice", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.mgr.SetAccountEffectiveAuthorization(func(_ context.Context, owner string) (bool, error) {
|
|
a, err := st.GetAccountByOwner(owner)
|
|
return err == nil && a.Enabled, err
|
|
})
|
|
c.run.ID = store.NewID("run")
|
|
c.run.Status = core.RunWaitingHITL
|
|
if err := st.CreateRun(&c.run); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := &runHandle{hitlCh: make(chan hitlAnswer, 1)}
|
|
c.mgr.active = map[string]*runHandle{c.run.ID: h}
|
|
card := core.HITLCard{ID: "disabled-card", RunID: c.run.ID, ConversationID: c.run.ConversationID}
|
|
if err := st.SaveHITLCard("alice", &card); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.DB().Exec(`UPDATE users SET enabled=FALSE WHERE owner='alice'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conv, err := st.GetConversation("alice", c.run.ConversationID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := c.mgr.StartConversationRun(conv, core.Message{}, nil, nil); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("disabled account started run without domain provider: %v", err)
|
|
}
|
|
res := c.ExecuteTool(core.ToolCallRequest{CallID: "disabled-tool", Name: "write_artifact", Arguments: json.RawMessage(`{"name":"denied.txt","content":"x"}`)})
|
|
if res.Status != "error" {
|
|
t.Fatalf("disabled account executed tool: %+v", res)
|
|
}
|
|
arts, err := st.ListArtifacts("alice", c.run.ProjectID)
|
|
if err != nil || len(arts) != 0 {
|
|
t.Fatalf("disabled account wrote artifact: %+v %v", arts, err)
|
|
}
|
|
|
|
if _, _, err := c.mgr.ResolveHITL("alice", card.ID, core.HITLResponse{ChoiceID: "yes"}); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("disabled account resolved HITL: %v", err)
|
|
}
|
|
persisted, err := st.GetHITLCard("alice", card.ID)
|
|
if err != nil || persisted.Resolved || len(h.hitlCh) != 0 {
|
|
t.Fatalf("disabled HITL changed state: %+v channel=%d err=%v", persisted, len(h.hitlCh), err)
|
|
}
|
|
payload, _ := json.Marshal(HITLCommandPayload{Owner: "alice", CardID: card.ID, Kind: "yes"})
|
|
c.mgr.applyHITLCommand(ownedCommand{CommandID: "queued-disabled", Payload: string(payload)})
|
|
if len(h.hitlCh) != 0 {
|
|
t.Fatal("queued approval delivered to disabled run")
|
|
}
|
|
}
|
|
|
|
func TestAccountGuardStoreReadFailureBlocksOutput(t *testing.T) {
|
|
c, st := newExecEnv(t)
|
|
runCtx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
c.ctx = runCtx
|
|
c.mgr.active = map[string]*runHandle{"active": {owner: "alice", cancel: cancel}}
|
|
readErr := errors.New("account store unavailable")
|
|
c.mgr.SetAccountEffectiveAuthorization(func(context.Context, string) (bool, error) { return false, readErr })
|
|
if err := c.Emit(core.EvToolOutput, core.ToolOutputPayload{Text: "secret"}); !errors.Is(err, readErr) {
|
|
t.Fatalf("store failure emitted output: %v", err)
|
|
}
|
|
if runCtx.Err() == nil {
|
|
t.Fatal("account-state read failure left active local run executing")
|
|
}
|
|
if err := c.SetAssistantParts([]core.Part{{Type: core.PartText, Text: "secret"}}); !errors.Is(err, readErr) {
|
|
t.Fatalf("store failure retained assistant output: %v", err)
|
|
}
|
|
evs, err := st.EventsAfter(c.run.ConversationID, 0, 100)
|
|
if err != nil || len(evs) != 0 {
|
|
t.Fatalf("store failure persisted output events: %+v %v", evs, err)
|
|
}
|
|
}
|
|
|
|
func TestAccountGuardBlocksRestoredNoticeOutput(t *testing.T) {
|
|
c, st := newExecEnv(t)
|
|
if err := st.CreateAccount("alice", "Alice", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.mgr.SetAccountEffectiveAuthorization(func(_ context.Context, owner string) (bool, error) {
|
|
a, err := st.GetAccountByOwner(owner)
|
|
return err == nil && a.Enabled, err
|
|
})
|
|
if _, err := st.DB().Exec(`UPDATE users SET enabled=FALSE WHERE owner='alice'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.mgr.startNoticeRun(c.run.ConversationID, []bgNotice{{owner: "alice", name: "restored", payload: core.AgentResultPayload{Summary: "must not deliver"}}}); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("restored notice delivered to disabled account: %v", err)
|
|
}
|
|
msgs, err := st.ListMessages(c.run.ConversationID)
|
|
if err != nil || len(msgs) != 0 {
|
|
t.Fatalf("restored notice persisted output: %+v %v", msgs, err)
|
|
}
|
|
}
|
|
|
|
func TestAccountGuardDiscardsComposedOutputAfterDisable(t *testing.T) {
|
|
c, st := newExecEnv(t)
|
|
if err := st.CreateAccount("alice", "Alice", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.mgr.SetAccountEffectiveAuthorization(func(_ context.Context, owner string) (bool, error) {
|
|
a, err := st.GetAccountByOwner(owner)
|
|
return err == nil && a.Enabled, err
|
|
})
|
|
c.run.ID = store.NewID("run")
|
|
c.run.Status = core.RunRunning
|
|
if err := st.CreateRun(&c.run); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.h = &runHandle{assistant: []core.Part{{Type: core.PartText, Text: "must not persist"}}}
|
|
if _, err := st.DB().Exec(`UPDATE users SET enabled=FALSE WHERE owner='alice'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.mgr.finishRun(c, core.RunCompleted, core.Usage{}, nil)
|
|
got, err := st.GetRunByID(c.run.ID)
|
|
if err != nil || got.Status == core.RunCompleted {
|
|
t.Fatalf("disabled run completed: %+v %v", got, err)
|
|
}
|
|
msgs, err := st.ListMessages(c.run.ConversationID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, msg := range msgs {
|
|
for _, part := range msg.Parts {
|
|
if part.Text == "must not persist" {
|
|
t.Fatal("previously composed assistant output persisted after disable")
|
|
}
|
|
}
|
|
}
|
|
}
|