package matching import ( "context" "testing" "github.com/shopspring/decimal" "github.com/missionworkforce/missionmatch/internal/modules/matching/engine" ) // buildResolver is a shorthand that wires maps via the unexported test constructor. func buildResolver(nameToID, aliasToID map[string]string, rels map[string][]relEdge) *DeterministicAliasResolver { return newResolverFromMaps(nameToID, aliasToID, rels) } // TestAliasResolver_CandidateNameIsAlias: the candidate carries a name that is a curated alias for // the required skill. Both canonical ids resolve to the same skill, so ResolveAlias returns // Similarity 1.0, Exact true, AliasMatched true. func TestAliasResolver_CandidateNameIsAlias(t *testing.T) { nameToID := map[string]string{ "signals intelligence": "skill-sigint", } aliasToID := map[string]string{ "sigint": "skill-sigint", } r := buildResolver(nameToID, aliasToID, nil) required := engine.RequiredSkill{SkillID: "skill-sigint", Name: "Signals Intelligence"} cands := []engine.CandidateSkill{ {SkillID: "", Name: "SIGINT"}, // candidate holds it by alias name } m, ok := r.ResolveAlias(context.Background(), required, cands) if !ok { t.Fatal("expected a match, got none") } if !m.Similarity.Equal(decimal.NewFromInt(1)) { t.Errorf("Similarity = %s, want 1", m.Similarity) } if !m.Exact { t.Error("Exact should be true for alias canonical match") } if !m.AliasMatched { t.Error("AliasMatched should be true") } if !m.Matched { t.Error("Matched should be true") } } // TestAliasResolver_RelationshipSubstitutesAboveThreshold: the candidate's canonical skill has a // substitutes_for edge to the required canonical skill at strength 0.80 (above the 0.62 gate). // Returns Similarity 0.80, Exact false, AliasMatched false. func TestAliasResolver_RelationshipSubstitutesAboveThreshold(t *testing.T) { nameToID := map[string]string{ "kubernetes": "skill-k8s", "docker swarm": "skill-swarm", } rels := map[string][]relEdge{ "skill-swarm": { {toID: "skill-k8s", relation: "substitutes_for", strength: decimal.RequireFromString("0.80")}, }, } r := buildResolver(nameToID, nil, rels) required := engine.RequiredSkill{SkillID: "skill-k8s", Name: "Kubernetes"} cands := []engine.CandidateSkill{ {SkillID: "", Name: "Docker Swarm"}, } m, ok := r.ResolveAlias(context.Background(), required, cands) if !ok { t.Fatal("expected a match via substitutes_for, got none") } if !m.Similarity.Equal(decimal.RequireFromString("0.80")) { t.Errorf("Similarity = %s, want 0.80", m.Similarity) } if m.Exact { t.Error("Exact should be false for relationship match") } if m.AliasMatched { t.Error("AliasMatched should be false for relationship match") } if !m.Matched { t.Error("Matched should be true") } } // TestAliasResolver_RelationshipBelowThreshold: the same edge at strength 0.50 is below the 0.62 // gate, so ResolveAlias returns no match. func TestAliasResolver_RelationshipBelowThreshold(t *testing.T) { nameToID := map[string]string{ "kubernetes": "skill-k8s", "docker swarm": "skill-swarm", } rels := map[string][]relEdge{ "skill-swarm": { {toID: "skill-k8s", relation: "substitutes_for", strength: decimal.RequireFromString("0.50")}, }, } r := buildResolver(nameToID, nil, rels) required := engine.RequiredSkill{SkillID: "skill-k8s", Name: "Kubernetes"} cands := []engine.CandidateSkill{ {SkillID: "", Name: "Docker Swarm"}, } _, ok := r.ResolveAlias(context.Background(), required, cands) if ok { t.Fatal("expected no match below 0.62 threshold, but got a match") } } // TestAliasResolver_NoAliasOrRelationship: neither an alias nor any qualifying relationship exists. // Returns no match. func TestAliasResolver_NoAliasOrRelationship(t *testing.T) { nameToID := map[string]string{ "golang": "skill-go", "rust": "skill-rust", } r := buildResolver(nameToID, nil, nil) required := engine.RequiredSkill{SkillID: "skill-go", Name: "Golang"} cands := []engine.CandidateSkill{ {SkillID: "", Name: "Rust"}, } _, ok := r.ResolveAlias(context.Background(), required, cands) if ok { t.Fatal("expected no match when no alias or relationship exists") } } // TestAliasResolver_RequiredResolvesViaAlias: the required skill name is itself an alias text. The // candidate holds the canonical skill by id. Alias canonical resolution on both sides yields a match // with Similarity 1.0, AliasMatched true. func TestAliasResolver_RequiredResolvesViaAlias(t *testing.T) { // required.Name "JS" is an alias for skill-javascript aliasToID := map[string]string{ "js": "skill-javascript", } nameToID := map[string]string{ "javascript": "skill-javascript", } r := buildResolver(nameToID, aliasToID, nil) // required has no SkillID set, name is the alias text required := engine.RequiredSkill{Name: "JS"} cands := []engine.CandidateSkill{ {SkillID: "skill-javascript", Name: "JavaScript"}, } m, ok := r.ResolveAlias(context.Background(), required, cands) if !ok { t.Fatal("expected alias match when required resolves via alias, got none") } if !m.Similarity.Equal(decimal.NewFromInt(1)) { t.Errorf("Similarity = %s, want 1", m.Similarity) } if !m.AliasMatched { t.Error("AliasMatched should be true") } }