package matching import ( "context" "encoding/json" "sort" "testing" "time" "github.com/missionworkforce/missionmatch/internal/modules/matching/engine" "github.com/missionworkforce/missionmatch/internal/platform/extraction" ) func TestRecommendFromBand(t *testing.T) { cases := []struct { band engine.Band want string }{ {engine.BandGreen, RecSubmit}, {engine.BandYellow, RecReview}, {engine.BandRed, RecDoNotSubmit}, } for _, tc := range cases { if got := RecommendFromBand(tc.band).Value; got != tc.want { t.Errorf("band %v: got %q want %q", tc.band, got, tc.want) } } // The persisted code path must agree, and an unknown code must never read as SUBMIT. if recommendFromBandCode("green").Value != RecSubmit { t.Error("code green should map to SUBMIT") } if recommendFromBandCode("bogus").Value != RecDoNotSubmit { t.Error("an unrecognized band code must default to DO NOT SUBMIT, never SUBMIT") } } func TestValidProvenance(t *testing.T) { for p, want := range map[string]bool{ "self_reported": true, "sponsor_verified": true, "document": true, "continuous_vetting": true, "verified": false, "bogus": false, "": false, } { if got := validProvenance(p); got != want { t.Errorf("validProvenance(%q) = %v want %v", p, got, want) } } } // TestExtractionStatusIsConservative proves a resume that does not assert an active clearance scores 0 // on clearance: status defaults to Unknown unless the text asserts active AND the level clears the bar. func TestExtractionStatusIsConservative(t *testing.T) { notAsserted := extraction.RawExtraction{ClearanceLevel: "secret", ClearanceActive: false} ec := engineCandidateFromExtraction(notAsserted) if ec.Clearance.Status != engine.StatusUnknown { t.Errorf("inactive parse: got status %v want Unknown", ec.Clearance.Status) } asserted := extraction.RawExtraction{ClearanceLevel: "secret", ClearanceActive: true} if engineCandidateFromExtraction(asserted).Clearance.Status != engine.StatusActive { t.Error("an asserted active secret clearance should map to StatusActive") } // Below Confidential never reads as active even if the text says active. bare := extraction.RawExtraction{ClearanceLevel: "", ClearanceActive: true} if engineCandidateFromExtraction(bare).Clearance.Status != engine.StatusUnknown { t.Error("no parsed level must stay Unknown status regardless of the active flag") } } func TestRequirementFromExtractionMarksMandatoryWithEmptySkillID(t *testing.T) { rx := extraction.RawExtraction{Skills: []string{"Python", "AWS"}, ClearanceLevel: "secret"} er := engineRequirementFromExtraction(rx) if len(er.RequiredSkills) != 2 { t.Fatalf("want 2 required skills, got %d", len(er.RequiredSkills)) } for _, rs := range er.RequiredSkills { if !rs.Mandatory { t.Errorf("required skill %q should be mandatory", rs.Name) } if rs.SkillID != "" { t.Errorf("parsed JD skill should carry an empty SkillID, got %q", rs.SkillID) } } if er.RequiredLevel != engine.ClearanceSecret { t.Errorf("required level: got %v want Secret", er.RequiredLevel) } } // TestMissionCheckScoreTiesOut proves the in memory Mission Check path produces the IDENTICAL Mission // Readiness value the ScreenView surfaces: the score the engine computes equals the score the readiness // panel renders, so a resume and a saved candidate with the same fields read the same everywhere. func TestMissionCheckScoreTiesOut(t *testing.T) { resume := extraction.RawExtraction{ ClearanceLevel: "ts_sci", ClearanceActive: true, Skills: []string{"Python", "AWS"}, Certifications: []string{"CISSP"}, Agencies: []string{"Air Force"}, YearsExperience: 10, } jd := extraction.RawExtraction{ ClearanceLevel: "secret", // TS/SCI satisfies Secret Skills: []string{"Python", "AWS"}, Certifications: []string{"CISSP"}, Agencies: []string{"Air Force"}, YearsExperience: 5, } ec := engineCandidateFromExtraction(resume) er := engineRequirementFromExtraction(jd) res := engine.ScoreCandidate(context.Background(), ec, er, nil) // nil matcher: exact only, deterministic if res.Band != engine.BandGreen { t.Fatalf("expected green band for a fully matching resume, got %v (total %s)", res.Band, res.Total.StringFixed(2)) } if RecommendFromBand(res.Band).Value != RecSubmit { t.Errorf("green should recommend SUBMIT") } view := assembleScreenView(ec, er, res, factsFromExtraction(resume), time.Now()) if view.MissionReadiness.Score != res.Total.StringFixed(2) { t.Errorf("readiness score %q must equal the engine total %q (one scoring source of truth)", view.MissionReadiness.Score, res.Total.StringFixed(2)) } if view.MissionReadiness.Band != res.Band.Code() { t.Errorf("readiness band %q must equal the engine band %q", view.MissionReadiness.Band, res.Band.Code()) } } // TestEphemeralScoreEventHasNoPII locks the ephemeral audit payload to the allowed keys only, with no // candidate id and no name, email, or resume text anywhere. func TestEphemeralScoreEventHasNoPII(t *testing.T) { // Score a trivial pair so Band and Total are set deterministically. res := engine.ScoreCandidate(context.Background(), engine.Candidate{ID: "mission_check"}, engine.Requirement{}, nil) ev := ephemeralScoreEvent("missionCheck.screen", res) if ev.Action != "aedt.ephemeral.evaluated" { t.Errorf("action: got %q", ev.Action) } if ev.EntityType != "mission_check" { t.Errorf("entity type: got %q", ev.EntityType) } if ev.EntityID != "" { t.Errorf("entity id must be empty (NULL), got %q", ev.EntityID) } var payload map[string]any if err := json.Unmarshal(ev.NewValues, &payload); err != nil { t.Fatalf("payload not valid json: %v", err) } allowed := map[string]bool{ "tool": true, "score": true, "band": true, "recommendation": true, "model_version": true, "advisory": true, "persisted": true, } keys := make([]string, 0, len(payload)) for k := range payload { keys = append(keys, k) if !allowed[k] { t.Errorf("unexpected key in ephemeral audit payload: %q", k) } } sort.Strings(keys) if len(keys) != len(allowed) { t.Errorf("payload keys %v do not match the allowed set", keys) } if payload["persisted"] != false { t.Errorf("persisted must be false for an ephemeral evaluation") } }