//go:build integration package integration import ( "context" "strings" "testing" "github.com/google/uuid" "github.com/missionworkforce/missionmatch/internal/modules/matching" "github.com/missionworkforce/missionmatch/internal/platform/audit" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" "github.com/missionworkforce/missionmatch/internal/platform/extraction" ) func missionCheckService() *matching.Service { // Real audit writer so the ephemeral audit row is actually written and can be read back. return matching.NewService(appDB, nil, audit.New(appDB)).WithExtractor(extraction.NewDeterministic()) } const ( mcResume = "Jane Q Operator\nHonolulu, HI\n" + "Senior Cyber Engineer with 12 years of experience supporting the Air Force.\n" + "Active TS/SCI clearance. Skills: Python, AWS, Splunk, Incident Response. Certifications: CISSP." mcJD = "Cyber Engineer requires an active Secret clearance and 8 years of experience. " + "Required skills: Python, AWS, Splunk. Supporting the Air Force." ) // TestMissionCheckModeBEphemeral proves Mode B scores in memory, writes exactly ONE ephemeral audit row // with a NULL entity id and no resume PII in the payload, and persists NO candidate. func TestMissionCheckModeBEphemeral(t *testing.T) { org := seedOrg(t) svc := missionCheckService() before := candidateCount(t, org) res, err := svc.MissionCheckJD(ctxOrg(org), mcResume, mcJD) if err != nil { t.Fatalf("MissionCheckJD: %v", err) } if res.View.MissionReadiness.Score == "" { t.Fatal("expected a Mission Readiness score in the view") } if res.Recommendation == "" { t.Fatal("expected a derived recommendation") } if len(res.DataCategories) == 0 { t.Fatal("expected the AEDT data category disclosure to be present") } // Store nothing: the candidate count is unchanged. if after := candidateCount(t, org); after != before { t.Fatalf("Mission Check must persist no candidate: count went %d to %d", before, after) } // Exactly one ephemeral audit row, NULL entity id, and no resume PII in the payload. rows, _ := migrator.Query(context.Background(), `SELECT entity_id, COALESCE(new_values::text,'') FROM audit_logs WHERE organization_id=$1 AND action='aedt.ephemeral.evaluated'`, org) defer rows.Close() n := 0 for rows.Next() { n++ var entityID *string var payload string if err := rows.Scan(&entityID, &payload); err != nil { t.Fatalf("scan audit row: %v", err) } if entityID != nil { t.Errorf("ephemeral audit entity_id must be NULL, got %q", *entityID) } for _, pii := range []string{"Jane", "Operator", "Honolulu"} { if strings.Contains(payload, pii) { t.Errorf("ephemeral audit payload leaked PII %q: %s", pii, payload) } } if !strings.Contains(payload, "missionCheck.screen") { t.Errorf("payload should name the tool, got %s", payload) } } if n != 1 { t.Fatalf("expected exactly 1 ephemeral audit row, got %d", n) } } // TestMissionCheckModeAReturnsRankedRows proves Mode A scores the resume against the open requirements // and returns ranked rows each carrying a derived recommendation, while persisting no candidate. func TestMissionCheckModeAReturnsRankedRows(t *testing.T) { org := seedOrg(t) // One open requirement (no opportunity link => treated as open) so Mode A has something to rank. if _, err := dbgen.New(migrator).CreateRequirement(context.Background(), dbgen.CreateRequirementParams{ OrganizationID: org, Title: "Cyber Engineer", RequiredLevel: "secret", RequiredPoly: "none", RequiredYears: 5, }); err != nil { t.Fatalf("seed requirement: %v", err) } res, err := missionCheckService().MissionCheckOpportunities(ctxOrg(org), mcResume, 10) if err != nil { t.Fatalf("MissionCheckOpportunities: %v", err) } if len(res.Rows) != 1 { t.Fatalf("expected 1 ranked row, got %d", len(res.Rows)) } if res.Rows[0].Recommendation == "" || res.Rows[0].Score == "" { t.Fatalf("row missing recommendation or score: %+v", res.Rows[0]) } if candidateCount(t, org) != 0 { t.Fatal("Mode A must persist no candidate") } } // TestSaveCandidateCreatesAuditedRecord proves Save Candidate creates a candidate with its skills, // certifications, and customer experience plus a candidate.created audit row, and that the saved // candidate is then screenable through the normal scoring path. func TestSaveCandidateCreatesAuditedRecord(t *testing.T) { org := seedOrg(t) req, err := dbgen.New(migrator).CreateRequirement(context.Background(), dbgen.CreateRequirementParams{ OrganizationID: org, Title: "Open Role", RequiredLevel: "none", RequiredPoly: "none", }) if err != nil { t.Fatalf("seed requirement: %v", err) } svc := missionCheckService() id, err := svc.SaveCandidate(ctxOrg(org), matching.SaveCandidateInput{ FullName: "Jane Q Operator", ClearanceLevel: "ts_sci", ClearancePoly: "none", ClearanceStatus: "active", YearsExperience: 12, Location: "Honolulu, HI", Available: true, Skills: []string{"Python", "AWS", "Splunk"}, Certifications: []string{"CISSP"}, CustomerExperience: []string{"Air Force"}, }) if err != nil { t.Fatalf("SaveCandidate: %v", err) } if id == uuid.Nil { t.Fatal("expected a candidate id") } if candidateCount(t, org) != 1 { t.Fatalf("expected exactly 1 saved candidate") } // A candidate.created audit row exists for the new candidate. var auditN int if err := migrator.QueryRow(context.Background(), `SELECT count(*) FROM audit_logs WHERE organization_id=$1 AND action='candidate.created' AND entity_id=$2`, org, id.String()).Scan(&auditN); err != nil { t.Fatalf("audit count: %v", err) } if auditN != 1 { t.Fatalf("expected 1 candidate.created audit row, got %d", auditN) } // The saved candidate is screenable through the normal path and reproduces a deterministic score. view, err := svc.Screen(ctxOrg(org), id, req.ID) if err != nil { t.Fatalf("Screen saved candidate: %v", err) } if view.MissionReadiness.Score == "" { t.Fatal("expected a Mission Readiness score for the saved candidate") } } func candidateCount(t *testing.T, org uuid.UUID) int { t.Helper() var n int if err := migrator.QueryRow(context.Background(), "SELECT count(*) FROM candidates WHERE organization_id=$1 AND deleted_at IS NULL", org).Scan(&n); err != nil { t.Fatalf("candidate count: %v", err) } return n }