From 2f8b03fe15ab76b79a0504ee79f27771321f81ae Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 5 Oct 2026 22:44:45 +0200 Subject: [PATCH] fix(linkcheck): keep the write when the link index cannot be built (#178) StripUnresolvedLinks runs before every brain_write and brain_update. Any walk error below a linked tree's root (e.g. one unreadable subdirectory) made it return an error, and the write was refused: the learning was lost. Already true on main. It now logs a warning and returns the content unchanged: a partial index cannot tell an invented link from one into the unreadable part, so it unwraps nothing (never inventing a resolution) and the note is written with its links as given. Co-Authored-By: Claude Opus 5.5 --- .../internal/api/write_keeps_learning_test.go | 34 +++++++++++++++ ingestion/internal/brain/linkcheck.go | 7 +++- .../internal/brain/linkcheck_fallback_test.go | 41 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 ingestion/internal/api/write_keeps_learning_test.go create mode 100644 ingestion/internal/brain/linkcheck_fallback_test.go diff --git a/ingestion/internal/api/write_keeps_learning_test.go b/ingestion/internal/api/write_keeps_learning_test.go new file mode 100644 index 0000000..74639ef --- /dev/null +++ b/ingestion/internal/api/write_keeps_learning_test.go @@ -0,0 +1,34 @@ +package api_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mathiasbq/hyperguild/ingestion/internal/api" +) + +// hyperguild#178: an unreadable directory under wiki/ made the link check +// fail and brain_write refuse the note, losing the learning. The note is now +// written with its links as given. +func TestWriteNoteKeepsTheLearningWhenTheLinkCheckCannotIndex(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip("root ignores directory permissions") + } + brain := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(brain, "wiki", "w", "facts"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(brain, "wiki", "w", "facts", "real.md"), []byte("x"), 0o644)) + locked := filepath.Join(brain, "wiki", "other", "locked") + require.NoError(t, os.MkdirAll(locked, 0o755)) + require.NoError(t, os.Chmod(locked, 0o000)) + t.Cleanup(func() { _ = os.Chmod(locked, 0o755) }) + + rel, err := api.WriteNote(brain, api.WriteNoteOptions{Wing: "w", Hall: "facts", Filename: "n", Content: "learning [[real]] and [[gone]]"}) + require.NoError(t, err) + got, err := os.ReadFile(filepath.Join(brain, filepath.FromSlash(rel))) + require.NoError(t, err) + assert.Contains(t, string(got), "[[gone]]", "links kept as given when the index is partial") +} diff --git a/ingestion/internal/brain/linkcheck.go b/ingestion/internal/brain/linkcheck.go index 1553015..3496a09 100644 --- a/ingestion/internal/brain/linkcheck.go +++ b/ingestion/internal/brain/linkcheck.go @@ -47,7 +47,12 @@ var ( func StripUnresolvedLinks(brainDir, content string) (string, []string, error) { index, err := entryIndex(brainDir) if err != nil { - return "", nil, err + // A partial index cannot tell an invented link from one into the + // part it could not read, so unwrap nothing, and do not refuse the + // write either: this runs before every brain_write, and a refused + // write is a lost learning (hyperguild#178). + slog.Warn("link check skipped: cannot index the brain", "err", err) + return content, nil, nil } if len(index) == 0 { return content, nil, nil diff --git a/ingestion/internal/brain/linkcheck_fallback_test.go b/ingestion/internal/brain/linkcheck_fallback_test.go new file mode 100644 index 0000000..547fbd7 --- /dev/null +++ b/ingestion/internal/brain/linkcheck_fallback_test.go @@ -0,0 +1,41 @@ +package brain_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mathiasbq/hyperguild/ingestion/internal/brain" +) + +// hyperguild#178: StripUnresolvedLinks runs before every brain_write and +// brain_update. An unreadable directory under knowledge/ or wiki/ made it +// error, the write was refused and the learning lost. When the link index +// cannot be built, the content is now returned unchanged (no link is +// unwrapped, none is invented) and no error stops the write. +func TestStripUnresolvedLinksKeepsContentWhenTheIndexCannotBeBuilt(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", "real.md"), []byte("x"), 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) }) + + in := "learning [[real]] and [[gone]]" + out, unlinked, err := brain.StripUnresolvedLinks(b, in) + require.NoError(t, err) + assert.Equal(t, in, out, "content unchanged: no link unwrapped on a partial index") + assert.Empty(t, unlinked) + + require.NoError(t, os.Chmod(locked, 0o755)) + out, _, err = brain.StripUnresolvedLinks(b, in) + require.NoError(t, err) + assert.NotContains(t, out, "[[gone]]", "positive control: with a readable tree the strip runs") +}