package staffing import ( "reflect" "strings" "testing" "github.com/missionworkforce/missionmatch/internal/modules/matching/engine" ) // protectedTokens are substrings of any protected attribute or close proxy. A field name containing one is // a demographic input and must never appear on a W3 core type. They are unambiguous enough not to collide // with the platform's real field names (score, clearance, skill, requirement, candidate, value, and so on). var protectedTokens = []string{ "gender", "race", "ethnic", "veteran", "disability", "religion", "pregnan", "genetic", "maritalstatus", "sexualorientation", "nationalorigin", "demographic", } // TestNoProtectedAttributeInCoreTypes walks the public input and output types of the W3 recommendation // core (and the engine candidate they build on) and fails if any field, at any depth, names a protected // attribute. The decision content is computed only from these types, so this proves by construction that no // protected attribute can be an input to a recommendation, the same guarantee the engine Candidate carries. func TestNoProtectedAttributeInCoreTypes(t *testing.T) { types := []any{ ScoredCandidate{}, GapContext{}, Recommendation{}, Evidence{}, ReskillFact{}, ReassignEvidence{}, Counterfactual{}, TriggerChange{}, AssignEdge{}, PortfolioInput{}, Assignment{}, engine.Candidate{}, // the scoring input the core is built on engine.Requirement{}, } for _, v := range types { walkFields(t, reflect.TypeOf(v), reflect.TypeOf(v).Name(), map[reflect.Type]bool{}) } } func walkFields(t *testing.T, rt reflect.Type, path string, seen map[reflect.Type]bool) { for rt.Kind() == reflect.Ptr || rt.Kind() == reflect.Slice || rt.Kind() == reflect.Array { rt = rt.Elem() } if rt.Kind() != reflect.Struct || seen[rt] { return } seen[rt] = true for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) lower := strings.ToLower(f.Name) for _, tok := range protectedTokens { if strings.Contains(lower, tok) { t.Errorf("PROTECTED ATTRIBUTE: %s.%s contains %q; no protected attribute may be an input to a recommendation", path, f.Name, tok) } } walkFields(t, f.Type, path+"."+f.Name, seen) } }