fix(watcher): don't re-ingest a note that cannot be retired; pin archive-failure keeps original
When both Remove and the marker write fail (read-only raw/), remember the path in memory so later polls skip it instead of writing another archive copy every tick. Adds a test that a failed archive copy leaves the original in raw/, mutation-checked against removing the early return. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -387,3 +387,60 @@ func TestProcessDir_DoesNotHangUnderAFIFOSwapRace(t *testing.T) {
|
||||
t.Fatalf("the watcher hung on a swapped-in FIFO after %d iterations", iters.Load())
|
||||
}
|
||||
}
|
||||
|
||||
// A note whose removal AND marker write both fail (read-only raw/) must not be
|
||||
// re-ingested on every poll: each pass would write another archive copy.
|
||||
func TestProcessDir_DoesNotReingestANoteItCannotRetire(t *testing.T) {
|
||||
brainDir := setupBrainDir(t)
|
||||
rawDir := filepath.Join(brainDir, "raw")
|
||||
require.NoError(t, os.WriteFile(filepath.Join(rawDir, "stuck.md"), []byte("Content."), 0o644))
|
||||
// processed/ must exist and stay writable; only raw/ itself is locked.
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(rawDir, "processed"), 0o755))
|
||||
require.NoError(t, os.Chmod(rawDir, 0o555))
|
||||
t.Cleanup(func() { _ = os.Chmod(rawDir, 0o755) })
|
||||
|
||||
var calls atomic.Int32
|
||||
cfg := Config{
|
||||
BrainDir: brainDir,
|
||||
Pipeline: pipeline.Config{
|
||||
Complete: func(ctx context.Context, s, u string) (string, error) {
|
||||
calls.Add(1)
|
||||
return successComplete(pipeline.RawPage{Title: "Stuck", Type: "source", Subtype: "article", Content: "x\n"})(ctx, s, u)
|
||||
},
|
||||
Schema: "# Schema",
|
||||
},
|
||||
stuck: &sync.Map{},
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
processDir(context.Background(), cfg, "2026-10-19")
|
||||
}
|
||||
assert.Equal(t, int32(1), calls.Load(), "a note that cannot be retired is ingested once, not once per poll")
|
||||
}
|
||||
|
||||
// If the archive copy cannot be written, the original is the only copy left:
|
||||
// it must stay in raw/.
|
||||
func TestProcessDir_KeepsTheOriginalWhenTheArchiveCopyFails(t *testing.T) {
|
||||
brainDir := setupBrainDir(t)
|
||||
rawDir := filepath.Join(brainDir, "raw")
|
||||
note := filepath.Join(rawDir, "keep-me.md")
|
||||
require.NoError(t, os.WriteFile(note, []byte("Content."), 0o644))
|
||||
// The dated archive dir exists but is read-only: MkdirAll succeeds, the
|
||||
// copy itself (os.Create) fails.
|
||||
dated := filepath.Join(rawDir, "processed", "2026-10-19")
|
||||
require.NoError(t, os.MkdirAll(dated, 0o755))
|
||||
require.NoError(t, os.Chmod(dated, 0o555))
|
||||
t.Cleanup(func() { _ = os.Chmod(dated, 0o755) })
|
||||
|
||||
cfg := Config{
|
||||
BrainDir: brainDir,
|
||||
Pipeline: pipeline.Config{
|
||||
Complete: successComplete(pipeline.RawPage{Title: "Keep Me", Type: "source", Subtype: "article", Content: "x\n"}),
|
||||
Schema: "# Schema",
|
||||
},
|
||||
}
|
||||
errs := processDir(context.Background(), cfg, "2026-10-19")
|
||||
|
||||
assert.NotEmpty(t, errs, "the failed archive is reported")
|
||||
_, err := os.Stat(note)
|
||||
assert.NoError(t, err, "original stays in raw/ when it could not be archived")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user