package symlinkguard import ( "context" "os" "path/filepath" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // hyperguild#157: the brain dir is a Syncthing sendreceive folder and // Syncthing syncs symlinks; search, vectorstore, context and graph readers // follow them out of the brain. Until every reader goes through an os.Root, // a symlink anywhere in the brain is a tripwire: the server refuses to start // on one, and reports one that appears while it runs. func TestScanFindsSymlinksAtAnyDepthAndSkipsGit(t *testing.T) { brain := t.TempDir() outside := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(brain, "wiki", "w", "facts"), 0o755)) require.NoError(t, os.MkdirAll(filepath.Join(brain, ".git", "objects"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(brain, "wiki", "w", "facts", "ok.md"), []byte("x"), 0o644)) require.NoError(t, os.Symlink(filepath.Join(outside, "f"), filepath.Join(brain, "wiki", "w", "facts", "leak.md"))) require.NoError(t, os.Symlink(outside, filepath.Join(brain, "wiki", "dirlink"))) require.NoError(t, os.Symlink(outside, filepath.Join(brain, ".git", "objects", "alt"))) found, err := Scan(brain) require.NoError(t, err) assert.ElementsMatch(t, []string{"wiki/w/facts/leak.md", "wiki/dirlink"}, found) } func TestScanOfACleanBrainFindsNothing(t *testing.T) { brain := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(brain, "raw"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(brain, "raw", "n.md"), []byte("x"), 0o644)) found, err := Scan(brain) require.NoError(t, err) assert.Empty(t, found) } // The brain dir itself being a symlink is fine (the volume mount may be one); // only entries inside it are checked. func TestScanFollowsTheRootButNotEntries(t *testing.T) { real := t.TempDir() link := filepath.Join(t.TempDir(), "brain") require.NoError(t, os.Symlink(real, link)) require.NoError(t, os.WriteFile(filepath.Join(real, "n.md"), []byte("x"), 0o644)) found, err := Scan(link) require.NoError(t, err) assert.Empty(t, found) } type recNotifier struct { mu sync.Mutex msgs []string } func (r *recNotifier) Notify(_ context.Context, m string) error { r.mu.Lock() defer r.mu.Unlock() r.msgs = append(r.msgs, m) return nil } func (r *recNotifier) count() int { r.mu.Lock(); defer r.mu.Unlock(); return len(r.msgs) } // Watch reports a symlink that appears while running, once per distinct set. func TestWatchNotifiesOncePerNewSymlinkSet(t *testing.T) { brain := t.TempDir() n := &recNotifier{} ctx, cancel := context.WithCancel(context.Background()) defer cancel() go Watch(ctx, brain, 10*time.Millisecond, n) time.Sleep(50 * time.Millisecond) assert.Zero(t, n.count(), "a clean brain raises nothing") require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(brain, "planted"))) require.Eventually(t, func() bool { return n.count() == 1 }, 2*time.Second, 10*time.Millisecond) time.Sleep(60 * time.Millisecond) assert.Equal(t, 1, n.count(), "the same symlink set is reported once, not every tick") assert.Contains(t, n.msgs[0], "planted") }