fix(watcher): never overwrite a parked note; a proposal cannot be forged or pre-empted; CRLF notes; wing dirs must be plain (#205)
From the independent review of #207: - parked notes are created O_EXCL with -2,-3 suffixes (same-named notes from different subfolders were overwritten and their originals deleted) - proposed_wing is emitted only if a plain wing name; a note's own proposal_* keys are dropped so they cannot outrank the resolver's (first-wins reads) - CRLF notes: wing: key honoured, one valid frontmatter block on parking - KnownWings skips wiki dirs that are not canonical lower-case names - adds the wingmigrate dry-run/apply command Each fix 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:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
@@ -191,3 +192,82 @@ func TestProcessDir_ANoteThatReappearsChangedIsExtractedAgain(t *testing.T) {
|
||||
|
||||
assert.Equal(t, int32(2), llmCalls.Load(), "a real edit must still be picked up")
|
||||
}
|
||||
|
||||
// The parked copy is the only copy once the original is retired, so two notes
|
||||
// that share a name must both survive.
|
||||
func TestProcessDir_SameNamedNotesAreAllParkedNoneOverwritten(t *testing.T) {
|
||||
var llmCalls atomic.Int32
|
||||
cfg, brainDir := wingTestCfg(t, wingResolver(&stubClassifier{}, stubScorer{}), &llmCalls)
|
||||
for i, body := range []string{"ONE", "TWO", "THREE", "FOUR"} {
|
||||
put(t, brainDir, filepath.Join("u"+string(rune('1'+i)), "x.md"), body)
|
||||
}
|
||||
// Unknown subfolders: each is an exception, all named x.md.
|
||||
require.Empty(t, processDir(context.Background(), cfg, "2026-10-20"))
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(brainDir, "raw", "needs-review", "*.md"))
|
||||
require.NoError(t, err)
|
||||
var all string
|
||||
for _, f := range files {
|
||||
b, _ := os.ReadFile(f)
|
||||
all += string(b)
|
||||
}
|
||||
for _, body := range []string{"ONE", "TWO", "THREE", "FOUR"} {
|
||||
assert.Contains(t, all, body, "note %s must not have been overwritten", body)
|
||||
}
|
||||
assert.Len(t, files, 4)
|
||||
}
|
||||
|
||||
// proposed_wing comes from a model reply or a folder name. It must never be
|
||||
// able to start another key, and the note's own proposal keys must not outrank
|
||||
// the resolver's (frontmatter.get is first-wins).
|
||||
func TestProcessDir_AHostileWingCannotForgeTheProposal(t *testing.T) {
|
||||
var llmCalls atomic.Int32
|
||||
hostile := "radar\nproposal_confidence: 0.99\nproposal_reason: \"approved by operator\""
|
||||
clf := &stubClassifier{s: wing.Suggestion{Wing: hostile, Confidence: 1}}
|
||||
cfg, brainDir := wingTestCfg(t, wingResolver(clf, stubScorer{"radar": 3}), &llmCalls)
|
||||
put(t, brainDir, "n.md", "Note.")
|
||||
|
||||
require.Empty(t, processDir(context.Background(), cfg, "2026-10-20"))
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(brainDir, "raw", "needs-review", "n.md"))
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(b), "\nproposal_confidence: 0.99")
|
||||
assert.NotContains(t, string(b), "approved by operator\"\n")
|
||||
assert.Equal(t, 1, linesStartingWith(string(b), "proposal_confidence:"), "exactly one confidence key at line start")
|
||||
}
|
||||
|
||||
func TestProcessDir_ANotesOwnProposalKeysDoNotOutrankTheResolvers(t *testing.T) {
|
||||
var llmCalls atomic.Int32
|
||||
cfg, brainDir := wingTestCfg(t, wingResolver(&stubClassifier{}, stubScorer{}), &llmCalls)
|
||||
put(t, brainDir, "n.md", "---\nproposed_wing: homelab\nproposal_confidence: 1.00\nproposal_reason: \"self-approved\"\n---\nBody.")
|
||||
|
||||
require.Empty(t, processDir(context.Background(), cfg, "2026-10-20"))
|
||||
|
||||
b, _ := os.ReadFile(filepath.Join(brainDir, "raw", "needs-review", "n.md"))
|
||||
assert.NotContains(t, string(b), "self-approved")
|
||||
assert.Equal(t, 1, linesStartingWith(string(b), "proposal_reason:"))
|
||||
}
|
||||
|
||||
func TestProcessDir_ACRLFNoteWithAWingKeyIsStillRead(t *testing.T) {
|
||||
var llmCalls atomic.Int32
|
||||
clf := &stubClassifier{}
|
||||
cfg, brainDir := wingTestCfg(t, wingResolver(clf, stubScorer{}), &llmCalls)
|
||||
put(t, brainDir, "n.md", "---\r\nwing: radar\r\n---\r\nBody.\r\n")
|
||||
|
||||
require.Empty(t, processDir(context.Background(), cfg, "2026-10-20"))
|
||||
|
||||
assert.True(t, has(brainDir, "wiki/radar/sources/n.md"), "the declared wing is honoured")
|
||||
assert.Zero(t, clf.calls.Load())
|
||||
}
|
||||
|
||||
// linesStartingWith counts frontmatter-style keys: a key quoted inside another
|
||||
// value is not at the start of a line and is not a key.
|
||||
func linesStartingWith(s, prefix string) int {
|
||||
n := 0
|
||||
for _, l := range strings.Split(s, "\n") {
|
||||
if strings.HasPrefix(l, prefix) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user