BuildWingIndex/BuildAllWingIndexes listed wiki/<wing>/<hall>/ with os.ReadDir and opened each note by path: a symlinked note put the target's title into _index.md (110k leaks in the swap race), a symlinked wing was followed and its _index.md written into the target, and a FIFO hung the build (it runs on every brain_write). - New brainfs.ReadDirDirs: real subdirectories only, every component of the dir a real directory. Wings and halls are listed with it and ReadDirRegular; titles are read with ReadRegular (readTitleAndCreatedIn). A symlinked wing is an error; a symlinked hall is skipped. - linkcheck entryIndex walks with WalkRegular: a symlinked .md name no longer makes a [[link]] resolve. - readTitleAndCreated (path-based) stays for tunnel.DetectTunnels until PR #170 lands; that is slice 5b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.7 KiB
Go
84 lines
3.7 KiB
Go
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/<wing>/<hall>/ 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 every brain_write.
|
|
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 wing or hall directory that is a symlink is not listed, and nothing is
|
|
// written into its target.
|
|
func TestBuildWingIndexDoesNotFollowASymlinkedWingOrHall(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"), 0o755))
|
|
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")))
|
|
|
|
_ = brain.BuildWingIndex(b, "w")
|
|
assert.NotContains(t, readIndex(t, b, "w"), "zqrawonly", "symlinked hall")
|
|
|
|
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")
|
|
}
|
|
|
|
// 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")
|
|
}
|