package decision_test import ( "context" "errors" "testing" "github.com/netkingdom/access-engine/internal/decision" "github.com/netkingdom/access-engine/pkg/api" ) // failingRecorder is an outbox whose commit always fails. type failingRecorder struct { appends int batches int uncommitted []api.DecisionEffect } func (r *failingRecorder) Append(api.DecisionEnvelope) error { r.appends++ return errors.New("disk full") } func (r *failingRecorder) AppendBatch([]api.DecisionEnvelope) error { r.batches++ return errors.New("disk full") } func (r *failingRecorder) NoteReleasedUncommitted(effect api.DecisionEffect) { r.uncommitted = append(r.uncommitted, effect) } var ( allowedRead = api.CheckRequest{ Subject: api.SubjectRef{ID: "user:alice"}, Action: "read", Resource: api.ResourceRef{ID: "document:internal-note", System: "markitect-tool"}, } deniedRead = api.CheckRequest{ Subject: api.SubjectRef{ID: "user:alice"}, Action: "read", Resource: api.ResourceRef{ID: "document:missing", Type: "document", System: "markitect-tool"}, } ) // GH-DEC-2026-024: a restriction is not exempt from required commitment. func TestUncommittedDenyIsWithheld(t *testing.T) { engine := newTestEngine(t) recorder := &failingRecorder{} engine.SetDecisionLog(recorder) got, err := engine.Check(context.Background(), deniedRead) if !errors.Is(err, decision.ErrRecordNotCommitted) || got.ID != "" { t.Fatalf("Check = %+v, %v; want unavailable and no decision", got, err) } if len(recorder.uncommitted) != 0 { t.Fatalf("withheld restriction counted as released: %v", recorder.uncommitted) } engine.SetDecisionLog(nil) committed, err := engine.Check(context.Background(), deniedRead) if err != nil || committed.Effect != api.DecisionEffectDeny { t.Fatalf("normal deny = %+v, %v", committed, err) } } // FLEX-DEC-2026-018: authority is never granted without its evidence. func TestUncommittedAllowIsWithheld(t *testing.T) { engine := newTestEngine(t) recorder := &failingRecorder{} engine.SetDecisionLog(recorder) got, err := engine.Check(context.Background(), allowedRead) if !errors.Is(err, decision.ErrRecordNotCommitted) { t.Fatalf("err = %v, decision = %+v; want ErrRecordNotCommitted", err, got) } if len(recorder.uncommitted) != 0 { t.Fatalf("a withheld allow was counted as released: %v", recorder.uncommitted) } } func TestBatchCommitsOnceAndWithholdsOnAnyUncommittedAllow(t *testing.T) { engine := newTestEngine(t) recorder := &failingRecorder{} engine.SetDecisionLog(recorder) _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{ Subject: allowedRead.Subject, Action: "read", Resources: []api.ResourceRef{allowedRead.Resource, deniedRead.Resource}, }) if !errors.Is(err, decision.ErrRecordNotCommitted) { t.Fatalf("err = %v; want ErrRecordNotCommitted for a batch holding an allow", err) } if recorder.batches != 1 || recorder.appends != 0 { t.Fatalf("batches=%d appends=%d; want one batch commit", recorder.batches, recorder.appends) } decisions, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{ Subject: deniedRead.Subject, Action: "read", Resources: []api.ResourceRef{deniedRead.Resource}, }) if !errors.Is(err, decision.ErrRecordNotCommitted) || len(decisions) != 0 { t.Fatalf("restriction-only batch: decisions=%v err=%v; want withheld", decisions, err) } } func TestListAllowedWithholdsWhenAllCandidateDenialsFailCommit(t *testing.T) { engine := newTestEngine(t) recorder := &failingRecorder{} engine.SetDecisionLog(recorder) got, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{ Subject: deniedRead.Subject, Action: "unknown-action", System: "markitect-tool", }) if !errors.Is(err, decision.ErrRecordNotCommitted) || got != nil || recorder.batches != 1 { t.Fatalf("ListAllowed = %v, %v; batches=%d; want unavailable", got, err, recorder.batches) } }