package brain_test import ( "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mathiasbq/hyperguild/ingestion/internal/brain" "github.com/mathiasbq/hyperguild/ingestion/internal/brainfs/brainfstest" ) func readIndex(t *testing.T, brainDir, wing string) string { t.Helper() b, _ := os.ReadFile(filepath.Join(brainDir, "wiki", wing, "_index.md")) return string(b) } // hyperguild#157 slice 5: BuildWingIndex listed wiki/// with // os.ReadDir and opened each note by path, so a symlinked note put the // target's title into _index.md, and a FIFO hung the index build. func TestBuildWingIndexRefusesSymlinkedNotesAndSurvivesSwapRaces(t *testing.T) { b := t.TempDir() facts := filepath.Join(b, "wiki", "w", "facts") require.NoError(t, os.MkdirAll(facts, 0o755)) require.NoError(t, os.WriteFile(filepath.Join(facts, "ok.md"), []byte("---\ntitle: zqinside\n---\n"), 0o644)) require.NoError(t, os.Symlink(brainfstest.Outside(t), filepath.Join(facts, "leak.md"))) inBrain := brainfstest.WriteSentinelAt(t, filepath.Join(b, "raw", "s.md")) require.NoError(t, os.Symlink(filepath.Join("..", "..", "..", "raw", "s.md"), filepath.Join(facts, "inlink.md"))) require.NoError(t, brain.BuildWingIndex(b, "w")) idx := readIndex(t, b, "w") assert.Contains(t, idx, "zqinside", "positive control") assert.NotContains(t, idx, brainfstest.Sentinel) assert.NotContains(t, idx, "leak") assert.NotContains(t, idx, "inlink") p := filepath.Join(facts, "r.md") read := func() string { _ = brain.BuildWingIndex(b, "w") return readIndex(t, b, "w") } brainfstest.AssertNoLeak(t, p, "---\ntitle: zqinside\n---\n", 1500*time.Millisecond, read) brainfstest.AssertNoLeakFrom(t, p, "---\ntitle: zqinside\n---\n", inBrain, 1500*time.Millisecond, read) brainfstest.AssertNoHang(t, p, "---\ntitle: zqinside\n---\n", 1500*time.Millisecond, 6*time.Second, func() { _ = read() }) } // A symlinked hall is skipped while a real hall next to it is indexed; a // symlinked wing is refused and nothing is written into its target. func TestBuildWingIndexSkipsASymlinkedHallAndRefusesASymlinkedWing(t *testing.T) { b := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(b, "raw", "facts"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(b, "raw", "facts", "r.md"), []byte("---\ntitle: zqrawonly\n---\n"), 0o644)) require.NoError(t, os.MkdirAll(filepath.Join(b, "wiki", "w", "decisions"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(b, "wiki", "w", "decisions", "d.md"), []byte("---\ntitle: zqrealhall\n---\n"), 0o644)) require.NoError(t, os.Symlink(filepath.Join("..", "..", "raw", "facts"), filepath.Join(b, "wiki", "w", "facts"))) require.NoError(t, os.Symlink(filepath.Join("..", "raw"), filepath.Join(b, "wiki", "alias"))) require.NoError(t, brain.BuildWingIndex(b, "w")) idx, err := os.ReadFile(filepath.Join(b, "wiki", "w", "_index.md")) require.NoError(t, err, "the wing index is written") assert.Contains(t, string(idx), "zqrealhall", "the real hall is indexed") assert.NotContains(t, string(idx), "zqrawonly", "the symlinked hall is not") assert.Error(t, brain.BuildWingIndex(b, "alias")) _, err = os.Stat(filepath.Join(b, "raw", "_index.md")) assert.True(t, os.IsNotExist(err), "an index was written into the symlinked wing's target") } // BuildAllWingIndexes indexes the real wings and skips a symlinked one. func TestBuildAllWingIndexesSkipsASymlinkedWing(t *testing.T) { b := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(b, "wiki", "w", "facts"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(b, "wiki", "w", "facts", "n.md"), []byte("---\ntitle: zqrealwing\n---\n"), 0o644)) require.NoError(t, os.MkdirAll(filepath.Join(b, "raw", "facts"), 0o755)) require.NoError(t, os.Symlink(filepath.Join("..", "raw"), filepath.Join(b, "wiki", "alias"))) require.NoError(t, brain.BuildAllWingIndexes(b)) idx, err := os.ReadFile(filepath.Join(b, "wiki", "w", "_index.md")) require.NoError(t, err) assert.Contains(t, string(idx), "zqrealwing") _, err = os.Stat(filepath.Join(b, "raw", "_index.md")) assert.True(t, os.IsNotExist(err)) } // A link to a symlinked note does not resolve: only regular files count as // entries, so StripUnresolvedLinks unwraps it. func TestStripUnresolvedLinksDoesNotCountASymlinkedNoteAsAnEntry(t *testing.T) { b := t.TempDir() facts := filepath.Join(b, "wiki", "w", "facts") require.NoError(t, os.MkdirAll(facts, 0o755)) require.NoError(t, os.WriteFile(filepath.Join(facts, "real.md"), []byte("x"), 0o644)) brainfstest.WriteSentinelAt(t, filepath.Join(b, "raw", "s.md")) require.NoError(t, os.Symlink(filepath.Join("..", "..", "..", "raw", "s.md"), filepath.Join(facts, "ghost.md"))) out, unlinked, err := brain.StripUnresolvedLinks(b, "see [[real]] and [[ghost]]") require.NoError(t, err) assert.Contains(t, out, "[[real]]", "positive control") assert.NotContains(t, out, "[[ghost]]") assert.Contains(t, unlinked, "ghost") } // A linked tree (knowledge/ or wiki/) that is not a real directory is // skipped, not an error: StripUnresolvedLinks runs before every brain_write, // and an error there loses the learning. func TestStripUnresolvedLinksSkipsALinkedTreeThatIsNotARealDirectory(t *testing.T) { for name, mk := range map[string]func(t *testing.T, b string){ "symlink to an in-brain dir": func(t *testing.T, b string) { require.NoError(t, os.MkdirAll(filepath.Join(b, "raw", "k"), 0o755)) require.NoError(t, os.Symlink(filepath.Join("raw", "k"), filepath.Join(b, "knowledge"))) }, "dangling symlink": func(t *testing.T, b string) { require.NoError(t, os.Symlink("nowhere", filepath.Join(b, "knowledge"))) }, "regular file": func(t *testing.T, b string) { require.NoError(t, os.WriteFile(filepath.Join(b, "knowledge"), []byte("x"), 0o644)) }, } { t.Run(name, func(t *testing.T) { b := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(b, "wiki", "w", "facts"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(b, "wiki", "w", "facts", "real.md"), []byte("x"), 0o644)) mk(t, b) out, _, err := brain.StripUnresolvedLinks(b, "learning [[real]] and [[gone]]") require.NoError(t, err) assert.Contains(t, out, "[[real]]", "links into wiki/ still resolve") assert.NotContains(t, out, "[[gone]]") }) } }