Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ai/audit_failclosed_test.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/ai/audit_failclosed_test.go |
| Original SHA-256 | f107f403b91b122dee3876a29e46c44790efedf3d62afc00dccd55a6b95b8013 |
| Git blob SHA | 9beba34a0155bad55e83129bc5a10a74426f2687 |
| Reading-copy SHA-256 | f107f403b91b122dee3876a29e46c44790efedf3d62afc00dccd55a6b95b8013 |
Every recorded occurrence
These 2 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/ai/audit_failclosed_test.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/ai/audit_failclosed_test.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓97 lines · 4.2 KB
package ai
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/missionworkforce/missionmatch/internal/platform/audit"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// failingAuditOrchestrator builds an orchestrator whose audit write is guaranteed to fail: it has an audit
// writer and a tenant DB, so the audit is CONFIGURED and expected to happen, but the DB carries no pool, so
// RunInTenantTx cannot open a transaction. That is the shape of a real audit outage (the pool is gone, the
// audit_logs insert is rejected), and it is the situation where the old code returned an answer anyway.
func failingAuditOrchestrator() *Orchestrator {
db := &tenant.DB{}
return NewOrchestrator(db, nil, audit.New(db))
}
// orgCtx supplies a validated tenant, so the failure under test is the audit write and not the missing
// tenant guard in front of it.
func orgCtx() context.Context {
return tenant.WithOrg(context.Background(), "11111111-1111-1111-1111-111111111111")
}
// TestAnswerIsNotServedWhenTheAEDTAuditCannotBeWritten is the regression this fix exists for. Every AI route
// records its AEDT event through Orchestrator.audit, which used to discard the write error, so an answer
// whose audit row was never written was served regardless. internal/platform/audit and
// matching.auditOnTx both state the opposite contract: a decision that cannot be recorded is not served.
func TestAnswerIsNotServedWhenTheAEDTAuditCannotBeWritten(t *testing.T) {
o := failingAuditOrchestrator()
resp, err := o.finalize(orgCtx(), RouteDirect, "", ErrNoModel, "the deterministic answer", []string{"evidence"})
if err == nil {
t.Fatal("an answer whose AEDT audit could not be written must not be served")
}
if resp != nil {
t.Fatalf("no response may be handed back when the decision was not recorded, got %+v", resp)
}
if !errors.Is(err, ErrAuditFailed) {
t.Fatalf("the failure must be the typed ErrAuditFailed so the handler can answer 503, got %v", err)
}
}
// TestRefusalIsNotServedWhenTheAEDTAuditCannotBeWritten covers the deliberate half of the choice. A refusal
// grants nothing either way, so failing it closed leaks nothing; what it buys is that an EEO probe cannot be
// repeated invisibly. Serving a clean refusal while recording none of them is exactly the combination a
// prober wants.
func TestRefusalIsNotServedWhenTheAEDTAuditCannotBeWritten(t *testing.T) {
o := failingAuditOrchestrator()
resp, err := o.refuse(orgCtx(), RouteRefuse, "the question references prohibited demographic data")
if err == nil {
t.Fatal("a refusal whose audit could not be written must surface an error, not a silent refusal")
}
if resp != nil {
t.Fatalf("no response may be handed back when the refusal was not recorded, got %+v", resp)
}
if !errors.Is(err, ErrAuditFailed) {
t.Fatalf("the failure must be the typed ErrAuditFailed, got %v", err)
}
}
// TestUnconfiguredAuditIsNotAFailure pins the one case that must stay a no op. An orchestrator with no audit
// writer or no database is a deliberate configuration (the unit tests, and a deployment with no audit pool
// wired), not a failed write, so answers still flow. Without this the fail closed change would take every
// test orchestrator and every unconfigured deployment offline.
func TestUnconfiguredAuditIsNotAFailure(t *testing.T) {
o := NewOrchestrator(nil, nil, nil)
resp, err := o.refuse(orgCtx(), RouteRefuse, "the question references prohibited demographic data")
if err != nil {
t.Fatalf("an orchestrator with no audit configured must still answer: %v", err)
}
if resp == nil || !resp.Refused {
t.Fatalf("the refusal must still be produced, got %+v", resp)
}
}
// TestAuditFailureMapsTo503 proves the caller sees the right thing: not a 4xx blaming a request that was
// fine, and not a raw internal error, but a service unavailable that invites a retry once the audit path is
// healthy again.
func TestAuditFailureMapsTo503(t *testing.T) {
rec := httptest.NewRecorder()
mapErr(rec, ErrAuditFailed)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("an unrecordable decision must be a 503, got %d", rec.Code)
}
if body := rec.Body.String(); !strings.Contains(body, "audit_unavailable") {
t.Fatalf("the envelope must name the audit as the cause, got %s", body)
}
}