fix(wing): the client filter survives cheap evasions; lookalike and reserved folder names are refused (review of #211)
- client pattern is matched against the text and against an NFKC-folded, format-character-free, dash-normalised, whitespace-free form (non-breaking hyphen, zero-width characters, soft hyphen, line splits, fullwidth letters). x/text/unicode/norm was already in the module graph as an indirect dependency; this makes it direct, no new download. - folder/frontmatter wing names must be ASCII (strings.ToLower folds the Kelvin sign to 'k') and must not be sources/concepts/entities/wiki; such names are never proposed as a wing, and a reserved default is a startup error Mutation-checked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,7 @@ func TestRouter_ANameThatIsNotPlainIsRefusedAsSuch(t *testing.T) {
|
||||
d := r.Resolve(context.Background(), wing.Note{Dir: bad, Text: "x"})
|
||||
assert.Equal(t, wing.Exception, d.Source)
|
||||
assert.Empty(t, d.Wing, "a malformed name is never echoed as a proposal")
|
||||
assert.Contains(t, d.Reason, "not a plain wing name")
|
||||
assert.Contains(t, d.Reason, "not a usable wing name")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -112,3 +112,63 @@ func TestNewRouter(t *testing.T) {
|
||||
assert.Error(t, err, "a client filter that silently does nothing is worse than none")
|
||||
})
|
||||
}
|
||||
|
||||
// From the review of #211: the content filter is the only guard on the default
|
||||
// lane, so it must survive the cheap evasions.
|
||||
func TestRouter_ClientMarkerSurvivesCheapEvasions(t *testing.T) {
|
||||
r := wing.Router{BrainDir: routerBrain(t), Default: "web-clip", ClientBlock: regexp.MustCompile(`(?i)acme-client`)}
|
||||
tests := []struct{ name, text string }{
|
||||
{"non-breaking hyphen", "notes on acme‑client"},
|
||||
{"zero-width joiner inside", "notes on acme\u200d-client"},
|
||||
{"zero-width space inside", "notes on ac\u200bme-client"},
|
||||
{"split across lines", "notes on acme-\nclient"},
|
||||
{"spaced hyphen", "notes on acme - client"},
|
||||
{"fullwidth letters", "notes on acme-client"},
|
||||
{"soft hyphen", "notes on acme\u00ad-client"},
|
||||
{"en dash", "notes on acme–client"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := r.Resolve(context.Background(), wing.Note{Text: tt.text})
|
||||
assert.Equal(t, wing.Exception, d.Source, tt.text)
|
||||
})
|
||||
}
|
||||
t.Run("an unrelated text still files", func(t *testing.T) {
|
||||
d := r.Resolve(context.Background(), wing.Note{Text: "a talk about agents and a client-server model"})
|
||||
assert.Equal(t, wing.Default, d.Source)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouter_FolderNamesAreAsciiAndNotReserved(t *testing.T) {
|
||||
dir := routerBrain(t)
|
||||
writePage(t, dir, "wiki/kubernetes/facts/a.md", "x")
|
||||
r := wing.Router{BrainDir: dir, Default: "web-clip"}
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
}{
|
||||
{"Kelvin sign lowercases to an ASCII k", "Kubernetes"},
|
||||
{"cyrillic lookalike", "hоmelab"},
|
||||
{"retired layout name: sources", "sources"},
|
||||
{"retired layout name: concepts", "concepts"},
|
||||
{"retired layout name: entities", "entities"},
|
||||
{"wiki", "wiki"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := r.Resolve(context.Background(), wing.Note{Dir: tt.dir, Text: "x"})
|
||||
assert.Equal(t, wing.Exception, d.Source)
|
||||
assert.Empty(t, d.Wing, "never proposed as a wing")
|
||||
})
|
||||
}
|
||||
t.Run("plain ASCII case folding still works", func(t *testing.T) {
|
||||
d := r.Resolve(context.Background(), wing.Note{Dir: "KubernetEs", Text: "x"})
|
||||
assert.Equal(t, wing.Explicit, d.Source)
|
||||
assert.Equal(t, "kubernetes", d.Wing)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewRouter_AReservedDefaultIsRefused(t *testing.T) {
|
||||
_, err := wing.NewRouter(routerBrain(t), "sources", "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user