package model import ( "fmt" "reflect" "strings" "testing" "modelRT/constants" ) func TestFuzzyRecommendOffsetUsesFullMatchedInput(t *testing.T) { tests := []struct { name string searchPrefix string searchInput string want int }{ { name: "zone fuzzy prefix", searchPrefix: "grid000", searchInput: "z", want: len([]rune("grid000.z")), }, { name: "level one fuzzy", searchPrefix: "", searchInput: "g", want: len([]rune("g")), }, { name: "measurement fuzzy preserves config token", searchPrefix: "grid.zone.station.nspath.comp.config", searchInput: "m", want: len([]rune("grid.zone.station.nspath.comp.config.m")), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := fuzzyRecommendOffset(tt.searchPrefix, tt.searchInput) if got != tt.want { t.Fatalf("expected offset %d, got %d", tt.want, got) } }) } } func TestNormalizeRecommendResultsUsesInputLengthForExactCompletion(t *testing.T) { tests := []struct { name string input string }{ { name: "full token1 to token7 structure", input: "grid000.zone000.station000.220kV_学府路1-testProject1.compTag.config.IA_rms_CTA-testProject1", }, { name: "local token4 to token7 structure", input: "220kV_学府路1-testProject1.IA_rms_CTA-testProject1", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { results := map[string]SearchResult{ constants.MeasTagRecommendHierarchyType.String(): { RecommendType: constants.MeasTagRecommendHierarchyType, QueryDatas: []string{""}, IsFuzzy: false, }, } got := normalizeRecommendResults(tt.input, results) result := got[constants.MeasTagRecommendHierarchyType.String()] wantOffset := len([]rune(tt.input)) if result.Offset != wantOffset { t.Fatalf("expected offset %d, got %d", wantOffset, result.Offset) } if len(result.QueryDatas) != 0 { t.Fatalf("expected exact completion to return no suffix, got %v", result.QueryDatas) } }) } } func TestNormalizeRecommendResultsUsesInputLengthForLevelContinuation(t *testing.T) { tests := []struct { name string input string recommendType constants.RecommendHierarchyType }{ { name: "token1 grid can continue", input: "grid000", recommendType: constants.GridRecommendHierarchyType, }, { name: "token1 token2 zone can continue", input: "grid000.zone000", recommendType: constants.ZoneRecommendHierarchyType, }, { name: "token1 token2 token3 station can continue", input: "grid000.zone000.station000", recommendType: constants.StationRecommendHierarchyType, }, { name: "token1 token2 token3 token4 nspath can continue", input: "grid000.zone000.station000.110kV_TV-demoProject", recommendType: constants.CompNSPathRecommendHierarchyType, }, { name: "token1 token2 token3 token4 token5 compTag can continue", input: "grid000.zone000.station000.110kV_TV-demoProject.cable_26-demoProject110kV_TV", recommendType: constants.CompTagRecommendHierarchyType, }, { name: "token1 through token6 config can continue", input: "grid000.zone000.station000.110kV_TV-demoProject.cable_26-demoProject110kV_TV.base_extend", recommendType: constants.ConfigRecommendHierarchyType, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { results := map[string]SearchResult{ tt.recommendType.String(): { RecommendType: tt.recommendType, QueryDatas: []string{"."}, IsFuzzy: false, }, } got := normalizeRecommendResults(tt.input, results) result := got[tt.recommendType.String()] wantOffset := len([]rune(tt.input)) if result.Offset != wantOffset { t.Fatalf("expected offset %d, got %d", wantOffset, result.Offset) } if len(result.QueryDatas) != 1 || result.QueryDatas[0] != "." { t.Fatalf("expected level continuation '.', got %v", result.QueryDatas) } }) } } func TestNormalizeRecommendResultsKeepsFallbackOffsetZero(t *testing.T) { input := "x" results := map[string]SearchResult{ constants.GridRecommendHierarchyType.String(): { RecommendType: constants.GridRecommendHierarchyType, QueryDatas: []string{"grid000", "grid001"}, IsFuzzy: true, IsFallback: true, Offset: 0, }, constants.CompNSPathRecommendHierarchyType.String(): { RecommendType: constants.CompNSPathRecommendHierarchyType, QueryDatas: []string{"nspath000", "nspath001"}, IsFuzzy: true, IsFallback: true, Offset: 0, }, } got := normalizeRecommendResults(input, results) for key, result := range got { if result.Offset != 0 { t.Fatalf("expected fallback offset 0 for %s, got %d", key, result.Offset) } if len(result.QueryDatas) != 2 { t.Fatalf("expected fallback recommends to remain intact for %s, got %v", key, result.QueryDatas) } } } func TestNormalizeRecommendResultsKeepsParentPrefixOffsetForSpecificFallback(t *testing.T) { nsPath := "220kV_学府路1-testProject1" input := nsPath + ".x" offset := recommendPrefixOffset([]string{nsPath}) results := map[string]SearchResult{ constants.CompTagRecommendHierarchyType.String(): { RecommendType: constants.CompTagRecommendHierarchyType, QueryDatas: []string{ nsPath + ".CTA-testProject1220kV_学府路1", nsPath + ".CB-testProject1220kV_学府路1", }, IsFuzzy: true, IsFallback: true, Offset: offset, }, constants.MeasTagRecommendHierarchyType.String(): { RecommendType: constants.MeasTagRecommendHierarchyType, QueryDatas: []string{ nsPath + ".IA_rms_CTA-testProject1", nsPath + ".IB_rms_CTA-testProject1", }, IsFuzzy: true, IsFallback: true, Offset: offset, }, } got := normalizeRecommendResults(input, results) if offset != 24 { t.Fatalf("expected sample parent prefix offset 24, got %d", offset) } for key, result := range got { if result.Offset != 24 { t.Fatalf("expected specific fallback offset 24 for %s, got %d", key, result.Offset) } for _, recommend := range result.QueryDatas { if strings.HasPrefix(recommend, nsPath+".") { t.Fatalf("expected trimmed fallback recommend for %s, got %s", key, recommend) } } } } func TestNormalizeRecommendResultsTrimsConfigFuzzySuffixes(t *testing.T) { input := "110kV_TV-testProject1.cable_22-testProject1110kV_TV.base_extend.c" offset := len([]rune(input)) results := map[string]SearchResult{ constants.MeasTagRecommendHierarchyType.String(): { RecommendType: constants.MeasTagRecommendHierarchyType, QueryDatas: []string{ input + "apacity", input + "ategory", input + "urrent", input + "ode", }, IsFuzzy: true, Offset: offset, }, } got := normalizeRecommendResults(input, results) result := got[constants.MeasTagRecommendHierarchyType.String()] want := []string{"apacity", "ategory", "urrent", "ode"} if result.Offset != offset { t.Fatalf("expected offset %d, got %d", offset, result.Offset) } if !reflect.DeepEqual(result.QueryDatas, want) { t.Fatalf("expected trimmed recommends %#v, got %#v", want, result.QueryDatas) } } func TestNormalizeRecommendResultsTrimsToken4FallbackCandidates(t *testing.T) { nsPath := "110kV_TV-testProject1" input := nsPath + ".x" offset := recommendPrefixOffset([]string{nsPath}) results := map[string]SearchResult{ constants.CompTagRecommendHierarchyType.String(): { RecommendType: constants.CompTagRecommendHierarchyType, QueryDatas: []string{ nsPath + ".cable_22-testProject1110kV_TV", nsPath + ".cable_23-testProject1110kV_TV", }, IsFuzzy: true, IsFallback: true, Offset: offset, }, constants.MeasTagRecommendHierarchyType.String(): { RecommendType: constants.MeasTagRecommendHierarchyType, QueryDatas: []string{ nsPath + ".IA_rms_CTA-testProject1", nsPath + ".IB_rms_CTA-testProject1", }, IsFuzzy: true, IsFallback: true, Offset: offset, }, } got := normalizeRecommendResults(input, results) for key, result := range got { if result.Offset != offset { t.Fatalf("expected offset %d for %s, got %d", offset, key, result.Offset) } for _, recommend := range result.QueryDatas { if strings.HasPrefix(recommend, nsPath+".") { t.Fatalf("expected trimmed token4 fallback recommend for %s, got %s", key, recommend) } } } if got[constants.CompTagRecommendHierarchyType.String()].QueryDatas[0] != "cable_22-testProject1110kV_TV" { t.Fatalf("expected token5 suffixes, got %v", got[constants.CompTagRecommendHierarchyType.String()].QueryDatas) } if got[constants.MeasTagRecommendHierarchyType.String()].QueryDatas[0] != "IA_rms_CTA-testProject1" { t.Fatalf("expected token7 suffixes, got %v", got[constants.MeasTagRecommendHierarchyType.String()].QueryDatas) } } func TestNormalizeRecommendResultsKeepsFullAndLocalConfigMeasurementSuffixesConsistent(t *testing.T) { fullPrefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.base_extend" localPrefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV.base_extend" members := []string{"capacity", "category", "current"} fullResults := map[string]SearchResult{ constants.MeasTagRecommendHierarchyType.String(): { RecommendType: constants.MeasTagRecommendHierarchyType, QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(fullPrefix+".", "."), members), IsFuzzy: false, }, } localResults := map[string]SearchResult{ constants.MeasTagRecommendHierarchyType.String(): { RecommendType: constants.MeasTagRecommendHierarchyType, QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(localPrefix+".", "."), members), IsFuzzy: false, }, } fullGot := normalizeRecommendResults(fullPrefix+".", fullResults) localGot := normalizeRecommendResults(localPrefix+".", localResults) fullRecommends := fullGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas localRecommends := localGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas if !reflect.DeepEqual(fullRecommends, localRecommends) { t.Fatalf("expected full/local config measurement suffixes to match, full=%#v local=%#v", fullRecommends, localRecommends) } } func TestNormalizeRecommendResultsKeepsFullAndLocalBayMeasurementSuffixesConsistent(t *testing.T) { fullPrefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay" localPrefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay" members := []string{"IA_rms", "IB_rms", "IC_rms"} fullResults := map[string]SearchResult{ constants.MeasTagRecommendHierarchyType.String(): { RecommendType: constants.MeasTagRecommendHierarchyType, QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(fullPrefix+".", "."), members), IsFuzzy: false, }, } localResults := map[string]SearchResult{ constants.MeasTagRecommendHierarchyType.String(): { RecommendType: constants.MeasTagRecommendHierarchyType, QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(localPrefix+".", "."), members), IsFuzzy: false, }, } fullGot := normalizeRecommendResults(fullPrefix+".", fullResults) localGot := normalizeRecommendResults(localPrefix+".", localResults) fullRecommends := fullGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas localRecommends := localGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas if !reflect.DeepEqual(fullRecommends, localRecommends) { t.Fatalf("expected full/local bay measurement suffixes to match, full=%#v local=%#v", fullRecommends, localRecommends) } } func TestRecommendGroupTokens(t *testing.T) { tests := []struct { name string inputSlice []string wantCompTag string wantConfig string wantOK bool }{ { name: "full token1 to token7 input", inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "base_extend", ""}, wantCompTag: "comp_tag", wantConfig: "base_extend", wantOK: true, }, { name: "local token4 to token7 input", inputSlice: []string{"nspath", "comp_tag", "component", ""}, wantCompTag: "comp_tag", wantConfig: "component", wantOK: true, }, { name: "missing config token", inputSlice: []string{"nspath", ""}, wantOK: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotCompTag, gotConfig, gotOK := recommendGroupTokens(tt.inputSlice) if gotOK != tt.wantOK { t.Fatalf("expected ok %t, got %t", tt.wantOK, gotOK) } if gotCompTag != tt.wantCompTag || gotConfig != tt.wantConfig { t.Fatalf("expected compTag/config %q/%q, got %q/%q", tt.wantCompTag, tt.wantConfig, gotCompTag, gotConfig) } }) } } func TestConfigRecommendCompTag(t *testing.T) { tests := []struct { name string inputSlice []string want string wantOK bool }{ { name: "full token1 to token6 input", inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "b"}, want: "comp_tag", wantOK: true, }, { name: "local token4 to token6 input", inputSlice: []string{"nspath", "comp_tag", "b"}, want: "comp_tag", wantOK: true, }, { name: "missing comp tag", inputSlice: []string{"nspath"}, wantOK: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, gotOK := configRecommendCompTag(tt.inputSlice) if gotOK != tt.wantOK { t.Fatalf("expected ok %t, got %t", tt.wantOK, gotOK) } if got != tt.want { t.Fatalf("expected compTag %q, got %q", tt.want, got) } }) } } func TestFilterMembersByTrimmedPrefix(t *testing.T) { tests := []struct { name string members []string searchInput string wantMembers []string wantMatchInput string }{ { name: "config typo falls back to previous rune", members: []string{"bay", "base_extend", "model", "rated", "stable", "component"}, searchInput: "bx", wantMembers: []string{"bay", "base_extend"}, wantMatchInput: "b", }, { name: "measurement typo falls back to previous rune", members: []string{"I_A_rms", "I_B_rms", "U_A_rms"}, searchInput: "Ix", wantMembers: []string{"I_A_rms", "I_B_rms"}, wantMatchInput: "I", }, { name: "no fallback to empty prefix", members: []string{"bay", "base_extend"}, searchInput: "x", wantMembers: []string{}, wantMatchInput: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotMembers, gotMatchInput := filterMembersByTrimmedPrefix(tt.members, tt.searchInput) if !reflect.DeepEqual(gotMembers, tt.wantMembers) { t.Fatalf("expected members %#v, got %#v", tt.wantMembers, gotMembers) } if gotMatchInput != tt.wantMatchInput { t.Fatalf("expected matched input %q, got %q", tt.wantMatchInput, gotMatchInput) } }) } } func TestDotAndTypoInputsShareTrimmedPrefixFallback(t *testing.T) { tests := []struct { name string dotInput string typoInput string members []string wantSuffix []string }{ { name: "token1 grid", dotInput: "g.", typoInput: "gx", members: []string{"grid000", "grid001", "zone000"}, wantSuffix: []string{"grid000", "grid001"}, }, { name: "token2 zone", dotInput: "z.", typoInput: "zx", members: []string{"zone000", "zone001", "station000"}, wantSuffix: []string{"zone000", "zone001"}, }, { name: "token3 station", dotInput: "s.", typoInput: "sx", members: []string{"station000", "station001", "zone000"}, wantSuffix: []string{"station000", "station001"}, }, { name: "token4 nspath", dotInput: "1.", typoInput: "1x", members: []string{"110kV_TV-demoProject", "110kV_TV-testProject1", "220kV_TV-demoProject"}, wantSuffix: []string{"110kV_TV-demoProject", "110kV_TV-testProject1"}, }, { name: "token5 component tag", dotInput: "c.", typoInput: "cx", members: []string{"cable_22-testProject1110kV_TV", "cable_26-demoProject110kV_TV", "bay"}, wantSuffix: []string{"cable_22-testProject1110kV_TV", "cable_26-demoProject110kV_TV"}, }, { name: "token6 config", dotInput: "b.", typoInput: "bx", members: []string{"bay", "base_extend", "component"}, wantSuffix: []string{"bay", "base_extend"}, }, { name: "token7 measurement", dotInput: "I.", typoInput: "Ix", members: []string{"IA_rms", "IB_rms", "UA_rms"}, wantSuffix: []string{"IA_rms", "IB_rms"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dotSearchInput := strings.TrimSuffix(tt.dotInput, ".") typoSearchInput := trimLastRune(tt.typoInput) dotMembers, dotMatchedInput := filterMembersByTrimmedPrefix(tt.members, dotSearchInput) typoMembers, typoMatchedInput := filterMembersByTrimmedPrefix(tt.members, typoSearchInput) if dotMatchedInput != typoMatchedInput { t.Fatalf("expected dot and typo matched input to be equal, dot=%q typo=%q", dotMatchedInput, typoMatchedInput) } if !reflect.DeepEqual(dotMembers, typoMembers) { t.Fatalf("expected dot and typo members to be equal, dot=%#v typo=%#v", dotMembers, typoMembers) } if !reflect.DeepEqual(dotMembers, tt.wantSuffix) { t.Fatalf("expected members %#v, got %#v", tt.wantSuffix, dotMembers) } }) } } func TestExactMatchChecksForInput(t *testing.T) { tests := []struct { name string inputSlice []string want []exactMatchCheck }{ { name: "token1 can be grid or local nspath", inputSlice: []string{"g"}, want: []exactMatchCheck{ {setKey: constants.RedisAllGridSetKey, member: "g"}, {setKey: constants.RedisAllCompNSPathSetKey, member: "g"}, }, }, { name: "token1 token2 can be zone comp tag or nspath meas", inputSlice: []string{"grid000", "z"}, want: []exactMatchCheck{ {setKey: fmt.Sprintf(constants.RedisSpecGridZoneSetKey, "grid000"), member: "z"}, {setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "grid000"), member: "z"}, {setKey: constants.RedisAllCompTagSetKey, member: "z"}, {setKey: fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, "grid000"), member: "z"}, }, }, { name: "token1 token2 token3 can be station or local config", inputSlice: []string{"grid000", "zone000", "s"}, want: []exactMatchCheck{ {setKey: fmt.Sprintf(constants.RedisSpecZoneStationSetKey, "zone000"), member: "s"}, {setKey: constants.RedisAllConfigSetKey, member: "s"}, }, }, { name: "token1 token2 token3 token4 can be nspath or local meas", inputSlice: []string{"grid000", "zone000", "station000", "1"}, want: []exactMatchCheck{ {setKey: fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, "station000"), member: "1"}, {setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "zone000"), member: "1"}, }, }, { name: "token1 through token5 component tag", inputSlice: []string{"grid000", "zone000", "station000", "nspath", "c"}, want: []exactMatchCheck{ {setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "nspath"), member: "c"}, {setKey: constants.RedisAllCompTagSetKey, member: "c"}, }, }, { name: "token1 through token6 config", inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "b"}, want: []exactMatchCheck{ {setKey: constants.RedisAllConfigSetKey, member: "b"}, }, }, { name: "token1 through token7 measurement", inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "bay", "I"}, want: []exactMatchCheck{ {setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"), member: "I"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := exactMatchChecksForInput(tt.inputSlice) if !reflect.DeepEqual(got, tt.want) { t.Fatalf("expected checks %#v, got %#v", tt.want, got) } }) } } func TestFallbackSpecificSetKey(t *testing.T) { tests := []struct { name string hierarchy constants.RecommendHierarchyType inputSlice []string want string wantOK bool }{ { name: "zone fallback uses grid specific set", hierarchy: constants.ZoneRecommendHierarchyType, inputSlice: []string{"grid000", "z"}, want: fmt.Sprintf(constants.RedisSpecGridZoneSetKey, "grid000"), wantOK: true, }, { name: "component tag fallback uses previous nspath token", hierarchy: constants.CompTagRecommendHierarchyType, inputSlice: []string{"grid000", "I"}, want: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "grid000"), wantOK: true, }, { name: "measurement fallback skips config token and uses component tag", hierarchy: constants.MeasTagRecommendHierarchyType, inputSlice: []string{"nspath", "comp_tag", "config", "m"}, want: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"), wantOK: true, }, { name: "config fallback has no parent specific set", hierarchy: constants.ConfigRecommendHierarchyType, inputSlice: []string{"nspath", "comp_tag", ""}, wantOK: false, }, { name: "missing previous token", hierarchy: constants.ZoneRecommendHierarchyType, inputSlice: []string{"z"}, wantOK: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := fallbackSpecificSetKey(tt.hierarchy, tt.inputSlice) if ok != tt.wantOK { t.Fatalf("expected ok %t, got %t", tt.wantOK, ok) } if got != tt.want { t.Fatalf("expected key %q, got %q", tt.want, got) } }) } } func TestShouldFallbackToInitialRecommend(t *testing.T) { tests := []struct { input string want bool }{ {input: "", want: false}, {input: ".", want: true}, {input: ".x", want: true}, {input: "..x", want: true}, {input: "...x", want: true}, {input: "grid000", want: false}, {input: "grid000.", want: false}, {input: "grid000.zone000", want: false}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { got := shouldFallbackToInitialRecommend(tt.input) if got != tt.want { t.Fatalf("expected %t, got %t", tt.want, got) } }) } }