diff --git a/ingestion/internal/brain/detect_root_test.go b/ingestion/internal/brain/detect_root_test.go index 93d5162..c5f451c 100644 --- a/ingestion/internal/brain/detect_root_test.go +++ b/ingestion/internal/brain/detect_root_test.go @@ -19,25 +19,37 @@ import ( // every note with a blocking os.Open, so a FIFO named *.md anywhere under // wiki/ hung every brain_write and brain_update (wikiUpkeep -> AutoTunnel), // and a symlinked note's title became a tunnel candidate. -func TestDetectTunnelsReturnsPromptlyWithAFIFOUnderWiki(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 Phrase\n---\n"), 0o644)) - require.NoError(t, syscall.Mkfifo(filepath.Join(facts, "f.md"), 0o644)) - done := make(chan []brain.TunnelCandidate, 1) - go func() { - cs, _ := brain.DetectTunnels(b, "text mentioning zqinside phrase here") - done <- cs - }() - select { - case cs := <-done: - assert.Contains(t, fmt.Sprintf("%+v", cs), "Zqinside Phrase", "positive control") - case <-time.After(3 * time.Second): - if r, err := os.OpenFile(filepath.Join(facts, "f.md"), os.O_WRONLY|syscall.O_NONBLOCK, 0); err == nil { - _ = r.Close() - } - t.Fatal("DetectTunnels blocked on a FIFO") +func TestDetectTunnelsReturnsPromptlyWithAFIFOUnderWikiWithAndWithoutAWriter(t *testing.T) { + for _, withWriter := range []bool{false, true} { + t.Run(fmt.Sprintf("writer=%v", withWriter), func(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 Phrase\n---\n"), 0o644)) + fifo := filepath.Join(facts, "f.md") + require.NoError(t, syscall.Mkfifo(fifo, 0o644)) + if withWriter { + // A writer held open: a reader that opens the FIFO gets no + // EOF and parks, unless the FIFO is never opened. + w, err := os.OpenFile(fifo, os.O_RDWR, 0) + require.NoError(t, err) + t.Cleanup(func() { _ = w.Close() }) + } + done := make(chan []brain.TunnelCandidate, 1) + go func() { + cs, _ := brain.DetectTunnels(b, "text mentioning zqinside phrase here") + done <- cs + }() + select { + case cs := <-done: + assert.Contains(t, fmt.Sprintf("%+v", cs), "Zqinside Phrase", "positive control") + case <-time.After(3 * time.Second): + if r, err := os.OpenFile(fifo, os.O_WRONLY|syscall.O_NONBLOCK, 0); err == nil { + _ = r.Close() + } + t.Fatal("DetectTunnels blocked on a FIFO") + } + }) } } @@ -45,16 +57,18 @@ func TestDetectTunnelsRefusesSymlinkedNotesAndSurvivesSwapRaces(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 Phrase\n---\n"), 0o644)) + // The static note's title differs from the raced note's, so the + // harness's "inside was read" floor can only be met by reading r.md. + require.NoError(t, os.WriteFile(filepath.Join(facts, "ok.md"), []byte("---\ntitle: Zqstatic Words\n---\n"), 0o644)) require.NoError(t, os.Symlink(brainfstest.Outside(t), filepath.Join(facts, "out.md"))) inBrain := brainfstest.WriteSentinelAt(t, filepath.Join(b, "raw", "s.md")) require.NoError(t, os.Symlink(filepath.Join("..", "..", "..", "raw", "s.md"), filepath.Join(facts, "in.md"))) - content := "zqinside phrase and " + brainfstest.Sentinel + content := "zqstatic words, zqinside phrase and " + brainfstest.Sentinel cs, err := brain.DetectTunnels(b, content) require.NoError(t, err) got := fmt.Sprintf("%+v", cs) - assert.Contains(t, got, "Zqinside Phrase", "positive control") + assert.Contains(t, got, "Zqstatic Words", "positive control") assert.NotContains(t, got, brainfstest.Sentinel) p := filepath.Join(facts, "r.md") @@ -77,3 +91,35 @@ func TestDetectTunnelsSkipsAWikiThatIsNotARealDirectory(t *testing.T) { require.NoError(t, err) assert.Empty(t, cs) } + +// An unreadable directory under wiki/ is a walk error and DetectTunnels +// reports it, as filepath.WalkDir did; it is not silently dropped. +func TestDetectTunnelsReportsAnUnreadableDirectoryUnderWiki(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip("root ignores directory permissions") + } + 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", "ok.md"), []byte("---\ntitle: Zqinside Phrase\n---\n"), 0o644)) + locked := filepath.Join(b, "wiki", "other", "locked") + require.NoError(t, os.MkdirAll(locked, 0o755)) + require.NoError(t, os.Chmod(locked, 0o000)) + t.Cleanup(func() { _ = os.Chmod(locked, 0o755) }) + _, err := brain.DetectTunnels(b, "zqinside phrase") + assert.ErrorIs(t, err, os.ErrPermission) +} + +// _index.md files are generated, not notes: their title never becomes a +// candidate. +func TestDetectTunnelsSkipsIndexFiles(t *testing.T) { + b := t.TempDir() + w := filepath.Join(b, "wiki", "w") + require.NoError(t, os.MkdirAll(filepath.Join(w, "facts"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(w, "_index.md"), []byte("---\ntitle: Zqindex Words\n---\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(w, "facts", "ok.md"), []byte("---\ntitle: Zqinside Phrase\n---\n"), 0o644)) + cs, err := brain.DetectTunnels(b, "zqindex words and zqinside phrase") + require.NoError(t, err) + got := fmt.Sprintf("%+v", cs) + assert.Contains(t, got, "Zqinside Phrase", "positive control") + assert.NotContains(t, got, "Zqindex") +} diff --git a/ingestion/internal/brain/index.go b/ingestion/internal/brain/index.go index 020b164..d700b74 100644 --- a/ingestion/internal/brain/index.go +++ b/ingestion/internal/brain/index.go @@ -116,7 +116,6 @@ func collectWingEntries(bfs *brainfs.Brain, wingRel string) ([]noteEntry, error) return out, nil } - // readTitleAndCreatedIn is readTitleAndCreated through the brain root, // regular files only (hyperguild#157). func readTitleAndCreatedIn(bfs *brainfs.Brain, rel, slug string) (string, string) {