package ai import ( "errors" "os" "path/filepath" "strings" "testing" ) func TestForbidEEORejectsDemographics(t *testing.T) { bad := []string{ "SELECT * FROM eeo.candidate_demographics", "select gender, race_ethnicity from eeo.candidate_demographics", "join EEO.candidate_demographics on true", "the candidate's veteran_status is", } for _, s := range bad { if err := ForbidEEO(s); !errors.Is(err, ErrEEOForbidden) { t.Errorf("ForbidEEO(%q) must refuse the eeo schema, got %v", s, err) } } if err := ForbidEEO("SELECT full_name FROM candidates WHERE clearance_level='secret'"); err != nil { t.Errorf("a clean query must pass: %v", err) } } func TestGuardReadOnlyQuery(t *testing.T) { bad := map[string]string{ "insert": "INSERT INTO candidates (full_name) VALUES ('x')", "update": "UPDATE candidates SET available=true", "delete": "DELETE FROM candidates", "stacked": "SELECT 1 FROM candidates; DROP TABLE candidates", "eeo read": "SELECT gender FROM eeo.candidate_demographics", "hidden write in cte": "WITH x AS (DELETE FROM candidates RETURNING id) SELECT * FROM x", "non allowlisted": "SELECT * FROM auth_credentials", } for name, sql := range bad { if err := GuardReadOnlyQuery(sql, ReadAllowlist); err == nil { t.Errorf("GuardReadOnlyQuery must refuse %q", name) } } ok := []string{ "SELECT full_name, clearance_level FROM candidates WHERE available = true", "WITH ranked AS (SELECT id FROM slate_members) SELECT count(*) FROM ranked", "SELECT c.full_name FROM candidates c JOIN candidate_skills cs ON cs.candidate_id = c.id", } for _, sql := range ok { if err := GuardReadOnlyQuery(sql, ReadAllowlist); err != nil { t.Errorf("a clean allowlisted read must pass: %q: %v", sql, err) } } } // TestGuardReadOnlyQueryRecursiveCTE is the load bearing security test for the WITH RECURSIVE guard fix. // It proves the guard now ACCEPTS a legitimate recursive traversal whose base tables are all allowlisted // (the recursive self reference is recognized as a query local CTE, not a missing table) and STILL REJECTS // every dangerous shape: a non allowlisted base table, a hidden mutation, an eeo marker, and a stray // stacked statement. A regression here would either break every future graph traversal or, worse, open a // hole in the read only and eeo guarantees. func TestGuardReadOnlyQueryRecursiveCTE(t *testing.T) { // A recursive closure whose only base table is allowlisted (skill_relationships) PASSES. The closure // name is recognized as a CTE, so the self reference in the recursive arm is not flagged as a table. okRecursive := []string{ "WITH RECURSIVE closure AS (" + "SELECT source_id, target_id, 1 AS depth FROM skill_relationships WHERE source_id = $1 " + "UNION " + "SELECT sr.source_id, sr.target_id, c.depth + 1 FROM skill_relationships sr JOIN closure c ON sr.source_id = c.target_id WHERE c.depth < 3" + ") SELECT target_id, depth FROM closure ORDER BY depth, target_id", // A multi CTE recursive query: BOTH a and b must register as CTEs, neither treated as a table. "WITH RECURSIVE a AS (" + "SELECT source_id, target_id FROM skill_relationships WHERE source_id = $1 " + "UNION " + "SELECT sr.source_id, sr.target_id FROM skill_relationships sr JOIN a ON sr.source_id = a.target_id" + "), b AS (SELECT target_id FROM skill_aliases) " + "SELECT a.target_id FROM a JOIN b ON a.target_id = b.target_id ORDER BY a.target_id", } for _, sql := range okRecursive { if err := GuardReadOnlyQuery(sql, ReadAllowlist); err != nil { t.Errorf("a clean recursive allowlisted read must pass: %q: %v", sql, err) } } // Every dangerous recursive shape STILL FAILS. badRecursive := map[string]string{ // Base table not in the allowlist: candidate_demographics is also an eeo marker, so this is caught // twice over, but the table not in allowlist path must independently reject an unlisted name too. "recursive eeo base table": "WITH RECURSIVE closure AS (" + "SELECT id FROM candidate_demographics WHERE id = $1 " + "UNION SELECT cd.id FROM candidate_demographics cd JOIN closure c ON cd.id = c.id) SELECT id FROM closure", "recursive unlisted base table": "WITH RECURSIVE closure AS (" + "SELECT id FROM some_unlisted_table WHERE id = $1 " + "UNION SELECT t.id FROM some_unlisted_table t JOIN closure c ON t.id = c.id) SELECT id FROM closure", // A mutation hidden inside the recursive CTE body must be caught by mutatingRe. "recursive hidden delete": "WITH RECURSIVE x AS (DELETE FROM candidates RETURNING id) SELECT id FROM x", "recursive hidden insert": "WITH RECURSIVE x AS (INSERT INTO candidates (full_name) VALUES ('z') RETURNING id) SELECT id FROM x", "recursive hidden update": "WITH RECURSIVE x AS (UPDATE candidates SET available = true RETURNING id) SELECT id FROM x", // An eeo marker anywhere in the recursive body must be caught by ForbidEEO. "recursive eeo marker": "WITH RECURSIVE closure AS (" + "SELECT id, veteran_status FROM candidates WHERE id = $1 " + "UNION SELECT c.id, c.veteran_status FROM candidates c JOIN closure cl ON c.id = cl.id) SELECT id FROM closure", // A stray semicolon (stacked statements) must still fail even with a recursive lead. "recursive stacked": "WITH RECURSIVE closure AS (SELECT id FROM skill_relationships WHERE id = $1) SELECT id FROM closure; DROP TABLE candidates", } for name, sql := range badRecursive { if err := GuardReadOnlyQuery(sql, ReadAllowlist); err == nil { t.Errorf("GuardReadOnlyQuery must refuse %q", name) } } // The multi CTE registration is asserted directly against the helper too: both a and b are recognized. ctes := cteNames(strings.ToLower( "with recursive a as (select 1), b as (select 2) select * from a join b on true")) if !ctes["a"] || !ctes["b"] { t.Errorf("a multi CTE WITH RECURSIVE must register both a and b as CTEs, got %v", ctes) } } // TestReadAllowlistComposition pins the security boundary of the read allowlist: the six tables added for // the graph layer must be present, and no eeo, auth secret, or audit table may ever appear. A future edit // that widens the allowlist into a protected surface fails here. func TestReadAllowlistComposition(t *testing.T) { mustHave := []string{ "skill_relationships", "skill_aliases", "ontology_crosswalk", "mission_areas", "federal_mission_tags", "award_incumbent_roles", } for _, tbl := range mustHave { if !ReadAllowlist[tbl] { t.Errorf("ReadAllowlist must contain the graph reference table %q", tbl) } } mustNotHave := []string{ "eeo", "candidate_demographics", "auth_credentials", "auth_sessions", "mfa_secrets", "audit_logs", "*", } for _, tbl := range mustNotHave { if ReadAllowlist[tbl] { t.Errorf("ReadAllowlist must NEVER contain the protected table %q", tbl) } } } // TestRegistryIsSafe asserts every curated TAG intent is read only, allowlisted, and eeo free, so an // unsafe intent cannot ship even if added by mistake. func TestRegistryIsSafe(t *testing.T) { if err := ValidateRegistry(); err != nil { t.Fatalf("a TAG intent is unsafe: %v", err) } if len(Registry) == 0 { t.Fatal("the TAG registry is empty; the safety check is a no op") } } // TestNoAIFileReferencesEEO is the required tripwire: it walks every Go file in the AI package, the W3 // staffing recommendations package, and the WS9 bench ingestion surface (the benchimport mapping, the // sourcing ingest path, and the import-bench CLI), and fails if any of them, other than the prohibition // guard (safety.go) and the tests, names the eeo schema or a demographic column. No embedding, retrieval, // graph, memory, generated query, prompt builder, classifier, optimizer, value function, confidence // function, bench record, embed profile composer, CSV or JSONL column mapping, IngestBenchRecord, or the // CLI may reach demographics; this test makes that hold by construction, not by review. func TestNoAIFileReferencesEEO(t *testing.T) { markers := []string{"eeo.", "candidate_demographics", "race_ethnicity", "veteran_status", "disability_status", "from eeo", "join eeo"} // The AI layer, the W3 staffing recommendations layer, and the WS9 bench ingestion surface. The WS9 dirs // are relative to this test's location (internal/modules/ai): benchimport and sourcing are two levels up // under platform, the CLI is three levels up under cmd. INVARIANT 4 requires the tripwire to cover them. dirs := []string{".", "../staffing", "../../platform/benchimport", "../../platform/sourcing", "../../../cmd/import-bench"} total := 0 for _, dir := range dirs { total += scanDirForEEO(t, dir, markers) } if total == 0 { t.Fatal("no source files were scanned; the tripwire is not actually running") } t.Logf("scanned %d AI, staffing, and bench ingestion source files, none reach eeo", total) } func scanDirForEEO(t *testing.T, dir string, markers []string) int { entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("read dir %s: %v", dir, err) } checked := 0 for _, e := range entries { name := e.Name() if !strings.HasSuffix(name, ".go") { continue } // safety.go IS the prohibition guard (it must name eeo to forbid it); tests describe the rule. if name == "safety.go" || strings.HasSuffix(name, "_test.go") { continue } body, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { t.Fatalf("read %s/%s: %v", dir, name, err) } // A prohibition COMMENT may name eeo (the rule being documented); only a CODE reference is a reach. // Drop comment lines, then scan the executable content. var code strings.Builder for _, line := range strings.Split(string(body), "\n") { if strings.HasPrefix(strings.TrimSpace(line), "//") { continue } code.WriteString(strings.ToLower(line)) code.WriteByte('\n') } lower := code.String() for _, m := range markers { if strings.Contains(lower, m) { t.Errorf("EEO REACH: %s/%s contains %q in executable code; no AI or staffing path may name the eeo schema", dir, name, m) } } checked++ } return checked }