Files
edfe46c0aa post_sync: run a local command when teammates' changes land (#163)
Inbound sync was invisible to the machine it landed on — a local index,
cache or notifier had to poll. A `post_sync` command in the folder's own
.bdrive/config.json now runs once per cycle that applied peer changes,
with the batch as JSON on stdin.

The batch rides out on a new Result.Inbound rather than the inbound
spool: DrainInbound is destructive and `bdrive sync --hook` is its only
consumer, so a second drainer would silently empty the agent's
"teammates changed X" context. Both are kept, and both comments now say
why.

Cycle becomes a thin wrapper over cycleLocked so the hook is spawned
after the volume flock drops — a property of the code shape, not a rule
each of the seven call sites has to remember.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 11:04:23 -07:00

58 lines
1.8 KiB
Go

package config
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
// TestPostSyncRoundTrip: the hook command survives save/load, and stays out of
// the file entirely when unset.
func TestPostSyncRoundTrip(t *testing.T) {
folder := t.TempDir()
if _, err := SaveProject(folder, Project{ID: "m-1234abcd", Volume: "wiki", PostSync: "qmd update && qmd embed"}); err != nil {
t.Fatal(err)
}
got, ok, err := LoadProject(folder)
if err != nil || !ok {
t.Fatalf("LoadProject: %v (ok=%v)", err, ok)
}
if got.PostSync != "qmd update && qmd embed" {
t.Fatalf("post_sync = %q, want the saved command", got.PostSync)
}
if _, err := SaveProject(folder, Project{ID: "m-1234abcd", Volume: "wiki"}); err != nil {
t.Fatal(err)
}
raw, err := os.ReadFile(filepath.Join(folder, ProjectDir, "config.json"))
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), "post_sync") {
t.Fatalf("unset post_sync should be omitted, got %s", raw)
}
}
func TestNormalizeInclude(t *testing.T) {
for _, tc := range []struct {
in []string
want []string
}{
{in: nil, want: nil},
{in: []string{"wiki/"}, want: []string{"/wiki/"}}, // legacy config, anchored on read
{in: []string{"wiki"}, want: []string{"/wiki"}},
{in: []string{"/wiki/"}, want: []string{"/wiki/"}}, // already anchored
{in: []string{"a/b/"}, want: []string{"a/b/"}}, // compile() anchors these already
{in: []string{"*.md"}, want: []string{"*.md"}}, // deliberate pattern, left alone
{in: []string{"!keep"}, want: []string{"!keep"}},
{in: []string{"wiki/", "*.md"}, want: []string{"/wiki/", "*.md"}},
} {
in := append([]string(nil), tc.in...)
if got := normalizeInclude(tc.in); !reflect.DeepEqual(got, tc.want) {
t.Errorf("normalizeInclude(%q) = %q, want %q", in, got, tc.want)
}
}
}