fix(server): wrap the shared client-name alternation like claudewatcher does (#209)
CI / Lint / Test / Vet (pull_request) Successful in 1m6s
CI / Mirror to GitHub (pull_request) Skipped

The deployment injects CLAUDE_INGEST_CLIENT_BLOCK as a bare alternation of client
names; claudewatcher compiles it as (?i)\b(...)\b. The raw/ router used it raw, so
a short client name would match inside ordinary words and park clips that have
nothing to do with the client. INGEST_CLIENT_BLOCK stays a full regexp, used as
is. Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 21:39:37 +02:00
co-authored by Claude Sonnet 5.5
parent 4c2276b46b
commit 0582dfdf6b
2 changed files with 55 additions and 4 deletions
+16 -4
View File
@@ -150,6 +150,21 @@ func envInt(key string, fallback int) int {
return fallback return fallback
} }
// ingestClientPattern picks the client-content pattern for raw/ routing.
// INGEST_CLIENT_BLOCK is a complete regexp and is used as is. Otherwise the
// shared CLAUDE_INGEST_CLIENT_BLOCK is used, which the deployment injects as a
// bare alternation of client names that claudewatcher wraps in \b(...)\b: it is
// wrapped the same way here, or a short name would match inside ordinary words.
func ingestClientPattern(getenv func(string) string) string {
if p := getenv("INGEST_CLIENT_BLOCK"); p != "" {
return p
}
if names := getenv("CLAUDE_INGEST_CLIENT_BLOCK"); names != "" {
return `\b(` + names + `)\b`
}
return ""
}
// envFlag reads a boolean switch. Unset is off; a value strconv.ParseBool does // envFlag reads a boolean switch. Unset is off; a value strconv.ParseBool does
// not understand is an error, so a typo ("ture") cannot silently leave a // not understand is an error, so a typo ("ture") cannot silently leave a
// safety-relevant switch off. // safety-relevant switch off.
@@ -426,10 +441,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
if routing { if routing {
pattern := os.Getenv("INGEST_CLIENT_BLOCK") pattern := ingestClientPattern(os.Getenv)
if pattern == "" {
pattern = os.Getenv("CLAUDE_INGEST_CLIENT_BLOCK")
}
router, rerr := wing.NewRouter(brainDir, envOr("INGEST_DEFAULT_WING", "web-clip"), pattern) router, rerr := wing.NewRouter(brainDir, envOr("INGEST_DEFAULT_WING", "web-clip"), pattern)
if rerr != nil { if rerr != nil {
logger.Error("build wing router", "err", rerr) logger.Error("build wing router", "err", rerr)
+39
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"github.com/mathiasbq/hyperguild/ingestion/internal/wing"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
@@ -129,3 +130,41 @@ func TestEnvFlag(t *testing.T) {
}) })
} }
} }
// The deployment injects CLAUDE_INGEST_CLIENT_BLOCK as an ALTERNATION OF CLIENT
// NAMES that claudewatcher wraps in \b(...)\b. Used raw, a short name would match
// inside ordinary words and park half the clips.
func TestIngestClientPattern(t *testing.T) {
get := func(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
tests := []struct {
name string
env map[string]string
want string
}{
{"nothing set", map[string]string{}, ""},
{"explicit pattern is a full regexp, used as is", map[string]string{"INGEST_CLIENT_BLOCK": `acme\s+corp`, "CLAUDE_INGEST_CLIENT_BLOCK": "other"}, `acme\s+corp`},
{"the shared client-name alternation is wrapped like claudewatcher wraps it", map[string]string{"CLAUDE_INGEST_CLIENT_BLOCK": "dif|trucker"}, `\b(dif|trucker)\b`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, ingestClientPattern(get(tt.env)))
})
}
}
func TestIngestClientPattern_AShortClientNameDoesNotParkOrdinaryWords(t *testing.T) {
pattern := ingestClientPattern(func(k string) string {
if k == "CLAUDE_INGEST_CLIENT_BLOCK" {
return "dif|trucker"
}
return ""
})
r, err := wing.NewRouter(t.TempDir(), "web-clip", pattern)
require.NoError(t, err)
d := r.Resolve(context.Background(), wing.Note{Text: "a different approach to modifying prompts"})
assert.Equal(t, wing.Default, d.Source, "'dif' inside 'different' and 'modifying' is not the client")
d = r.Resolve(context.Background(), wing.Note{Text: "kickoff for the DIF project next week"})
assert.Equal(t, wing.Exception, d.Source)
d = r.Resolve(context.Background(), wing.Note{Text: "Trucker, the client, asked for"})
assert.Equal(t, wing.Exception, d.Source)
}