Files
hyperguild/ingestion/internal/wing/router_test.go
T
mathiasandClaude Sonnet 5.5 6b6d430383 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>
2026-10-20 20:29:09 +02:00

175 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package wing_test
import (
"context"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mathiasbq/hyperguild/ingestion/internal/wing"
)
func routerBrain(t *testing.T) string {
dir := t.TempDir()
writePage(t, dir, "wiki/homelab/facts/a.md", "x")
writePage(t, dir, "wiki/rikatillsammans/facts/b.md", "x")
return dir
}
func TestRouter_Decisions(t *testing.T) {
tests := []struct {
name string
note wing.Note
want wing.Source
wantWing string
}{
{"no signal goes to the default wing", wing.Note{Text: "a talk about agents"}, wing.Default, "web-clip"},
{"a subfolder naming an existing wing places there", wing.Note{Dir: "homelab", Text: "x"}, wing.Explicit, "homelab"},
{"a deliberate subfolder into a client wing is the owner's call and is placed", wing.Note{Dir: "rikatillsammans", Text: "x"}, wing.Explicit, "rikatillsammans"},
{"frontmatter wing places like a subfolder", wing.Note{FrontmatterWing: "homelab", Text: "x"}, wing.Explicit, "homelab"},
{"subfolder is case-insensitive", wing.Note{Dir: "HomeLab", Text: "x"}, wing.Explicit, "homelab"},
{"a typo never creates a wing", wing.Note{Dir: "hoemlab", Text: "x"}, wing.Exception, "hoemlab"},
{"conflicting signals are parked", wing.Note{Dir: "homelab", FrontmatterWing: "rikatillsammans", Text: "x"}, wing.Exception, ""},
{"the default wing may be named explicitly even before it exists", wing.Note{Dir: "web-clip", Text: "x"}, wing.Explicit, "web-clip"},
{"client marker with no signal is parked, not defaulted", wing.Note{Text: "notes from the ACME-CLIENT kickoff"}, wing.Exception, ""},
{"client marker does not block a deliberate subfolder", wing.Note{Dir: "homelab", Text: "ACME-CLIENT"}, wing.Explicit, "homelab"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := wing.Router{BrainDir: routerBrain(t), Default: "web-clip", ClientBlock: regexp.MustCompile(`(?i)acme-client`)}
d := r.Resolve(context.Background(), tt.note)
assert.Equal(t, tt.want, d.Source)
assert.Equal(t, tt.wantWing, d.Wing)
assert.NotEmpty(t, d.Reason)
})
}
}
func TestRouter_ReadsTheWingsFreshOnEveryDecision(t *testing.T) {
dir := routerBrain(t)
r := wing.Router{BrainDir: dir, Default: "web-clip"}
assert.Equal(t, wing.Exception, r.Resolve(context.Background(), wing.Note{Dir: "newwing", Text: "x"}).Source)
writePage(t, dir, "wiki/newwing/facts/c.md", "x") // a wing created after startup
assert.Equal(t, wing.Explicit, r.Resolve(context.Background(), wing.Note{Dir: "newwing", Text: "x"}).Source,
"a wing made while the server runs is usable without a restart")
}
func TestRouter_AnUnreadableBrainParksInsteadOfGuessing(t *testing.T) {
r := wing.Router{BrainDir: t.TempDir() + "/missing", Default: "web-clip"}
d := r.Resolve(context.Background(), wing.Note{Dir: "homelab", Text: "x"})
assert.Equal(t, wing.Exception, d.Source)
}
func TestRouter_WithoutADefaultAnUnsignalledNoteIsParked(t *testing.T) {
r := wing.Router{BrainDir: routerBrain(t)}
assert.Equal(t, wing.Exception, r.Resolve(context.Background(), wing.Note{Text: "x"}).Source)
}
func TestRouter_ADefaultThatIsNotAPlainNameIsRefused(t *testing.T) {
r := wing.Router{BrainDir: routerBrain(t), Default: "../escape"}
assert.Equal(t, wing.Exception, r.Resolve(context.Background(), wing.Note{Text: "x"}).Source)
}
func TestRouter_ANameThatIsNotPlainIsRefusedAsSuch(t *testing.T) {
for _, bad := range []string{"../x", "a/b", "has space", "dot.ted"} {
t.Run(bad, func(t *testing.T) {
// Default equals the bad name: the "named explicitly" shortcut for the
// default wing must not wave a malformed name through.
r := wing.Router{BrainDir: routerBrain(t), Default: bad}
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 usable wing name")
})
}
}
func TestNewRouter(t *testing.T) {
dir := routerBrain(t)
t.Run("valid", func(t *testing.T) {
r, err := wing.NewRouter(dir, "web-clip", "acme-client|globex")
assert.NoError(t, err)
d := r.Resolve(context.Background(), wing.Note{Text: "kickoff with GLOBEX"})
assert.Equal(t, wing.Exception, d.Source, "the marker is matched case-insensitively")
})
t.Run("no marker pattern means no content check", func(t *testing.T) {
r, err := wing.NewRouter(dir, "web-clip", "")
assert.NoError(t, err)
assert.Equal(t, wing.Default, r.Resolve(context.Background(), wing.Note{Text: "anything"}).Source)
})
t.Run("a default that is not a plain wing name is a startup error", func(t *testing.T) {
_, err := wing.NewRouter(dir, "Web Clip", "")
assert.Error(t, err)
})
t.Run("an empty default is a startup error", func(t *testing.T) {
_, err := wing.NewRouter(dir, "", "")
assert.Error(t, err)
})
t.Run("a pattern that does not compile is a startup error", func(t *testing.T) {
_, err := wing.NewRouter(dir, "web-clip", "([unclosed")
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)
}