// ingestion/cmd/server/main_test.go package main import ( "context" "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") }) } }