package syncer import ( "os" "path/filepath" "strings" "testing" "time" "github.com/runbear-io/beardrive/internal/config" "github.com/runbear-io/beardrive/internal/journal" "github.com/runbear-io/beardrive/internal/remote" "github.com/runbear-io/beardrive/internal/store" "github.com/runbear-io/beardrive/internal/templates" ) // End-to-end scenarios for the init knowledge flows documented in // plugin/commands/init.md and the beardrive skill ("Connecting knowledge // tooling"): a shared subfolder carved out of a repo, a teammate connecting // over pre-existing local content, and a nested mount (e.g. a team knowledge // folder inside a personal brain that is itself a mount) syncing through its // own project. // deviceAt is newDevice with an explicit working folder, for topologies where // the folder's location matters (nested mounts). func deviceAt(t *testing.T, name, folder string, backend remote.Backend) *Session { t.Helper() st, err := store.Open(filepath.Join(t.TempDir(), "volume")) if err != nil { t.Fatal(err) } return &Session{ Folder: folder, Store: st, Device: config.Device{ID: name, Name: name, Author: name + "@test"}, Backend: backend, } } func conflictFiles(t *testing.T, folder string) []string { t.Helper() var out []string err := filepath.WalkDir(folder, func(p string, d os.DirEntry, err error) error { if err == nil && !d.IsDir() && strings.Contains(d.Name(), ".bdrive-conflict-") { out = append(out, p) } return nil }) if err != nil { t.Fatal(err) } return out } // A repo shares only its knowledge subfolder (a legacy include-list mount); // a teammate connects from their own checkout with the same scope. Wiki // content flows both ways; each side's code never leaves its machine. func TestSharedSubfolderScopeBothDevices(t *testing.T) { be := sharedRemote(t) a := newDevice(t, "deva", be) b := newDevice(t, "devb", be) write(t, a.Folder, ".bdrive/config.json", `{"include": ["Wiki/"]}`) write(t, a.Folder, "Wiki/home.md", "welcome") write(t, a.Folder, "src/code.go", "package main") cycle(t, a) write(t, b.Folder, ".bdrive/config.json", `{"include": ["Wiki/"]}`) write(t, b.Folder, "main.py", "print('local only')") res := cycle(t, b) if res.LocalOps != 0 { t.Fatalf("b journaled %d ops for out-of-scope files, want 0", res.LocalOps) } if got := read(t, b.Folder, "Wiki/home.md"); got != "welcome" { t.Fatalf("Wiki/home.md = %q, want welcome", got) } // Wiki edits flow back; code never crosses in either direction. write(t, b.Folder, "Wiki/home.md", "welcome v2") cycle(t, b) cycle(t, a) if got := read(t, a.Folder, "Wiki/home.md"); got != "welcome v2" { t.Fatalf("a Wiki/home.md = %q, want welcome v2", got) } for folder, absent := range map[string]string{a.Folder: "main.py", b.Folder: "src/code.go"} { if _, err := os.Stat(filepath.Join(folder, absent)); !os.IsNotExist(err) { t.Fatalf("%s leaked across devices", absent) } } } // The git-handoff teammate story: after the first user connects docs/, a // teammate's checkout already holds the same files. Connecting must converge // with zero conflict copies (identical content is adopted, not duplicated). func TestConnectWithIdenticalLocalContent(t *testing.T) { be := sharedRemote(t) a := newDevice(t, "deva", be) write(t, a.Folder, "docs/guide.md", "v1") write(t, a.Folder, "docs/setup.md", "steps") cycle(t, a) b := newDevice(t, "devb", be) write(t, b.Folder, "docs/guide.md", "v1") write(t, b.Folder, "docs/setup.md", "steps") cycle(t, b) cycle(t, a) cycle(t, b) for _, s := range []*Session{a, b} { if got := read(t, s.Folder, "docs/guide.md"); got != "v1" { t.Fatalf("guide.md = %q, want v1", got) } if c := conflictFiles(t, s.Folder); len(c) != 0 { t.Fatalf("identical content produced conflict copies: %v", c) } } } // Same story with a stale divergent copy. Joining is an adoption, not a merge: // the project's version wins on every device — even though the joiner's copy is // the later write — and no conflict copy is made. The joiner's content is not // lost, it is journaled (and pushed) as a superseded version, which is what // `bdrive restore` reads. func TestConnectWithDivergentLocalContent(t *testing.T) { be := sharedRemote(t) a := newDevice(t, "deva", be) write(t, a.Folder, "docs/guide.md", "hub version") cycle(t, a) b := newDevice(t, "devb", be) time.Sleep(10 * time.Millisecond) write(t, b.Folder, "docs/guide.md", "stale local version") if res := cycle(t, b); res.Adopted != 1 { t.Fatalf("Adopted = %d, want 1", res.Adopted) } cycle(t, a) cycle(t, b) for _, s := range []*Session{a, b} { if got := read(t, s.Folder, "docs/guide.md"); got != "hub version" { t.Fatalf("%s guide.md = %q, want the project's version", s.Device.ID, got) } if c := conflictFiles(t, s.Folder); len(c) != 0 { t.Fatalf("%s: joining made conflict copies: %v", s.Device.ID, c) } } // The superseded content is still in history, on both devices. for _, s := range []*Session{a, b} { ops, err := s.Store.AllOps() if err != nil { t.Fatal(err) } var found bool for _, op := range ops { if op.Path == "docs/guide.md" && s.Store.HasBlob(op.Blob) { if body, err := os.ReadFile(s.Store.BlobPath(op.Blob)); err == nil && string(body) == "stale local version" { found = true } } } if !found { t.Fatalf("%s: the joiner's version is not recoverable from history", s.Device.ID) } } } // The reported connect experience: a folder that already holds a .bdriveignore // (`bdrive init` seeds one) and an agent-written AGENTS.md joins a project that // has its own versions of both. Neither may fork into a // `.bdriveignore.bdrive-conflict--