Skip to content

Commit 0e10d85

Browse files
committed
guard: add fuzzy fallback for versioned group process names
1 parent fc4f63a commit 0e10d85

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

internal/guard/match.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ func matcherMatches(m config.Matcher, p Proc) bool {
4040
return true
4141
}
4242
}
43+
for _, s := range m.NameContains {
44+
needle := normalizeProcName(s)
45+
if needle == "" {
46+
continue
47+
}
48+
if fuzzyNameContainsMatch(p.NameNorm, needle) {
49+
return true
50+
}
51+
}
4352
for _, s := range m.ExeContains {
4453
needle := strings.ToLower(strings.TrimSpace(s))
4554
if needle == "" {
@@ -57,6 +66,93 @@ func shouldUseExactNameMatch(needle string) bool {
5766
return len(needle) <= 5 && !strings.Contains(needle, " ")
5867
}
5968

69+
func fuzzyNameContainsMatch(nameNorm string, needle string) bool {
70+
if nameNorm == needle {
71+
return true
72+
}
73+
74+
if trimmed := trimVersionLikeSuffix(nameNorm); trimmed != "" && trimmed != nameNorm {
75+
if trimmed == needle {
76+
return true
77+
}
78+
if !shouldUseExactNameMatch(needle) && strings.Contains(trimmed, needle) {
79+
return true
80+
}
81+
}
82+
83+
if shouldUseExactNameMatch(needle) {
84+
return hasDelimitedAffix(nameNorm, needle)
85+
}
86+
return false
87+
}
88+
89+
func hasDelimitedAffix(nameNorm string, needle string) bool {
90+
if len(nameNorm) <= len(needle) {
91+
return false
92+
}
93+
if strings.HasPrefix(nameNorm, needle) && isNameDelimiter(rune(nameNorm[len(needle)])) {
94+
return true
95+
}
96+
if strings.HasSuffix(nameNorm, needle) && isNameDelimiter(rune(nameNorm[len(nameNorm)-len(needle)-1])) {
97+
return true
98+
}
99+
return false
100+
}
101+
102+
func trimVersionLikeSuffix(nameNorm string) string {
103+
trimmed := strings.TrimSpace(nameNorm)
104+
for {
105+
base, suffix, ok := splitTrailingSegment(trimmed)
106+
if !ok || !looksVersionLikeSegment(suffix) {
107+
return trimmed
108+
}
109+
trimmed = strings.TrimSpace(base)
110+
}
111+
}
112+
113+
func splitTrailingSegment(s string) (string, string, bool) {
114+
if s == "" {
115+
return "", "", false
116+
}
117+
for i := len(s) - 1; i >= 0; i-- {
118+
if isNameDelimiter(rune(s[i])) {
119+
if i == len(s)-1 {
120+
return "", "", false
121+
}
122+
return s[:i], s[i+1:], true
123+
}
124+
}
125+
return "", "", false
126+
}
127+
128+
func looksVersionLikeSegment(seg string) bool {
129+
seg = strings.TrimSpace(seg)
130+
if seg == "" {
131+
return false
132+
}
133+
hasDigit := false
134+
for _, r := range seg {
135+
if r >= '0' && r <= '9' {
136+
hasDigit = true
137+
continue
138+
}
139+
if (r >= 'a' && r <= 'z') || r == '.' {
140+
continue
141+
}
142+
return false
143+
}
144+
return hasDigit
145+
}
146+
147+
func isNameDelimiter(r rune) bool {
148+
switch r {
149+
case ' ', '-', '_', '.', '(', ')', '[', ']', '{', '}':
150+
return true
151+
default:
152+
return false
153+
}
154+
}
155+
60156
func groupRootMatch(g config.GroupSpec, p Proc) bool {
61157
for _, m := range g.RootMatchers {
62158
if matcherMatches(m, p) {

internal/guard/match_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package guard
2+
3+
import (
4+
"testing"
5+
6+
"remem/internal/config"
7+
)
8+
9+
func TestMatcherMatchesExactBeforeFuzzy(t *testing.T) {
10+
m := config.Matcher{NameContains: []string{"codex"}}
11+
p := Proc{NameNorm: "codex"}
12+
if !matcherMatches(m, p) {
13+
t.Fatalf("expected exact match to succeed")
14+
}
15+
}
16+
17+
func TestMatcherMatchesFuzzyVersionSuffixForShortName(t *testing.T) {
18+
m := config.Matcher{NameContains: []string{"codex"}}
19+
p := Proc{NameNorm: "codex-1.2.3"}
20+
if !matcherMatches(m, p) {
21+
t.Fatalf("expected fuzzy suffix match to succeed")
22+
}
23+
}
24+
25+
func TestMatcherMatchesFuzzyVersionSuffixForLongName(t *testing.T) {
26+
m := config.Matcher{NameContains: []string{"firefox"}}
27+
p := Proc{NameNorm: "firefox-135.0.1"}
28+
if !matcherMatches(m, p) {
29+
t.Fatalf("expected long-name fuzzy suffix match to succeed")
30+
}
31+
}
32+
33+
func TestMatcherMatchesDoesNotUseMidStringContainsForShortName(t *testing.T) {
34+
m := config.Matcher{NameContains: []string{"code"}}
35+
p := Proc{NameNorm: "xcode-helper"}
36+
if matcherMatches(m, p) {
37+
t.Fatalf("expected short-name fallback to avoid mid-string false positive")
38+
}
39+
}
40+
41+
func TestMatcherMatchesShortNameAllowsDelimitedSuffix(t *testing.T) {
42+
m := config.Matcher{NameContains: []string{"code"}}
43+
p := Proc{NameNorm: "code-insiders"}
44+
if !matcherMatches(m, p) {
45+
t.Fatalf("expected short-name delimited suffix match to succeed")
46+
}
47+
}

0 commit comments

Comments
 (0)