merge: land #195 (watcher/pipeline writes through the brain root, #158 part B) on top of inbox zero and the wing resolver
Reconciled with v0.25.0 semantics: - markers are only the fallback when the original cannot be removed, and are now written through the brain root; the refused set (size+mtime keyed) replaces the path-only 'stuck' set - #195's tests updated for inbox zero (archive written, original retired, no marker), and the planted-marker-link test keeps its point: nothing is ever written through the link, and the note is processed once - the 'cannot retire' test now counts archive copies; it counted LLM calls, which pipeline.Run's source-hash check hides (mutation: removing the remember step left it green) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/api"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/audit"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/capture"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/capturehttp"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/classification"
|
||||
@@ -90,11 +91,15 @@ func (s *claudeSink) Ingest(ctx context.Context, b claudewatcher.Batch) error {
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
slug := "session-" + b.Host + "-" + b.SessionID
|
||||
dest := filepath.Join(s.brainDir, "archive", "claude-sessions", b.Host, slug+".md")
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||
return fmt.Errorf("create claude-sessions archive dir: %w", err)
|
||||
dest := "archive/claude-sessions/" + b.Host + "/" + slug + ".md"
|
||||
// Written through the brain root, never through a symlinked directory
|
||||
// or file (hyperguild#158).
|
||||
bfs, err := brainfs.Open(s.brainDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open brain: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(dest, []byte(sb.String()), 0o644); err != nil {
|
||||
defer func() { _ = bfs.Close() }()
|
||||
if err := bfs.PutRegular(dest, []byte(sb.String())); err != nil {
|
||||
return fmt.Errorf("write claude session archive: %w", err)
|
||||
}
|
||||
s.logger.Debug("claude session archived (non-indexed)", "path", dest)
|
||||
|
||||
@@ -77,6 +77,30 @@ 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvFloat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user