Files
hyperguild/ingestion/cmd/server/main_test.go
T
mathiasandClaude Sonnet 5.5 0582dfdf6b
CI / Lint / Test / Vet (pull_request) Successful in 1m6s
CI / Mirror to GitHub (pull_request) Skipped
fix(server): wrap the shared client-name alternation like claudewatcher does (#209)
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>
2026-10-20 21:39:37 +02:00

171 lines
5.8 KiB
Go

// ingestion/cmd/server/main_test.go
package main
import (
"context"
"github.com/mathiasbq/hyperguild/ingestion/internal/wing"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mathiasbq/hyperguild/ingestion/internal/claudewatcher"
)
func TestClaudeSink_IngestWritesToNonIndexedArchiveNotWiki(t *testing.T) {
dir := t.TempDir()
sink := &claudeSink{brainDir: dir, logger: slog.New(slog.NewTextHandler(os.Stderr, nil))}
err := sink.Ingest(context.Background(), claudewatcher.Batch{
Host: "koala",
FilePath: "/host-home-claude/projects/-home-mathias-dev/abc.jsonl",
SessionID: "abc",
ProjectID: "-home-mathias-dev",
Turns: []claudewatcher.Turn{
{Type: "assistant", Content: "did a thing"},
},
})
require.NoError(t, err)
got, err := os.ReadFile(filepath.Join(dir, "archive", "claude-sessions", "koala", "session-koala-abc.md"))
require.NoError(t, err, "raw session dump must land in the non-indexed archive")
assert.Contains(t, string(got), "did a thing")
_, err = os.Stat(filepath.Join(dir, "wiki", "claude-sessions"))
assert.True(t, os.IsNotExist(err), "raw transcripts must never land under wiki/ (ai-sessions#10 — BM25 pollution)")
}
// --- #107: an unconfigured alert must announce itself ----------------------
func TestAlertingWarnings(t *testing.T) {
tests := []struct {
name string
env map[string]string
want []string // substrings that must appear across the warnings
none bool // true = expect no warnings at all
}{
{
// The failure this exists to prevent: two alerts dark in
// production, discovered only by firing a real event and watching
// nothing arrive (#100).
name: "notifier unconfigured names what will not fire",
env: map[string]string{},
want: []string{"BRAIN_NTFY_URL", "brain_edit_root", "audit"},
},
{
name: "configured notifier is silent",
env: map[string]string{"BRAIN_NTFY_URL": "http://ntfy/brain-alerts"},
none: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := alertingWarnings(func(k string) string { return tt.env[k] })
if tt.none {
assert.Empty(t, got)
return
}
joined := strings.Join(got, " ")
for _, w := range tt.want {
assert.Contains(t, joined, w)
}
})
}
}
// hyperguild#158: the claude-session archive was written with os.MkdirAll +
// os.WriteFile by path, so a symlinked archive directory redirected it.
func TestClaudeSinkDoesNotWriteThroughASymlinkedArchiveDir(t *testing.T) {
for name, mk := range map[string]func(t *testing.T, dir string) (link, target string){
"outside": func(t *testing.T, _ string) (string, string) { o := t.TempDir(); return o, o },
"in-brain": func(t *testing.T, dir string) (string, string) {
require.NoError(t, os.MkdirAll(filepath.Join(dir, "raw"), 0o755))
return filepath.Join("..", "raw"), filepath.Join(dir, "raw")
},
} {
t.Run(name, func(t *testing.T) {
dir := t.TempDir()
link, target := mk(t, dir)
require.NoError(t, os.Symlink(link, filepath.Join(dir, "archive")))
sink := &claudeSink{brainDir: dir, logger: slog.New(slog.NewTextHandler(os.Stderr, nil))}
err := sink.Ingest(context.Background(), claudewatcher.Batch{Host: "koala", SessionID: "abc",
Turns: []claudewatcher.Turn{{Type: "assistant", Content: "x"}}})
assert.Error(t, err)
ents, _ := os.ReadDir(target)
assert.Empty(t, ents, "the archive was written at the link's target")
})
}
}
func TestEnvFlag(t *testing.T) {
tests := []struct {
name, set string
want bool
wantErr bool
}{
{"unset is off", "", false, false},
{"1", "1", true, false},
{"true", "true", true, false},
{"TRUE", "TRUE", true, false},
{"0", "0", false, false},
{"false", "false", false, false},
{"a typo is an error, not silently off", "ture", false, true},
{"yes is not a bool here", "yes", false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("TEST_ENV_FLAG", tt.set)
got, err := envFlag("TEST_ENV_FLAG")
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
// 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)
}