fix(watcher): park through the brain root, keep a note edited mid-resolve, honour only real fences (#205)
From the second independent review of #207:
- parkForReview now skips the delete when the note changed since it was read
(an LLM call sits in that window; Syncthing can deliver a newer version)
- parked copies are created via brainfs (real dirs only, O_EXCL, no symlink
following), so a planted raw/needs-review symlink cannot redirect or hide a
note; this also removes the need for the guard allowlist entry added in
1e14c56, which is reverted
- a '----' line no longer closes the proposal block
Each has a test that failed first and a mutant that fails it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -271,3 +271,61 @@ func linesStartingWith(s, prefix string) int {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Resolving costs an LLM call. If Syncthing delivers a newer version of the note
|
||||
// meanwhile, deleting the original loses that version: the parked copy is stale.
|
||||
func TestProcessDir_ANoteEditedWhileTheResolverRunsIsNotDeleted(t *testing.T) {
|
||||
var llmCalls atomic.Int32
|
||||
var notePath string
|
||||
clf := &editingClassifier{}
|
||||
cfg, brainDir := wingTestCfg(t, wingResolver(clf, stubScorer{}), &llmCalls)
|
||||
notePath = filepath.Join(brainDir, "raw", "n.md")
|
||||
clf.edit = func() { require.NoError(t, os.WriteFile(notePath, []byte("NEWER and longer version"), 0o644)) }
|
||||
put(t, brainDir, "n.md", "ORIGINAL")
|
||||
|
||||
require.Empty(t, processDir(context.Background(), cfg, "2026-10-20"))
|
||||
|
||||
got, err := os.ReadFile(notePath)
|
||||
require.NoError(t, err, "the edited note must still be in raw/")
|
||||
assert.Equal(t, "NEWER and longer version", string(got))
|
||||
}
|
||||
|
||||
type editingClassifier struct{ edit func() }
|
||||
|
||||
func (c *editingClassifier) Classify(context.Context, string, []string) (wing.Suggestion, error) {
|
||||
c.edit()
|
||||
return wing.Suggestion{}, nil
|
||||
}
|
||||
|
||||
// A planted symlink at raw/needs-review must not redirect a parked note out of
|
||||
// the brain, nor lose it from the review queue.
|
||||
func TestProcessDir_AParkedNoteIsNeverWrittenThroughASymlinkedNeedsReview(t *testing.T) {
|
||||
var llmCalls atomic.Int32
|
||||
cfg, brainDir := wingTestCfg(t, wingResolver(&stubClassifier{}, stubScorer{}), &llmCalls)
|
||||
outside := t.TempDir()
|
||||
require.NoError(t, os.Symlink(outside, filepath.Join(brainDir, "raw", "needs-review")))
|
||||
put(t, brainDir, "n.md", "Note.")
|
||||
|
||||
errs := processDir(context.Background(), cfg, "2026-10-20")
|
||||
|
||||
assert.NotEmpty(t, errs, "parking is refused, loudly")
|
||||
entries, _ := os.ReadDir(outside)
|
||||
assert.Empty(t, entries, "nothing was written through the symlink")
|
||||
assert.True(t, has(brainDir, "raw/n.md"), "the note stays where it was")
|
||||
}
|
||||
|
||||
func TestWithProposal_ANonFenceDashLineDoesNotCloseTheBlock(t *testing.T) {
|
||||
out := withProposal("---\nfoo: bar\n----\nBody", wing.Decision{Wing: "radar", Confidence: 0.4, Reason: "r"})
|
||||
lines := strings.Split(out, "\n")
|
||||
require.Equal(t, "---", lines[0])
|
||||
closing := -1
|
||||
for i := 1; i < len(lines); i++ {
|
||||
if lines[i] == "---" {
|
||||
closing = i
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Positive(t, closing, "the proposal block is closed by a real fence")
|
||||
assert.Contains(t, strings.Join(lines[:closing], "\n"), "proposal_reason:")
|
||||
assert.NotContains(t, strings.Join(lines[:closing], "\n"), "foo: bar", "the unfenced note is body, not part of the block")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user