fix(watcher): refusals end in a terminal state; pin every converted writer (#158)
Review round 1 on PR #195: - A refused marker or archive write left no marker, so the note was reprocessed every interval (archive copies piling up; an LLM call per round on the failure path). The marker check now uses Lstat (any entry at the marker path counts, so a dangling marker link skips the note instead of looping), and a note whose archive copy or marker write is refused is remembered (path, size, mtime) and not retried until it changes. - cleanRel no longer refuses a backslash: it is a legal filename byte on Linux, not a traversal, and a raw note named with one was never archived or marked. - Tests now reach every converted writer: the log.md append (real raw/failed so the log is reached, symlinked log.md), a dangling marker link, the refused-retry loop with a positive control, a backslash name, a buildAndWrite swap race, and the claude-session archive with in-brain and outside symlinked archive dirs. The stale lint allowlist entry for appendWatcherLog is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,3 +76,27 @@ func TestAlertingWarnings(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// hyperguild#158: the claude-session archive was written with os.MkdirAll +
|
||||
// os.WriteFile by path, so a symlinked archive directory redirected it.
|
||||
func TestClaudeSinkDoesNotWriteThroughASymlinkedArchiveDir(t *testing.T) {
|
||||
for name, mk := range map[string]func(t *testing.T, dir string) (link, target string){
|
||||
"outside": func(t *testing.T, _ string) (string, string) { o := t.TempDir(); return o, o },
|
||||
"in-brain": func(t *testing.T, dir string) (string, string) {
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "raw"), 0o755))
|
||||
return filepath.Join("..", "raw"), filepath.Join(dir, "raw")
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
link, target := mk(t, dir)
|
||||
require.NoError(t, os.Symlink(link, filepath.Join(dir, "archive")))
|
||||
sink := &claudeSink{brainDir: dir, logger: slog.New(slog.NewTextHandler(os.Stderr, nil))}
|
||||
err := sink.Ingest(context.Background(), claudewatcher.Batch{Host: "koala", SessionID: "abc",
|
||||
Turns: []claudewatcher.Turn{{Type: "assistant", Content: "x"}}})
|
||||
assert.Error(t, err)
|
||||
ents, _ := os.ReadDir(target)
|
||||
assert.Empty(t, ents, "the archive was written at the link's target")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user