package ai import ( "strings" "testing" ) // TestGraphSkillIntentsAreRegistered proves the two G3 traversals are in the closed Registry, so // ValidateRegistry asserts them at boot and ExecuteIntent can run them. A missing intent here means the // init registration regressed. func TestGraphSkillIntentsAreRegistered(t *testing.T) { for _, name := range []string{IntentSkillSubstitutionClosure, IntentSkillGapBridge} { it, ok := IntentByName(name) if !ok { t.Fatalf("intent %q must be registered in the closed Registry", name) } if len(it.Params) != 2 { t.Errorf("intent %q must bind exactly two params (the start skill and the depth), got %d", name, len(it.Params)) } if it.Params[0].Kind != ParamUUID { t.Errorf("intent %q first param must be a uuid skill id, got %q", name, it.Params[0].Kind) } if it.Params[1].Kind != ParamInt { t.Errorf("intent %q second param must be an int depth, got %q", name, it.Params[1].Kind) } } } // TestGraphSkillIntentsPassTheReadOnlyGuard is the load bearing security assertion: each registered graph // intent passes GuardReadOnlyQuery against the real ReadAllowlist. A recursive CTE that referenced a non // allowlisted base table, hid a mutation, or named an eeo marker would fail here, the same gate // ValidateRegistry runs at boot and ExecuteIntent re runs at execution time. func TestGraphSkillIntentsPassTheReadOnlyGuard(t *testing.T) { for _, sql := range []string{skillSubstitutionClosureSQL, skillGapBridgeSQL} { if err := GuardReadOnlyQuery(sql, ReadAllowlist); err != nil { t.Errorf("a registered graph intent must pass the read only guard: %v", err) } } } // TestGraphSkillRecursiveDeterminism is the static proof that every recursive determinism guarantee the // AEDT reproducibility mandate requires is present in the FIXED SQL: a hard depth cap bound to the depth // param, a cycle guard (the visited path array plus the not member predicate plus UNION not UNION ALL), // and a unique id tiebreaker on the final ORDER BY. The model never authors this SQL, so pinning its shape // in a test is how the determinism is enforced by construction rather than by review. func TestGraphSkillRecursiveDeterminism(t *testing.T) { for _, tc := range []struct { name string sql string }{ {"substitution_closure", skillSubstitutionClosureSQL}, {"gap_bridge", skillGapBridgeSQL}, } { lower := strings.ToLower(tc.sql) // The hard depth cap: the recursive arm admits a hop only while the carried depth is below the bound // param $2. Without this the walk would only be bounded by the cycle guard. if !strings.Contains(lower, "c.depth < $2") { t.Errorf("%s: the recursive arm must enforce the hard depth cap (c.depth < $2)", tc.name) } // The cycle guard, three independent defenses: a carried visited path array, the not member predicate // that excludes an already visited skill id, and UNION (set semantics) rather than UNION ALL. if !strings.Contains(lower, "c.path || sr.to_skill_id") { t.Errorf("%s: the recursive arm must carry and extend the visited path array", tc.name) } if !strings.Contains(lower, "not (sr.to_skill_id = any(c.path))") { t.Errorf("%s: the recursive arm must exclude an already visited skill id (the cycle guard)", tc.name) } if strings.Contains(lower, "union all") { t.Errorf("%s: the recursion must use UNION (set semantics), never UNION ALL", tc.name) } if !strings.Contains(lower, "union") { t.Errorf("%s: the recursion must use UNION", tc.name) } // The id tiebreaker plus a total order: every ORDER BY carries the skill id tiebreaker AND ends with // the full evidence path, so two rows that tie on depth, strength, and skill id (two distinct paths to // the same node) still sort to a single byte stable order. if !strings.Contains(lower, "order by") { t.Errorf("%s: the final select must be ordered", tc.name) } if !strings.Contains(lower, "skill_id,") { t.Errorf("%s: the ORDER BY must carry the skill_id tiebreaker", tc.name) } if !strings.HasSuffix(strings.TrimSpace(lower), "path") { t.Errorf("%s: the ORDER BY must end with the full path, the total order disambiguator", tc.name) } // The curated graph, not the semantic matcher: the walk reads the curated edges, never an embedding. // (cosine and embedding live in SkillAdjacency, which this file deliberately does not touch.) if strings.Contains(lower, "embedding") || strings.Contains(lower, "<=>") { t.Errorf("%s: the curated graph traversal must not use an embedding or cosine operator", tc.name) } // The curated edge filter: only verified substitutes_for and broader_than edges are walked. if !strings.Contains(lower, "verified_at is not null") { t.Errorf("%s: the walk must restrict to verified curated edges", tc.name) } if !strings.Contains(lower, "substitutes_for") || !strings.Contains(lower, "broader_than") { t.Errorf("%s: the walk must restrict to the substitutes_for and broader_than relations", tc.name) } } } // TestGapBridgeUsesCrosswalk pins that the gap bridge intent additionally consults ontology_crosswalk for a // taxonomy or certification bridge, the second arm the task requires alongside the curated closure. func TestGapBridgeUsesCrosswalk(t *testing.T) { lower := strings.ToLower(skillGapBridgeSQL) if !strings.Contains(lower, "ontology_crosswalk") { t.Error("the gap bridge intent must consult ontology_crosswalk for a taxonomy or certification bridge") } if !strings.Contains(lower, "ontology_crosswalk' as bridge_kind") { t.Error("a crosswalk bridge row must be labeled with its bridge_kind so the caller can distinguish it") } } // TestClampGraphDepth proves the depth is always normalized into the bounded, deterministic range, so no // caller bound depth (zero, negative, or oversized) can turn a bounded walk into an unbounded one. func TestClampGraphDepth(t *testing.T) { cases := map[int]int{ 0: DefaultGraphDepth, -3: DefaultGraphDepth, 1: 1, 3: 3, 5: maxGraphDepth, 99: maxGraphDepth, } for in, want := range cases { if got := ClampGraphDepth(in); got != want { t.Errorf("ClampGraphDepth(%d) = %d, want %d", in, got, want) } } }