Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

2605 lines
85 KiB
Go

package remotesync
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
stdsync "sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/agentsview/internal/parser"
syncpkg "go.kenn.io/agentsview/internal/sync"
"go.kenn.io/agentsview/internal/testjsonl"
)
func TestHTTPSyncDownloadsArchiveAndImports(t *testing.T) {
archive := buildHTTPTestTar(t, map[string]string{
"home/wes/.claude/projects/test-project/session.jsonl": testjsonl.NewSessionBuilder().
AddClaudeUser("2024-01-01T00:00:00Z", "http remote").
String(),
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer remote-token", r.Header.Get("Authorization"))
switch r.URL.Path {
case "/api/v1/remote-sync/targets":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"dirs":{"claude":["/home/wes/.claude/projects"]}}`))
case "/api/v1/remote-sync/archive":
w.Header().Set("Content-Type", "application/x-tar")
_, _ = w.Write(archive)
default:
http.NotFound(w, r)
}
}))
t.Cleanup(ts.Close)
database, err := db.Open(filepath.Join(t.TempDir(), "test.db"))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, database.Close()) })
stats, err := HTTPSync{
Host: "devbox",
URL: ts.URL,
Token: "remote-token",
DB: database,
}.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, stats.SessionsSynced)
}
func TestHTTPSyncReportsDownloadAndImportProgress(t *testing.T) {
archive := buildHTTPTestTar(t, map[string]string{
"home/wes/.claude/projects/test-project/session.jsonl": testjsonl.NewSessionBuilder().
AddClaudeUser("2024-01-01T00:00:00Z", "http remote progress").
String(),
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/remote-sync/targets":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"dirs":{"claude":["/home/wes/.claude/projects"]}}`))
case "/api/v1/remote-sync/archive":
w.Header().Set("Content-Type", "application/x-tar")
w.Header().Set("Content-Length", strconv.Itoa(len(archive)))
_, _ = w.Write(archive)
default:
http.NotFound(w, r)
}
}))
t.Cleanup(ts.Close)
database, err := db.Open(filepath.Join(t.TempDir(), "test.db"))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, database.Close()) })
var progress []syncpkg.Progress
stats, err := HTTPSync{
Host: "devbox",
URL: ts.URL,
Token: "remote-token",
DB: database,
Progress: func(p syncpkg.Progress) {
progress = append(progress, p)
},
}.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, stats.SessionsSynced)
assert.Contains(t, progressDetails(progress),
"Resolving agent directories on devbox")
assert.Contains(t, progressDetails(progress),
"Downloading session archive from devbox")
assert.Contains(t, progressDetails(progress),
"Extracting session archive from devbox")
assert.Contains(t, progressDetails(progress),
"Processing sessions from devbox")
require.NotEmpty(t, progress, "expected progress events")
assert.Equal(t, int64(len(archive)), maxBytesDone(progress))
assert.Equal(t, int64(len(archive)), maxBytesTotal(progress))
}
func TestHTTPSyncReportsCompressedTransferThenExtraction(t *testing.T) {
archive := buildHTTPTestTar(t, map[string]string{
"home/user/.claude/projects/test/session.jsonl": "compressed archive",
})
var compressed bytes.Buffer
gz := gzip.NewWriter(&compressed)
_, err := gz.Write(archive)
require.NoError(t, err)
require.NoError(t, gz.Close())
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/remote-sync/archive", r.URL.Path)
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Length", strconv.Itoa(compressed.Len()))
_, _ = w.Write(compressed.Bytes())
}))
t.Cleanup(ts.Close)
var progress []syncpkg.Progress
hs := HTTPSync{
Host: "devbox",
URL: ts.URL,
Progress: func(p syncpkg.Progress) {
progress = append(progress, p)
},
}
mirrorRoot := filepath.Join(t.TempDir(), "remote-mirrors", "devbox")
err = hs.downloadIntoMirror(
context.Background(), ts.Client(), TargetSet{}, []string{"session.jsonl"},
false, mirrorRoot,
)
require.NoError(t, err)
downloadLabel := "Downloading 1 changed files from devbox"
extractLabel := "Extracting 1 changed files from devbox"
assert.Equal(t, []string{downloadLabel, extractLabel},
uniqueProgressDetails(progress))
require.NotEmpty(t, progress)
assert.Equal(t, int64(compressed.Len()), maxBytesDone(progress))
assert.Equal(t, int64(compressed.Len()), maxBytesTotal(progress))
}
func TestPrepareHTTPSyncReportsManifestComparisonBeforeTransfer(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
_, hs := newMirrorSync(t, remote, t.TempDir())
var progress []syncpkg.Progress
hs.Progress = func(p syncpkg.Progress) {
progress = append(progress, p)
}
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, prepared.Close()) })
assert.Equal(t, []string{
"Resolving agent directories on devbox",
"Fetching session manifest from devbox",
"Compared session manifest from devbox: 1 total, 1 changed, 0 deleted",
"Downloading session archive from devbox",
"Extracting session archive from devbox",
}, uniqueProgressDetails(progress))
}
func TestHTTPSyncLegacyCompressedTransferPreservesWireProgress(t *testing.T) {
archive := buildHTTPTestTar(t, map[string]string{
"home/user/.claude/projects/test/session.jsonl": "legacy compressed",
})
var compressed bytes.Buffer
gz := gzip.NewWriter(&compressed)
_, err := gz.Write(archive)
require.NoError(t, err)
require.NoError(t, gz.Close())
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "gzip", r.Header.Get("Accept-Encoding"))
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Length", strconv.Itoa(compressed.Len()))
_, _ = w.Write(compressed.Bytes())
}))
t.Cleanup(ts.Close)
var progress []syncpkg.Progress
hs := HTTPSync{
Host: "devbox",
URL: ts.URL,
Progress: func(p syncpkg.Progress) {
progress = append(progress, p)
},
}
root, err := hs.downloadAndExtract(
context.Background(), ts.Client(), TargetSet{},
)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, os.RemoveAll(root)) })
body, err := os.ReadFile(filepath.Join(
root, "home/user/.claude/projects/test/session.jsonl",
))
require.NoError(t, err)
assert.Equal(t, "legacy compressed", string(body))
assert.Equal(t, []string{
"Downloading session archive from devbox",
"Extracting session archive from devbox",
}, uniqueProgressDetails(progress))
assert.Equal(t, int64(compressed.Len()), maxBytesDone(progress))
assert.Equal(t, int64(compressed.Len()), maxBytesTotal(progress))
}
func TestHTTPSyncRemovesSpooledArchive(t *testing.T) {
tests := []struct {
name string
archive []byte
contentLength int
status int
wantErr bool
}{
{
name: "after successful extraction",
archive: buildHTTPTestTar(t, map[string]string{
"home/user/.claude/projects/test/session.jsonl": "complete",
}),
},
{
name: "after extraction failure",
archive: tarWithoutEndMarker(t,
"home/user/.claude/projects/test/session.jsonl", "truncated"),
wantErr: true,
},
{
name: "after response body read failure",
archive: buildHTTPTestTar(t, map[string]string{
"home/user/.claude/projects/test/session.jsonl": "incomplete response",
}),
contentLength: 1 << 20,
wantErr: true,
},
{
name: "after non-success response body read failure",
archive: []byte("upstream failure"),
contentLength: 1 << 20,
status: http.StatusBadGateway,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if tt.contentLength > 0 {
w.Header().Set("Content-Length", strconv.Itoa(tt.contentLength))
}
if tt.status > 0 {
w.WriteHeader(tt.status)
}
_, _ = w.Write(tt.archive)
}))
t.Cleanup(ts.Close)
parent := filepath.Join(t.TempDir(), "remote-mirrors")
mirrorRoot := filepath.Join(parent, "devbox")
err := (HTTPSync{Host: "devbox", URL: ts.URL}).downloadIntoMirror(
context.Background(), ts.Client(), TargetSet{}, []string{"session.jsonl"},
false, mirrorRoot,
)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if tt.status > 0 {
var statusErr *StatusError
require.ErrorAs(t, err, &statusErr)
assert.Equal(t, tt.status, statusErr.Code)
}
entries, readErr := os.ReadDir(parent)
if errors.Is(readErr, os.ErrNotExist) {
return
}
require.NoError(t, readErr)
for _, entry := range entries {
assert.NotContains(t, entry.Name(), "agentsview-http-archive-",
"owned spool artifacts must be removed")
}
})
}
}
func TestHTTPSyncMirrorRetainsPostExtractionSpoolCleanup(t *testing.T) {
archive := buildHTTPTestTar(t, map[string]string{
"home/user/session.jsonl": "complete",
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write(archive)
}))
t.Cleanup(ts.Close)
removeErr := errors.New("spool removal failed")
removeCalls := 0
hs := HTTPSync{Host: "devbox", URL: ts.URL, removeArchiveSpool: func(path string) error {
removeCalls++
if removeCalls == 1 {
return removeErr
}
return os.RemoveAll(path)
}}
mirrorRoot := filepath.Join(t.TempDir(), "mirrors", "devbox")
err := hs.downloadIntoMirror(
context.Background(), ts.Client(), TargetSet{}, []string{"session.jsonl"},
false, mirrorRoot,
)
require.ErrorIs(t, err, removeErr)
var owner *downloadedArchiveCleanupError
require.ErrorAs(t, err, &owner,
"post-extraction cleanup must remain retryable")
assert.FileExists(t, filepath.Join(mirrorRoot, "home/user/session.jsonl"))
require.NoError(t, owner.RetryCleanup())
assert.Equal(t, 2, removeCalls)
}
func TestHTTPSyncLegacyRetainsCleanupWithoutLeakingExtractedRoot(t *testing.T) {
archive := buildHTTPTestTar(t, map[string]string{
"home/user/session.jsonl": "complete",
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write(archive)
}))
t.Cleanup(ts.Close)
tempParent := t.TempDir()
setPortableTempDir(t, tempParent)
removeErr := errors.New("spool removal failed")
removeCalls := 0
hs := HTTPSync{Host: "devbox", URL: ts.URL, removeArchiveSpool: func(path string) error {
removeCalls++
if removeCalls == 1 {
return removeErr
}
return os.RemoveAll(path)
}}
root, err := hs.downloadAndExtract(
context.Background(), ts.Client(), TargetSet{},
)
assert.Empty(t, root, "failed cleanup must not transfer root ownership")
require.ErrorIs(t, err, removeErr)
var owner *downloadedArchiveCleanupError
require.ErrorAs(t, err, &owner,
"post-extraction cleanup must remain retryable")
require.NoError(t, owner.RetryCleanup())
entries, readErr := os.ReadDir(tempParent)
require.NoError(t, readErr)
assert.Empty(t, entries,
"retry must release both the spool and extracted legacy root")
}
func TestHTTPSyncLegacyRetainsSpoolWhenExtractionRootCreationFails(t *testing.T) {
archive := buildHTTPTestTar(t, map[string]string{
"home/user/session.jsonl": "complete",
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(len(archive)))
_, _ = w.Write(archive)
}))
t.Cleanup(ts.Close)
validTemp := t.TempDir()
setPortableTempDir(t, validTemp)
invalidTemp := filepath.Join(t.TempDir(), "not-a-directory")
require.NoError(t, os.WriteFile(invalidTemp, []byte("block"), 0o600))
removeErr := errors.New("spool cleanup failed")
removeCalls := 0
hs := HTTPSync{
Host: "devbox",
URL: ts.URL,
Progress: func(p syncpkg.Progress) {
if p.BytesDone == int64(len(archive)) {
for _, name := range []string{"TMPDIR", "TMP", "TEMP"} {
require.NoError(t, os.Setenv(name, invalidTemp))
}
}
},
removeArchiveSpool: func(string) error {
removeCalls++
if removeCalls == 1 {
return removeErr
}
return nil
},
}
root, err := hs.downloadAndExtract(
context.Background(), ts.Client(), TargetSet{},
)
assert.Empty(t, root)
require.ErrorContains(t, err, "create temp dir")
require.ErrorIs(t, err, removeErr)
var owner *downloadedArchiveCleanupError
require.ErrorAs(t, err, &owner,
"failed cleanup must retain the downloaded spool")
require.NoError(t, owner.RetryCleanup())
assert.Equal(t, 2, removeCalls)
}
func TestHTTPSyncRemovesSpooledArchiveOnCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
parent := filepath.Join(t.TempDir(), "remote-mirrors")
resp := &http.Response{
StatusCode: http.StatusOK,
Body: &cancelEOFBody{ctx: ctx},
Header: make(http.Header),
}
hs := HTTPSync{Progress: func(p syncpkg.Progress) {
if p.BytesDone > 0 {
cancel()
}
}}
archive, err := hs.downloadArchive(ctx, resp, "Downloading", parent)
if archive != nil {
t.Cleanup(func() { require.NoError(t, archive.Close()) })
}
require.ErrorIs(t, err, context.Canceled)
entries, readErr := os.ReadDir(parent)
require.NoError(t, readErr)
assert.Empty(t, entries)
}
func TestDownloadArchiveCloseFailureRemovesIsolatedSpool(t *testing.T) {
closeErr := errors.New("close response body")
parent := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(parent, "existing"), []byte("keep"), 0o644,
))
resp := &http.Response{
StatusCode: http.StatusOK,
Body: &closeErrorBody{
Reader: bytes.NewReader([]byte("wire archive")),
err: closeErr,
},
Header: make(http.Header),
}
archive, err := (HTTPSync{}).downloadArchive(
context.Background(), resp, "Downloading", parent,
)
assert.Nil(t, archive)
require.ErrorIs(t, err, closeErr)
entries, readErr := os.ReadDir(parent)
require.NoError(t, readErr)
require.Len(t, entries, 1)
assert.Equal(t, "existing", entries[0].Name())
}
func TestDownloadedArchiveUsesIsolatedSpoolOutsideMirror(t *testing.T) {
parent := t.TempDir()
mirrorRoot := filepath.Join(parent, "devbox")
require.NoError(t, os.Mkdir(mirrorRoot, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(parent, "existing"), []byte("keep"), 0o644,
))
resp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte("wire archive"))),
Header: make(http.Header),
}
archive, err := (HTTPSync{}).downloadArchive(
context.Background(), resp, "Downloading", parent,
)
require.NoError(t, err)
require.NotNil(t, archive)
t.Cleanup(func() { require.NoError(t, archive.Close()) })
tempDir := filepath.Dir(archive.path)
assert.DirExists(t, tempDir)
assert.FileExists(t, archive.path)
assert.Equal(t, parent, filepath.Dir(tempDir))
assert.NotEqual(t, parent, tempDir)
assert.False(t, within(mirrorRoot, archive.path),
"spool must not be created inside the manifest mirror")
require.NoError(t, archive.Close())
entries, readErr := os.ReadDir(parent)
require.NoError(t, readErr)
require.Len(t, entries, 2)
assert.Equal(t, []string{"devbox", "existing"},
[]string{entries[0].Name(), entries[1].Name()})
}
func TestDownloadedArchiveExtractionCancellationMidEntry(t *testing.T) {
body := make([]byte, 32<<10)
state := uint32(1)
for i := range body {
state ^= state << 13
state ^= state >> 17
state ^= state << 5
body[i] = byte(state)
}
archiveBytes := buildHTTPTestTar(t, map[string]string{
"home/user/large-session.jsonl": string(body),
})
var compressed bytes.Buffer
gz := gzip.NewWriter(&compressed)
_, err := gz.Write(archiveBytes)
require.NoError(t, err)
require.NoError(t, gz.Close())
require.Greater(t, compressed.Len(), 1024)
tests := []struct {
name string
wire []byte
compressed bool
}{
{name: "plain", wire: archiveBytes},
{name: "gzip", wire: compressed.Bytes(), compressed: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
spoolParent := t.TempDir()
spoolDir := filepath.Join(spoolParent, "agentsview-http-archive-test")
require.NoError(t, os.Mkdir(spoolDir, 0o700))
require.NoError(t, os.WriteFile(
filepath.Join(spoolDir, "owned"), []byte("spool"), 0o600,
))
reached := make(chan struct{})
release := make(chan struct{})
reader := &gatedArchiveReader{
r: bytes.NewReader(tt.wire),
gateAfter: 768,
reached: reached,
release: release,
}
archive := &downloadedArchive{
dir: spoolDir,
path: filepath.Join(spoolDir, "archive"),
compressed: tt.compressed,
open: func() (io.ReadCloser, error) { return reader, nil },
}
t.Cleanup(func() { require.NoError(t, archive.Close()) })
dst := filepath.Join(t.TempDir(), "extracted")
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
released := false
t.Cleanup(func() {
if !released {
close(release)
}
})
errCh := make(chan error, 1)
go func() {
errCh <- archive.extract(ctx, dst, nil, "Extracting")
}()
select {
case <-reached:
case <-time.After(time.Second):
require.FailNow(t, "timed out waiting for extraction to enter file body")
}
cancel()
close(release)
released = true
select {
case err := <-errCh:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
require.FailNow(t, "timed out waiting for canceled extraction")
}
assert.NoFileExists(t,
filepath.Join(dst, "home/user/large-session.jsonl"))
require.NoError(t, archive.Close())
assert.NoDirExists(t, spoolDir)
})
}
}
func TestPrepareHTTPSyncCancellationReleasesMirrorOwnership(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.writeSession(t, "large.jsonl",
time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC),
strings.Repeat("entry\n", 4096))
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
ctx, cancel := context.WithCancel(context.Background())
extractionStarted := false
hs.Progress = func(p syncpkg.Progress) {
if strings.HasPrefix(p.Detail, "Extracting ") {
extractionStarted = true
cancel()
}
}
prepared, err := hs.Prepare(ctx)
assert.Nil(t, prepared)
require.ErrorIs(t, err, context.Canceled)
assert.True(t, extractionStarted)
mirrorRoot := MirrorDir(dataDir, "devbox")
assertMirrorUnlocked(t, mirrorRoot)
entries, readErr := os.ReadDir(filepath.Dir(mirrorRoot))
require.NoError(t, readErr)
for _, entry := range entries {
assert.NotContains(t, entry.Name(), "agentsview-http-archive-",
"canceled preparation must release its spool ownership")
}
}
func TestHTTPSyncLegacyCancellationCleansOwnedRoots(t *testing.T) {
archive := buildHTTPTestTar(t, map[string]string{
"home/user/large-session.jsonl": strings.Repeat("entry\n", 4096),
})
var compressed bytes.Buffer
gz := gzip.NewWriter(&compressed)
_, err := gz.Write(archive)
require.NoError(t, err)
require.NoError(t, gz.Close())
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Length", strconv.Itoa(compressed.Len()))
_, _ = w.Write(compressed.Bytes())
}))
t.Cleanup(ts.Close)
tempParent := t.TempDir()
setPortableTempDir(t, tempParent)
ctx, cancel := context.WithCancel(context.Background())
hs := HTTPSync{
Host: "devbox",
URL: ts.URL,
Progress: func(p syncpkg.Progress) {
if strings.HasPrefix(p.Detail, "Extracting ") {
cancel()
}
},
}
root, err := hs.downloadAndExtract(ctx, ts.Client(), TargetSet{})
assert.Empty(t, root)
require.ErrorIs(t, err, context.Canceled)
entries, readErr := os.ReadDir(tempParent)
require.NoError(t, readErr)
assert.Empty(t, entries,
"canceled legacy extraction must remove its spool and extraction root")
}
type cancelEOFBody struct {
ctx context.Context
sent bool
}
func (b *cancelEOFBody) Read(p []byte) (int, error) {
if !b.sent {
b.sent = true
return copy(p, "partial response"), nil
}
<-b.ctx.Done()
return 0, io.EOF
}
func (*cancelEOFBody) Close() error { return nil }
type closeErrorBody struct {
io.Reader
err error
}
func (b *closeErrorBody) Close() error { return b.err }
type gatedArchiveReader struct {
r *bytes.Reader
gateAfter int
read int
reached chan struct{}
release chan struct{}
gated bool
}
func (r *gatedArchiveReader) Read(p []byte) (int, error) {
if r.read >= r.gateAfter && !r.gated {
r.gated = true
close(r.reached)
<-r.release
}
if len(p) > 256 {
p = p[:256]
}
n, err := r.r.Read(p)
r.read += n
return n, err
}
func (*gatedArchiveReader) Close() error { return nil }
func progressDetails(progress []syncpkg.Progress) []string {
out := make([]string, 0, len(progress))
for _, p := range progress {
if p.Detail != "" {
out = append(out, p.Detail)
}
}
return out
}
func uniqueProgressDetails(progress []syncpkg.Progress) []string {
var out []string
for _, detail := range progressDetails(progress) {
if len(out) == 0 || out[len(out)-1] != detail {
out = append(out, detail)
}
}
return out
}
func maxBytesDone(progress []syncpkg.Progress) int64 {
var max int64
for _, p := range progress {
if p.BytesDone > max {
max = p.BytesDone
}
}
return max
}
func maxBytesTotal(progress []syncpkg.Progress) int64 {
var max int64
for _, p := range progress {
if p.BytesTotal > max {
max = p.BytesTotal
}
}
return max
}
func buildHTTPTestTar(t *testing.T, files map[string]string) []byte {
t.Helper()
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
mtime := time.Date(2026, 6, 28, 12, 0, 0, 0, time.UTC)
for name, body := range files {
hdr := &tar.Header{
Name: name,
Mode: 0o644,
Size: int64(len(body)),
ModTime: mtime,
}
require.NoError(t, tw.WriteHeader(hdr))
_, err := tw.Write([]byte(body))
require.NoError(t, err)
}
require.NoError(t, tw.Close())
return buf.Bytes()
}
// mirrorTestRemote is a fake remote daemon backed by a real directory
// tree, serving targets/manifest/archive with the same package
// functions the real server uses.
type mirrorTestRemote struct {
dir string // remote-side agent dir (absolute)
targets TargetSet
archiveRequests []ArchiveRequest
archiveBody []byte
onArchive func(ArchiveRequest)
manifestStatus int // 0 = serve manifest; else respond with this status
manifestHTML bool // true = mimic an old daemon's SPA catch-all
onManifest func() // called before serving a manifest response
onManifestRequest func(*http.Request)
rejectDelta bool
ts *httptest.Server
}
func newMirrorTestRemote(t *testing.T) *mirrorTestRemote {
t.Helper()
remote := &mirrorTestRemote{
dir: filepath.Join(t.TempDir(), "claude-projects"),
}
require.NoError(t, os.MkdirAll(remote.dir, 0o755))
remote.targets = TargetSet{
Dirs: map[parser.AgentType][]string{parser.AgentClaude: {remote.dir}},
}
remote.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/remote-sync/targets":
w.Header().Set("Content-Type", "application/json")
require.NoError(t, json.NewEncoder(w).Encode(remote.targets))
case "/api/v1/remote-sync/manifest":
if remote.onManifest != nil {
remote.onManifest()
}
if remote.onManifestRequest != nil {
remote.onManifestRequest(r)
}
if remote.manifestStatus != 0 {
http.Error(w, "no manifest here", remote.manifestStatus)
return
}
if remote.manifestHTML {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(
"<!doctype html><html><body>spa</body></html>"))
return
}
// Mirror the real handler: build the manifest from the
// requested targets and refuse file-scoped agents.
var req TargetSet
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
if req.HasFileScopedAgents() {
http.Error(w, "manifest unavailable for file-scoped agents",
http.StatusNotImplemented)
return
}
manifest, err := BuildManifest(req)
require.NoError(t, err)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
require.NoError(t, json.NewEncoder(gz).Encode(manifest))
require.NoError(t, gz.Close())
case "/api/v1/remote-sync/archive":
var req ArchiveRequest
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
remote.archiveRequests = append(remote.archiveRequests, req)
if remote.onArchive != nil {
remote.onArchive(req)
}
if remote.rejectDelta && req.DeltaFiles != nil {
http.Error(w, "delta not allowed", http.StatusForbidden)
return
}
// Serve the requested target subset, as the real handler
// does after SelectAllowedTargets.
w.Header().Set("Content-Type", "application/x-tar")
if remote.archiveBody != nil {
_, _ = w.Write(remote.archiveBody)
return
}
if req.DeltaFiles != nil {
require.NoError(t, WriteArchiveFiles(
w, remote.targets.DeltaAllowedRoots(), req.DeltaFiles))
} else {
require.NoError(t, WriteArchive(w, req.TargetSet))
}
default:
http.NotFound(w, r)
}
}))
t.Cleanup(remote.ts.Close)
return remote
}
// addFileScopedAgent registers a second agent whose targets are file
// scoped, mimicking Windsurf's curated export, and returns the remote
// file path. The file's content never parses as a session; partition
// tests assert transfer behavior, and Windsurf import correctness is
// covered by the server and sync tests.
func (r *mirrorTestRemote) addFileScopedAgent(t *testing.T) string {
t.Helper()
dir := filepath.Join(filepath.Dir(r.dir), "scoped-agent")
require.NoError(t, os.MkdirAll(dir, 0o755))
path := filepath.Join(dir, "export.txt")
require.NoError(t, os.WriteFile(path, []byte("file-scoped export\n"), 0o644))
r.targets.Dirs[parser.AgentGemini] = []string{dir}
r.targets.Files = map[parser.AgentType][]string{
parser.AgentGemini: {path},
}
return path
}
// writeSession writes a session file with one user message per text.
// Message timestamps are deterministic, so writing the same file again
// with the previous texts plus new ones is a byte-identical prefix
// append — the realistic mutation for JSONL session files, and one the
// engine's incremental parse handles. (In-place rewrites that grow are
// misread as appends for remote paths — pre-existing engine gap, out
// of scope here.)
func (r *mirrorTestRemote) writeSession(
t *testing.T, name string, mtime time.Time, userTexts ...string,
) string {
t.Helper()
dir := filepath.Join(r.dir, "test-project")
require.NoError(t, os.MkdirAll(dir, 0o755))
builder := testjsonl.NewSessionBuilder()
for i, text := range userTexts {
ts := time.Date(2024, 1, 1, 0, i, 0, 0, time.UTC).Format(time.RFC3339)
builder = builder.AddClaudeUser(ts, text)
}
path := filepath.Join(dir, name)
require.NoError(t, os.WriteFile(path, []byte(builder.String()), 0o644))
require.NoError(t, os.Chtimes(path, mtime, mtime))
return path
}
func newMirrorSync(t *testing.T, remote *mirrorTestRemote, dataDir string) (*db.DB, HTTPSync) {
t.Helper()
database, err := db.Open(filepath.Join(t.TempDir(), "test.db"))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, database.Close()) })
return database, HTTPSync{
Host: "devbox",
URL: remote.ts.URL,
Token: "remote-token",
DataDir: dataDir,
DB: database,
}
}
func TestHTTPSyncMirrorSecondSyncTransfersOnlyDelta(t *testing.T) {
remote := newMirrorTestRemote(t)
base := time.Date(2026, 7, 8, 10, 0, 0, 123456789, time.UTC)
remote.writeSession(t, "a.jsonl", base, "session a")
staleRemote := remote.writeSession(t, "b.jsonl", base, "session b")
remote.writeSession(t, "c.jsonl", base, "session c")
remote.writeSession(t, "d.jsonl", base, "session d")
remote.writeSession(t, "e.jsonl", base, "session e")
dataDir := t.TempDir()
database, hs := newMirrorSync(t, remote, dataDir)
stats, err := hs.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 5, stats.SessionsSynced)
require.Len(t, remote.archiveRequests, 1)
assert.Empty(t, remote.archiveRequests[0].DeltaFiles, "bootstrap uses the full archive")
// Append to one, add one, delete one on the remote. The fetch set
// (2 of the 5 files now in the manifest) stays under the bootstrap
// heuristic's half-corpus threshold, so this sync must go delta.
changed := remote.writeSession(t, "a.jsonl", base.Add(5*time.Second),
"session a", "session a continued")
added := remote.writeSession(t, "f.jsonl", base.Add(6*time.Second), "session f")
require.NoError(t, os.Remove(staleRemote))
stats, err = hs.Run(context.Background())
require.NoError(t, err)
require.Len(t, remote.archiveRequests, 2)
assert.ElementsMatch(t, []string{changed, added}, remote.archiveRequests[1].DeltaFiles)
assert.Equal(t, 2, stats.SessionsSynced)
// The deleted remote file is gone from the mirror, but its
// session survives in the DB (archive semantics).
mirrorRoot := MirrorDir(dataDir, "devbox")
staleLocal, err := safeRemappedRemotePath(mirrorRoot, staleRemote)
require.NoError(t, err)
assert.NoFileExists(t, staleLocal)
page, err := database.ListSessions(context.Background(), db.SessionFilter{Limit: 10})
require.NoError(t, err)
assert.Len(t, page.Sessions, 6)
// Third sync with no remote changes: no archive request at all.
stats, err = hs.Run(context.Background())
require.NoError(t, err)
assert.Len(t, remote.archiveRequests, 2)
assert.Equal(t, 0, stats.SessionsSynced)
}
func TestHTTPSyncFallsBackToLegacyWhenManifestMissing(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusNotFound
remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "legacy fallback")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
stats, err := hs.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, stats.SessionsSynced)
require.Len(t, remote.archiveRequests, 1)
assert.Empty(t, remote.archiveRequests[0].DeltaFiles)
assert.NoDirExists(t, MirrorDir(dataDir, "devbox"))
}
func TestHTTPSyncFallsBackToLegacyWhenManifestServesSPAHTML(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
remote.manifestHTML = true
_, hs := newMirrorSync(t, remote, t.TempDir())
stats, err := hs.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, stats.SessionsSynced)
require.Len(t, remote.archiveRequests, 1)
assert.Empty(t, remote.archiveRequests[0].DeltaFiles)
}
func TestHTTPSyncFallsBackToFullWhenDeltaRejected(t *testing.T) {
remote := newMirrorTestRemote(t)
base := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
remote.writeSession(t, "a.jsonl", base, "session a")
remote.writeSession(t, "b.jsonl", base, "session b")
remote.writeSession(t, "c.jsonl", base, "session c")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
_, err := hs.Run(context.Background())
require.NoError(t, err)
remote.rejectDelta = true
remote.writeSession(t, "a.jsonl", base.Add(5*time.Second),
"session a", "session a continued")
stats, err := hs.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, stats.SessionsSynced)
// Requests: bootstrap full, rejected delta, retried full.
require.Len(t, remote.archiveRequests, 3)
assert.NotEmpty(t, remote.archiveRequests[1].DeltaFiles)
assert.Empty(t, remote.archiveRequests[2].DeltaFiles)
}
func TestHTTPSyncIncrementalMatchesFreshFullSync(t *testing.T) {
remote := newMirrorTestRemote(t)
base := time.Date(2026, 7, 8, 10, 0, 0, 555666777, time.UTC)
// Enough unchanged files that the second sync's two changed files
// stay under the half-corpus bootstrap heuristic and exercise the
// delta path rather than a full re-download.
appended := remote.writeSession(t, "a.jsonl", base, "session a")
remote.writeSession(t, "b.jsonl", base, "session b")
remote.writeSession(t, "d.jsonl", base, "session d")
remote.writeSession(t, "e.jsonl", base, "session e")
incDB, incSync := newMirrorSync(t, remote, t.TempDir())
_, err := incSync.Run(context.Background())
require.NoError(t, err)
remote.writeSession(t, "a.jsonl", base.Add(2*time.Second),
"session a", "session a continued")
added := remote.writeSession(t, "c.jsonl", base.Add(3*time.Second), "session c")
_, err = incSync.Run(context.Background())
require.NoError(t, err)
// Requests: bootstrap full, then a delta for exactly the changes.
require.Len(t, remote.archiveRequests, 2)
assert.Empty(t, remote.archiveRequests[0].DeltaFiles)
assert.ElementsMatch(t, []string{appended, added},
remote.archiveRequests[1].DeltaFiles)
freshDB, freshSync := newMirrorSync(t, remote, t.TempDir())
_, err = freshSync.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, sessionSummaries(t, freshDB), sessionSummaries(t, incDB))
}
func TestHTTPSyncMirrorRecoversFromDirAtFilePath(t *testing.T) {
remote := newMirrorTestRemote(t)
base := time.Date(2026, 7, 8, 10, 0, 0, 123456789, time.UTC)
wedged := remote.writeSession(t, "a.jsonl", base, "session a")
remote.writeSession(t, "b.jsonl", base, "session b")
remote.writeSession(t, "c.jsonl", base, "session c")
remote.writeSession(t, "d.jsonl", base, "session d")
remote.writeSession(t, "e.jsonl", base, "session e")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
_, err := hs.Run(context.Background())
require.NoError(t, err)
// Simulate a crashed extraction: a directory occupies a.jsonl's
// mirror path (MkdirAll ran, the file write never happened).
local, err := safeRemappedRemotePath(MirrorDir(dataDir, "devbox"), wedged)
require.NoError(t, err)
require.NoError(t, os.Remove(local))
require.NoError(t, os.Mkdir(local, 0o755))
_, err = hs.Run(context.Background())
require.NoError(t, err)
info, err := os.Stat(local)
require.NoError(t, err)
assert.True(t, info.Mode().IsRegular())
// Recovery re-fetched only the wedged file, via the delta path.
require.Len(t, remote.archiveRequests, 2)
assert.Equal(t, []string{wedged}, remote.archiveRequests[1].DeltaFiles)
}
// sessionSummaries returns a sorted, comparable projection of every
// session: identity plus message count, which changes when a session
// file's new content is parsed.
func sessionSummaries(t *testing.T, database *db.DB) []string {
t.Helper()
page, err := database.ListSessions(context.Background(), db.SessionFilter{Limit: 100})
require.NoError(t, err)
out := make([]string, 0, len(page.Sessions))
for _, s := range page.Sessions {
count, ok := database.GetSessionMessageCount(s.ID)
require.True(t, ok, "message count for %s", s.ID)
hash, ok := database.GetSessionFileHash(s.ID)
require.True(t, ok, "file hash for %s", s.ID)
out = append(out, fmt.Sprintf("%s|%s|%d|%s", s.ID, s.Machine, count, hash))
}
sort.Strings(out)
return out
}
// A host with a file-scoped agent (Windsurf) must not lose incremental
// sync for its dir-scoped corpus: the manifest request carries only the
// dir-scoped targets, and the file-scoped exports arrive as a separate
// small full archive on every sync.
func TestHTTPSyncMirrorPartitionsFileScopedAgents(t *testing.T) {
remote := newMirrorTestRemote(t)
base := time.Date(2026, 7, 8, 10, 0, 0, 123456789, time.UTC)
remote.writeSession(t, "a.jsonl", base, "session a")
remote.writeSession(t, "b.jsonl", base, "session b")
remote.writeSession(t, "c.jsonl", base, "session c")
remote.writeSession(t, "d.jsonl", base, "session d")
remote.writeSession(t, "e.jsonl", base, "session e")
scoped := remote.addFileScopedAgent(t)
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
stats, err := hs.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 5, stats.SessionsSynced)
// Bootstrap: one full archive for the dir-scoped corpus, one for
// the file-scoped agent — never a combined whole-host archive.
require.Len(t, remote.archiveRequests, 2)
assert.Nil(t, remote.archiveRequests[0].DeltaFiles)
assert.Contains(t, remote.archiveRequests[0].Dirs, parser.AgentClaude)
assert.NotContains(t, remote.archiveRequests[0].Dirs, parser.AgentGemini)
assert.Contains(t, remote.archiveRequests[1].Files, parser.AgentGemini)
assert.NotContains(t, remote.archiveRequests[1].Dirs, parser.AgentClaude)
mirrorRoot := MirrorDir(dataDir, "devbox")
scopedLocal, err := safeRemappedRemotePath(mirrorRoot, scoped)
require.NoError(t, err)
assert.FileExists(t, scopedLocal)
// Second sync: the changed session travels as a delta, and the
// file-scoped export — cleared by the mirror deletion pass because
// it is never in the manifest — is re-fetched in full and lands
// back in the mirror.
changed := remote.writeSession(t, "a.jsonl", base.Add(5*time.Second),
"session a", "session a continued")
stats, err = hs.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, stats.SessionsSynced)
require.Len(t, remote.archiveRequests, 4)
assert.Equal(t, []string{changed}, remote.archiveRequests[2].DeltaFiles)
assert.Contains(t, remote.archiveRequests[3].Files, parser.AgentGemini)
assert.FileExists(t, scopedLocal)
// The file-scoped export disappears from the remote: the deletion
// pass clears its mirror copy and nothing re-populates it, matching
// the legacy path where only the current export was ever extracted.
require.NoError(t, os.Remove(scoped))
delete(remote.targets.Dirs, parser.AgentGemini)
remote.targets.Files = nil
_, err = hs.Run(context.Background())
require.NoError(t, err)
assert.NoFileExists(t, scopedLocal)
assert.Len(t, remote.archiveRequests, 4,
"no archive requests when nothing changed and no file-scoped targets remain")
}
// The mirror lock must already be held when the manifest is fetched:
// otherwise two concurrent syncs can fetch manifests in one order and
// apply them in another, and the stale manifest's deletion pass
// removes files the newer sync just mirrored.
func TestHTTPSyncHoldsMirrorLockDuringManifestFetch(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
mirrorRoot := MirrorDir(dataDir, "devbox")
remote.onManifest = func() {
// assert, not require: this runs on the server goroutine, where
// FailNow must not be called.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
lock, err := AcquireMirrorLock(ctx, mirrorRoot)
if lock != nil {
// On regression the acquire succeeds; release immediately so
// the sync under test fails on the assertion instead of
// deadlocking against a leaked lock.
_ = lock.Close()
}
assert.Error(t, err, "mirror lock must be held during the manifest fetch")
}
_, err := hs.Run(context.Background())
require.NoError(t, err)
}
func TestPrepareHTTPSyncContributorDeltaCardinality(t *testing.T) {
for _, fileCount := range []int{5, 500} {
t.Run(strconv.Itoa(fileCount), func(t *testing.T) {
remote := newMirrorTestRemote(t)
base := time.Date(2026, 7, 8, 10, 0, 0, 123456789, time.UTC)
var changed string
for i := range fileCount {
path := remote.writeSession(t, fmt.Sprintf("%03d.jsonl", i),
base, fmt.Sprintf("session %d", i))
if i == 0 {
changed = path
}
}
dataDir := t.TempDir()
database, hs := newMirrorSync(t, remote, dataDir)
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
require.NoError(t, prepared.Close())
require.Len(t, remote.archiveRequests, 1)
assert.Nil(t, remote.archiveRequests[0].DeltaFiles)
remote.writeSession(t, "000.jsonl", base.Add(time.Second),
"session 0", "session 0 continued")
hs.Full = true
prepared, err = hs.Prepare(context.Background())
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, prepared.Close()) })
contributor, err := prepared.RebuildContributor()
require.NoError(t, err)
engine := syncpkg.NewEngine(database, syncpkg.EngineConfig{})
stats, err := engine.ResyncAllWithOptions(
context.Background(), nil,
syncpkg.RebuildOptions{Contributors: []syncpkg.RebuildContributor{
contributor,
}},
)
engine.Close()
require.NoError(t, err)
assert.False(t, stats.Aborted)
require.NoError(t, prepared.Close())
require.Len(t, remote.archiveRequests, 2)
assert.Equal(t, []string{changed}, remote.archiveRequests[1].DeltaFiles)
})
}
}
func TestPreparedHTTPSyncContributorKeepsLockUntilClose(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.writeSession(t, "session.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "locked contributor")
database, hs := newMirrorSync(t, remote, t.TempDir())
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, prepared.Close()) })
contributor, err := prepared.RebuildContributor()
require.NoError(t, err)
contributorStarted := make(chan struct{})
continueContributor := make(chan struct{})
originalProgress := contributor.Progress
contributor.Progress = func(progress syncpkg.Progress) syncpkg.Progress {
if progress.Phase == syncpkg.PhaseDiscovering {
select {
case <-contributorStarted:
default:
close(contributorStarted)
<-continueContributor
}
}
return originalProgress(progress)
}
engine := syncpkg.NewEngine(database, syncpkg.EngineConfig{})
t.Cleanup(engine.Close)
type rebuildResult struct {
stats syncpkg.SyncStats
err error
}
rebuilt := make(chan rebuildResult, 1)
go func() {
stats, runErr := engine.ResyncAllWithOptions(
context.Background(), nil,
syncpkg.RebuildOptions{Contributors: []syncpkg.RebuildContributor{
contributor,
}},
)
rebuilt <- rebuildResult{stats: stats, err: runErr}
}()
select {
case <-contributorStarted:
case <-time.After(time.Second):
require.FailNow(t, "timed out waiting for contributor")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
competing, lockErr := AcquireMirrorLock(ctx, prepared.Root())
cancel()
if competing != nil {
require.NoError(t, competing.Close())
}
assert.Error(t, lockErr, "prepared source must retain its lock during contribution")
close(continueContributor)
select {
case result := <-rebuilt:
require.NoError(t, result.err)
assert.False(t, result.stats.Aborted)
case <-time.After(time.Second):
require.FailNow(t, "timed out waiting for rebuild")
}
require.NoError(t, prepared.Close())
assertMirrorUnlocked(t, MirrorDir(hs.DataDir, hs.Host))
}
func TestPrepareHTTPSyncUnchangedFullMakesNoArchiveRequest(t *testing.T) {
for _, fileCount := range []int{5, 500} {
t.Run(strconv.Itoa(fileCount), func(t *testing.T) {
remote := newMirrorTestRemote(t)
mtime := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
for i := range fileCount {
remote.writeSession(t, fmt.Sprintf("%03d.jsonl", i), mtime,
fmt.Sprintf("session %d", i))
}
_, hs := newMirrorSync(t, remote, t.TempDir())
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
require.NoError(t, prepared.Close())
require.Len(t, remote.archiveRequests, 1)
hs.Full = true
prepared, err = hs.Prepare(context.Background())
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, prepared.Close()) })
assert.Len(t, remote.archiveRequests, 1,
"unchanged full preparation must not transfer the %d-file mirror",
fileCount)
})
}
}
func TestPrepareHTTPSyncManualMirrorRemovalBootstrapsFull(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
require.NoError(t, prepared.Close())
require.Len(t, remote.archiveRequests, 1)
require.NoError(t, os.RemoveAll(MirrorDir(dataDir, "devbox")))
prepared, err = hs.Prepare(context.Background())
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, prepared.Close()) })
require.Len(t, remote.archiveRequests, 2)
assert.Nil(t, remote.archiveRequests[1].DeltaFiles)
}
func TestPreparedHTTPSyncCloseReleasesLockAndRemovesLegacyRoot(t *testing.T) {
t.Run("manifest mirror persists", func(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
root := prepared.Root()
assert.Equal(t, MirrorDir(dataDir, "devbox"), root)
assert.DirExists(t, root)
assert.Equal(t, remote.targets, prepared.Targets())
assertMirrorLocked(t, root)
require.NoError(t, prepared.Close())
require.NoError(t, prepared.Close(), "persistent Close must be idempotent")
assert.DirExists(t, root, "persistent mirror must survive Close")
assertMirrorUnlocked(t, root)
})
t.Run("legacy root is owned", func(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusNotFound
remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
root := prepared.Root()
assert.DirExists(t, root)
assert.NotEqual(t, MirrorDir(dataDir, "devbox"), root)
assert.NoDirExists(t, MirrorDir(dataDir, "devbox"))
assertMirrorLocked(t, MirrorDir(dataDir, "devbox"))
require.NoError(t, prepared.Close())
require.NoError(t, prepared.Close(), "legacy Close must be idempotent")
assert.NoDirExists(t, root, "owned legacy root must be removed")
assertMirrorUnlocked(t, MirrorDir(dataDir, "devbox"))
})
}
func TestPreparedHTTPSyncCloseRetriesFailedCleanup(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusNotFound
remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
root := prepared.Root()
removeErr := errors.New("transient remove failure")
unlockErr := errors.New("transient unlock failure")
removeCalls := 0
prepared.removeRoot = func(path string) error {
removeCalls++
if removeCalls == 1 {
return removeErr
}
return os.RemoveAll(path)
}
unlockCalls := 0
prepared.releaseLock = func(lock *MirrorLockHandle) error {
unlockCalls++
if unlockCalls == 1 {
return unlockErr
}
return lock.Close()
}
err = prepared.Close()
assert.ErrorIs(t, err, removeErr)
assert.ErrorIs(t, err, unlockErr)
assert.DirExists(t, root)
assertMirrorLocked(t, MirrorDir(dataDir, "devbox"))
require.NoError(t, prepared.Close())
assert.NoDirExists(t, root)
assertMirrorUnlocked(t, MirrorDir(dataDir, "devbox"))
require.NoError(t, prepared.Close(), "fully cleaned Close must be idempotent")
}
func TestPreparedHTTPSyncCloseTracksCleanupIndependently(t *testing.T) {
prepareLegacy := func(t *testing.T) (*PreparedHTTP, string) {
t.Helper()
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusNotFound
remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
return prepared, MirrorDir(dataDir, "devbox")
}
t.Run("successful unlock is not repeated", func(t *testing.T) {
prepared, mirrorRoot := prepareLegacy(t)
removeErr := errors.New("transient remove failure")
removeCalls := 0
prepared.removeRoot = func(path string) error {
removeCalls++
if removeCalls == 1 {
return removeErr
}
return os.RemoveAll(path)
}
unlockCalls := 0
prepared.releaseLock = func(lock *MirrorLockHandle) error {
unlockCalls++
return lock.Close()
}
assert.ErrorIs(t, prepared.Close(), removeErr)
assertMirrorUnlocked(t, mirrorRoot)
require.NoError(t, prepared.Close())
assert.Equal(t, 1, unlockCalls)
})
t.Run("successful removal is not repeated", func(t *testing.T) {
prepared, mirrorRoot := prepareLegacy(t)
unlockErr := errors.New("transient unlock failure")
removeCalls := 0
prepared.removeRoot = func(path string) error {
removeCalls++
return os.RemoveAll(path)
}
unlockCalls := 0
prepared.releaseLock = func(lock *MirrorLockHandle) error {
unlockCalls++
if unlockCalls == 1 {
return unlockErr
}
return lock.Close()
}
assert.ErrorIs(t, prepared.Close(), unlockErr)
assert.NoDirExists(t, prepared.Root())
assertMirrorLocked(t, mirrorRoot)
require.NoError(t, prepared.Close())
assert.Equal(t, 1, removeCalls)
assertMirrorUnlocked(t, mirrorRoot)
})
}
func TestPrepareHTTPSyncClearsOnlySelectedHostCacheBeforeMirrorMutation(t *testing.T) {
remote := newMirrorTestRemote(t)
base := time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)
changed := remote.writeSession(t, "a.jsonl", base, "session a")
remote.writeSession(t, "b.jsonl", base, "session b")
remote.writeSession(t, "c.jsonl", base, "session c")
database, hs := newMirrorSync(t, remote, t.TempDir())
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
require.NoError(t, prepared.Close())
require.NoError(t, database.ReplaceRemoteSkippedFiles(
"devbox", map[string]int64{changed: 101},
))
require.NoError(t, database.ReplaceRemoteSkippedFiles(
"other-host", map[string]int64{"/remote/other.jsonl": 202},
))
remote.writeSession(t, "a.jsonl", base.Add(time.Second),
"session a", "continued")
type cacheSnapshot struct {
selected map[string]int64
other map[string]int64
err error
}
atArchive := make(chan cacheSnapshot, 1)
remote.onArchive = func(ArchiveRequest) {
selected, err := database.LoadRemoteSkippedFiles("devbox")
if err != nil {
atArchive <- cacheSnapshot{err: err}
return
}
other, err := database.LoadRemoteSkippedFiles("other-host")
atArchive <- cacheSnapshot{selected: selected, other: other, err: err}
}
prepared, err = hs.Prepare(context.Background())
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, prepared.Close()) })
var snapshot cacheSnapshot
select {
case snapshot = <-atArchive:
case <-time.After(time.Second):
require.FailNow(t, "timed out waiting for archive cache snapshot")
}
require.NoError(t, snapshot.err)
assert.Empty(t, snapshot.selected,
"selected cache must be clear before archive extraction starts")
assert.Equal(t, map[string]int64{"/remote/other.jsonl": 202}, snapshot.other)
selected, err := database.LoadRemoteSkippedFiles("devbox")
require.NoError(t, err)
assert.Empty(t, selected)
}
func TestPrepareHTTPSyncUnchangedPreservesSelectedHostCache(t *testing.T) {
remote := newMirrorTestRemote(t)
path := remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
database, hs := newMirrorSync(t, remote, t.TempDir())
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
require.NoError(t, prepared.Close())
require.NoError(t, database.ReplaceRemoteSkippedFiles(
"devbox", map[string]int64{path: 303},
))
require.NoError(t, database.ReplaceRemoteSkippedFiles(
"other-host", map[string]int64{"/remote/other.jsonl": 304},
))
requests := len(remote.archiveRequests)
prepared, err = hs.Prepare(context.Background())
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, prepared.Close()) })
assert.Len(t, remote.archiveRequests, requests)
selected, err := database.LoadRemoteSkippedFiles("devbox")
require.NoError(t, err)
assert.Equal(t, map[string]int64{path: 303}, selected)
other, err := database.LoadRemoteSkippedFiles("other-host")
require.NoError(t, err)
assert.Equal(t, map[string]int64{"/remote/other.jsonl": 304}, other)
}
func TestPrepareHTTPSyncFailureCleansOwnedResources(t *testing.T) {
t.Run("manifest failure releases lock", func(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusInternalServerError
_, hs := newMirrorSync(t, remote, t.TempDir())
prepared, err := hs.Prepare(context.Background())
require.Error(t, err)
assert.Nil(t, prepared)
assertMirrorUnlocked(t, MirrorDir(hs.DataDir, "devbox"))
})
t.Run("legacy extraction failure removes temp root", func(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusNotFound
path := remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
name, err := safeRemotePathArchiveName(path)
require.NoError(t, err)
remote.archiveBody = tarWithoutEndMarker(t, name, "partial legacy")
tempParent := t.TempDir()
setPortableTempDir(t, tempParent)
_, hs := newMirrorSync(t, remote, t.TempDir())
prepared, err := hs.Prepare(context.Background())
require.Error(t, err)
assert.Nil(t, prepared)
roots, globErr := filepath.Glob(filepath.Join(tempParent, "agentsview-http-*"))
require.NoError(t, globErr)
assert.Empty(t, roots)
assert.NoDirExists(t, MirrorDir(hs.DataDir, "devbox"))
assertMirrorUnlocked(t, MirrorDir(hs.DataDir, "devbox"))
})
t.Run("failed mirror extraction leaves bytes and invalidated cache", func(t *testing.T) {
remote := newMirrorTestRemote(t)
path := remote.writeSession(t, "a.jsonl",
time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC), "session a")
name, err := safeRemotePathArchiveName(path)
require.NoError(t, err)
remote.archiveBody = tarWithoutEndMarker(t, name, "partial mirror")
database, hs := newMirrorSync(t, remote, t.TempDir())
require.NoError(t, database.ReplaceRemoteSkippedFiles(
"devbox", map[string]int64{path: 404},
))
require.NoError(t, database.ReplaceRemoteSkippedFiles(
"other-host", map[string]int64{"/remote/other.jsonl": 505},
))
prepared, err := hs.Prepare(context.Background())
require.Error(t, err)
assert.Nil(t, prepared)
local, mapErr := safeRemappedRemotePath(MirrorDir(hs.DataDir, "devbox"), path)
require.NoError(t, mapErr)
body, readErr := os.ReadFile(local)
require.NoError(t, readErr)
assert.Equal(t, "partial mirror", string(body))
selected, loadErr := database.LoadRemoteSkippedFiles("devbox")
require.NoError(t, loadErr)
assert.Empty(t, selected)
other, loadErr := database.LoadRemoteSkippedFiles("other-host")
require.NoError(t, loadErr)
assert.Equal(t, map[string]int64{"/remote/other.jsonl": 505}, other)
assertMirrorUnlocked(t, MirrorDir(hs.DataDir, "devbox"))
})
}
func TestPrepareHTTPSyncsSortsHostsAndUnwindsOnFailure(t *testing.T) {
remoteA := newMirrorTestRemote(t)
remoteA.manifestStatus = http.StatusNotFound
remoteA.writeSession(t, "a.jsonl",
time.Date(2026, 7, 11, 10, 0, 0, 0, time.UTC), "host a")
remoteB := newMirrorTestRemote(t)
remoteB.manifestStatus = http.StatusInternalServerError
remoteB.writeSession(t, "b.jsonl",
time.Date(2026, 7, 11, 10, 0, 0, 0, time.UTC), "host b")
dataDir := t.TempDir()
tempParent := t.TempDir()
setPortableTempDir(t, tempParent)
database, syncA := newMirrorSync(t, remoteA, dataDir)
syncA.Host = "host-a"
syncB := syncA
syncB.Host = "host-b"
syncB.URL = remoteB.ts.URL
syncB.DB = database
var requestOrder []string
remoteA.onManifest = func() { requestOrder = append(requestOrder, "host-a") }
remoteB.onManifest = func() { requestOrder = append(requestOrder, "host-b") }
syncs := []HTTPSync{syncB, syncA}
prepared, err := PrepareHTTPSyncs(context.Background(), syncs)
require.Error(t, err)
assert.Nil(t, prepared)
assert.Equal(t, []string{"host-b", "host-a"}, []string{
syncs[0].Host, syncs[1].Host,
}, "preparation must not mutate caller ordering")
assert.Equal(t, []string{"host-a", "host-b"}, requestOrder)
var hostErr *HostError
require.ErrorAs(t, err, &hostErr)
assert.Equal(t, "host-b", hostErr.Host)
assertMirrorUnlocked(t, MirrorDir(dataDir, "host-a"))
assertMirrorUnlocked(t, MirrorDir(dataDir, "host-b"))
roots, globErr := filepath.Glob(filepath.Join(tempParent, "agentsview-http-*"))
require.NoError(t, globErr)
assert.Empty(t, roots, "unwind removes the successful legacy root and spools")
}
func TestPrepareHTTPSyncsLegacySourcesDoNotCreateWorkingDirectoryLocks(t *testing.T) {
remoteA := newMirrorTestRemote(t)
remoteA.writeSession(t, "a.jsonl",
time.Date(2026, 7, 11, 11, 0, 0, 0, time.UTC), "host a")
remoteB := newMirrorTestRemote(t)
remoteB.writeSession(t, "b.jsonl",
time.Date(2026, 7, 11, 11, 0, 0, 0, time.UTC), "host b")
workingDir := t.TempDir()
previousDir, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(workingDir))
t.Cleanup(func() { require.NoError(t, os.Chdir(previousDir)) })
setPortableTempDir(t, t.TempDir())
var requestOrder []string
remoteA.onArchive = func(ArchiveRequest) {
requestOrder = append(requestOrder, "host-a")
}
remoteB.onArchive = func(ArchiveRequest) {
requestOrder = append(requestOrder, "host-b")
}
syncA := HTTPSync{Host: "host-a", URL: remoteA.ts.URL, Client: remoteA.ts.Client()}
syncB := HTTPSync{Host: "host-b", URL: remoteB.ts.URL, Client: remoteB.ts.Client()}
syncs := []HTTPSync{syncB, syncA}
prepared, err := PrepareHTTPSyncs(context.Background(), syncs)
require.NoError(t, err)
require.NotNil(t, prepared)
require.NoError(t, prepared.Close())
assert.Equal(t, []string{"host-b", "host-a"}, []string{
syncs[0].Host, syncs[1].Host,
}, "preparation must not mutate caller ordering")
assert.Equal(t, []string{"host-a", "host-b"}, requestOrder)
assert.NoDirExists(t, filepath.Join(workingDir, "remote-mirrors"),
"legacy sources have no persistent mirror lock identity")
}
func TestPrepareHTTPSyncsOrdersConcurrentCallersByMirrorLockPath(t *testing.T) {
remoteA := newMirrorTestRemote(t)
remoteA.writeSession(t, "a.jsonl",
time.Date(2026, 7, 11, 14, 0, 0, 0, time.UTC), "mirror a")
remoteB := newMirrorTestRemote(t)
remoteB.writeSession(t, "b.jsonl",
time.Date(2026, 7, 11, 14, 0, 0, 0, time.UTC), "mirror b")
base := t.TempDir()
dataDirA := base + string(filepath.Separator) + "z" +
string(filepath.Separator) + ".." + string(filepath.Separator) + "a-data"
dataDirB := filepath.Join(base, "b-data")
database, syncA := newMirrorSync(t, remoteA, dataDirA)
syncA.Host = "shared-host"
syncB := syncA
syncB.DataDir = dataDirB
syncB.URL = remoteB.ts.URL
syncB.DB = database
assert.Less(t, MirrorDir(dataDirA, syncA.Host), MirrorDir(dataDirB, syncB.Host))
firstA := make(chan struct{})
firstB := make(chan struct{})
var onceA, onceB stdsync.Once
var orderMu stdsync.Mutex
orders := map[string][]string{}
record := func(
caller, mirror string, ownOnce *stdsync.Once,
own chan struct{}, other <-chan struct{},
) {
orderMu.Lock()
orders[caller] = append(orders[caller], mirror)
orderMu.Unlock()
ownOnce.Do(func() { close(own) })
select {
case <-other:
case <-time.After(50 * time.Millisecond):
}
}
remoteA.onManifestRequest = func(r *http.Request) {
record(r.Header.Get("Authorization"), "mirror-a",
&onceA, firstA, firstB)
}
remoteB.onManifestRequest = func(r *http.Request) {
record(r.Header.Get("Authorization"), "mirror-b",
&onceB, firstB, firstA)
}
callerOne := []HTTPSync{syncB, syncA}
callerOne[0].Token = "caller-one"
callerOne[1].Token = "caller-one"
callerTwo := []HTTPSync{syncA, syncB}
callerTwo[0].Token = "caller-two"
callerTwo[1].Token = "caller-two"
type result struct {
caller string
prepared *PreparedHTTPSyncs
err error
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
start := make(chan struct{})
results := make(chan result, 2)
for caller, syncs := range map[string][]HTTPSync{
"caller-one": callerOne,
"caller-two": callerTwo,
} {
go func() {
<-start
prepared, err := PrepareHTTPSyncs(ctx, syncs)
results <- result{caller: caller, prepared: prepared, err: err}
}()
}
close(start)
got := make(map[string]error, 2)
for range 2 {
select {
case result := <-results:
got[result.caller] = result.err
if result.prepared != nil {
require.NoError(t, result.prepared.Close())
}
case <-ctx.Done():
require.FailNow(t, "concurrent preparation deadlocked",
"orders=%v", orders)
}
}
require.NoError(t, got["caller-one"])
require.NoError(t, got["caller-two"])
orderMu.Lock()
assert.Equal(t, []string{"mirror-a", "mirror-b"}, orders["Bearer caller-one"])
assert.Equal(t, []string{"mirror-a", "mirror-b"}, orders["Bearer caller-two"])
orderMu.Unlock()
assert.Equal(t, []string{"shared-host", "shared-host"}, []string{
callerOne[0].Host, callerOne[1].Host,
})
assert.Equal(t, []string{dataDirB, dataDirA}, []string{
callerOne[0].DataDir, callerOne[1].DataDir,
}, "preparation must preserve caller-one input order")
assert.Equal(t, []string{dataDirA, dataDirB}, []string{
callerTwo[0].DataDir, callerTwo[1].DataDir,
}, "preparation must preserve caller-two input order")
}
func TestPrepareHTTPSyncsRejectsDuplicateCanonicalLockIdentity(t *testing.T) {
t.Run("clean parent alias", func(t *testing.T) {
base := t.TempDir()
canonical := filepath.Join(base, "data")
alias := base + string(filepath.Separator) + "z" +
string(filepath.Separator) + ".." + string(filepath.Separator) + "data"
assertDuplicateCanonicalLockIdentity(t, canonical, alias)
})
t.Run("symlink parent alias", func(t *testing.T) {
base := t.TempDir()
realParent := filepath.Join(base, "real")
require.NoError(t, os.MkdirAll(realParent, 0o755))
linkParent := filepath.Join(base, "link")
if err := os.Symlink(realParent, linkParent); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
assertDuplicateCanonicalLockIdentity(
t, filepath.Join(realParent, "data"), filepath.Join(linkParent, "data"),
)
})
t.Run("case alias on insensitive volume", func(t *testing.T) {
base := t.TempDir()
probe := filepath.Join(base, "CaseSensitiveProbe")
require.NoError(t, os.Mkdir(probe, 0o755))
probeAlias := filepath.Join(base, "cASEsENSITIVEpROBE")
actualInfo, err := os.Stat(probe)
require.NoError(t, err)
aliasInfo, err := os.Stat(probeAlias)
if os.IsNotExist(err) {
t.Skip("test volume is case-sensitive")
}
require.NoError(t, err)
if !os.SameFile(actualInfo, aliasInfo) {
t.Skip("case aliases do not identify the same directory")
}
require.NoError(t, os.Remove(probe))
actualParent := filepath.Join(base, "InitiallyMissingCaseParent")
aliasParent := filepath.Join(base, "iNITIALLYmISSINGcASEpARENT")
assert.NoDirExists(t, actualParent)
assert.NoDirExists(t, aliasParent)
assertDuplicateCanonicalLockIdentity(
t, filepath.Join(actualParent, "data"), filepath.Join(aliasParent, "data"),
)
})
t.Run("unicode normalization alias", func(t *testing.T) {
base := t.TempDir()
probe := filepath.Join(base, "Caf\u00e9Probe")
probeAlias := filepath.Join(base, "Cafe\u0301Probe")
require.NoError(t, os.Mkdir(probe, 0o755))
actualInfo, err := os.Stat(probe)
require.NoError(t, err)
aliasInfo, err := os.Stat(probeAlias)
if os.IsNotExist(err) {
t.Skip("test volume does not alias Unicode normalization forms")
}
require.NoError(t, err)
if !os.SameFile(actualInfo, aliasInfo) {
t.Skip("Unicode normalization forms identify different directories")
}
require.NoError(t, os.Remove(probe))
actualParent := filepath.Join(base, "Caf\u00e9Missing")
aliasParent := filepath.Join(base, "Cafe\u0301Missing")
assert.NoDirExists(t, actualParent)
assert.NoDirExists(t, aliasParent)
assertDuplicateCanonicalLockIdentity(
t, filepath.Join(actualParent, "data"), filepath.Join(aliasParent, "data"),
)
})
}
func assertDuplicateCanonicalLockIdentity(t *testing.T, dataDirA, dataDirB string) {
t.Helper()
remote := newMirrorTestRemote(t)
remote.writeSession(t, "duplicate.jsonl",
time.Date(2026, 7, 11, 14, 30, 0, 0, time.UTC), "duplicate")
database, syncA := newMirrorSync(t, remote, dataDirA)
syncA.Host = "shared-host"
syncB := syncA
syncB.DataDir = dataDirB
syncB.DB = database
manifestRequests := 0
remote.onManifest = func() { manifestRequests++ }
syncs := []HTTPSync{syncB, syncA}
ctx, cancel := context.WithTimeout(context.Background(), 750*time.Millisecond)
defer cancel()
prepared, err := PrepareHTTPSyncs(ctx, syncs)
require.Error(t, err)
assert.Nil(t, prepared)
assert.ErrorIs(t, err, ErrDuplicateMirrorLock)
var hostErr *HostError
require.ErrorAs(t, err, &hostErr)
assert.Equal(t, "shared-host", hostErr.Host)
assert.Equal(t, "resolve mirror lock", hostErr.Operation)
assert.Contains(t, hostErr.Error(), `"shared-host"`)
assert.Zero(t, manifestRequests,
"duplicates must fail before acquiring a lock or preparing a source")
lockArtifact := MirrorDir(dataDirA, syncA.Host) + ".lock"
lockInfo, statErr := os.Stat(lockArtifact)
require.NoError(t, statErr)
assert.False(t, lockInfo.IsDir())
if runtime.GOOS != "windows" {
assert.Equal(t, os.FileMode(0o600), lockInfo.Mode().Perm(),
"preflight creates only the same private lock artifact acquisition needs")
}
assert.Equal(t, []string{dataDirB, dataDirA}, []string{
syncs[0].DataDir, syncs[1].DataDir,
}, "duplicate detection must preserve caller order")
lockCtx, lockCancel := context.WithTimeout(context.Background(), time.Second)
defer lockCancel()
lock, lockErr := AcquireMirrorLock(lockCtx, MirrorDir(dataDirA, syncA.Host))
require.NoError(t, lockErr)
require.NoError(t, lock.Close())
}
func TestPreparedHTTPSyncsHoldAllLocksUntilClose(t *testing.T) {
remoteA := newMirrorTestRemote(t)
remoteA.writeSession(t, "a.jsonl",
time.Date(2026, 7, 11, 11, 0, 0, 0, time.UTC), "host a")
remoteB := newMirrorTestRemote(t)
remoteB.writeSession(t, "b.jsonl",
time.Date(2026, 7, 11, 11, 0, 0, 0, time.UTC), "host b")
dataDir := t.TempDir()
database, syncA := newMirrorSync(t, remoteA, dataDir)
syncA.Host = "host-a"
syncB := syncA
syncB.Host = "host-b"
syncB.URL = remoteB.ts.URL
syncB.DB = database
prepared, err := PrepareHTTPSyncs(
context.Background(), []HTTPSync{syncB, syncA},
)
require.NoError(t, err)
contributors, release, err := prepared.BorrowRebuildContributors()
require.NoError(t, err)
t.Cleanup(func() {
release()
require.NoError(t, prepared.Close())
})
assert.Equal(t, []string{"host-a", "host-b"}, []string{
contributors[0].Name, contributors[1].Name,
})
for _, host := range []string{"host-a", "host-b"} {
lockCtx, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond)
competing, lockErr := AcquireMirrorLock(
lockCtx, MirrorDir(dataDir, host),
)
cancel()
if competing != nil {
require.NoError(t, competing.Close())
}
assert.Error(t, lockErr,
"all mirror locks remain held across earlier contributor work")
}
release()
require.NoError(t, prepared.Close())
for _, host := range []string{"host-a", "host-b"} {
lockCtx, cancel := context.WithTimeout(context.Background(), time.Second)
competing, lockErr := AcquireMirrorLock(
lockCtx, MirrorDir(dataDir, host),
)
cancel()
require.NoError(t, lockErr, host)
require.NotNil(t, competing, host)
require.NoError(t, competing.Close())
}
_, _, err = prepared.BorrowRebuildContributors()
assert.ErrorIs(t, err, ErrPreparedClosed,
"closed aggregate cannot expose contributors")
}
func TestPreparedHTTPSyncsBorrowBlocksCloseUntilRelease(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.writeSession(t, "borrow.jsonl",
time.Date(2026, 7, 11, 15, 30, 0, 0, time.UTC), "borrowed")
dataDir := t.TempDir()
_, hs := newMirrorSync(t, remote, dataDir)
prepared, err := PrepareHTTPSyncs(context.Background(), []HTTPSync{hs})
require.NoError(t, err)
contributors, release, err := prepared.BorrowRebuildContributors()
require.NoError(t, err)
t.Cleanup(func() {
release()
require.NoError(t, prepared.Close())
})
require.Len(t, contributors, 1)
assert.Equal(t, hs.Host, contributors[0].Name)
assert.ErrorIs(t, prepared.Close(), ErrPreparedInUse)
_, _, err = prepared.BorrowRebuildContributors()
assert.ErrorIs(t, err, ErrPreparedClosed,
"a Close attempt ends new borrowing while retained ownership remains retryable")
lockCtx, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond)
competing, lockErr := AcquireMirrorLock(lockCtx, MirrorDir(dataDir, hs.Host))
cancel()
if competing != nil {
require.NoError(t, competing.Close())
}
assert.Error(t, lockErr, "borrowed Close must retain its mirror lock")
release()
release()
require.NoError(t, prepared.Close())
lockCtx, cancel = context.WithTimeout(context.Background(), time.Second)
competing, lockErr = AcquireMirrorLock(lockCtx, MirrorDir(dataDir, hs.Host))
cancel()
require.NoError(t, lockErr)
require.NotNil(t, competing)
require.NoError(t, competing.Close())
_, _, err = prepared.BorrowRebuildContributors()
assert.ErrorIs(t, err, ErrPreparedClosed)
}
func TestPreparedHTTPSyncsBorrowAndCloseAreRaceSafe(t *testing.T) {
prepared := &PreparedHTTPSyncs{sources: []*PreparedHTTP{{
sync: HTTPSync{Host: "race-host"},
lock: &MirrorLockHandle{},
releaseLock: func(*MirrorLockHandle) error { return nil },
}}}
_, initialRelease, err := prepared.BorrowRebuildContributors()
require.NoError(t, err)
start := make(chan struct{})
borrowResults := make(chan error, 8)
closeResult := make(chan error, 1)
for range 8 {
go func() {
<-start
_, release, borrowErr := prepared.BorrowRebuildContributors()
if borrowErr == nil {
release()
release()
}
borrowResults <- borrowErr
}()
}
go func() {
<-start
closeResult <- prepared.Close()
}()
close(start)
for range 8 {
borrowErr := <-borrowResults
assert.True(t, borrowErr == nil || errors.Is(borrowErr, ErrPreparedClosed),
"unexpected borrow result: %v", borrowErr)
}
assert.ErrorIs(t, <-closeResult, ErrPreparedInUse)
initialRelease()
require.NoError(t, prepared.Close())
}
func TestPreparedHTTPSyncsCloseReversesJoinsAndRetries(t *testing.T) {
transient := errors.New("transient release")
var order []string
bCalls := 0
prepared := &PreparedHTTPSyncs{sources: []*PreparedHTTP{
{
lock: &MirrorLockHandle{},
releaseLock: func(*MirrorLockHandle) error {
order = append(order, "host-a")
return nil
},
},
{
lock: &MirrorLockHandle{},
releaseLock: func(*MirrorLockHandle) error {
order = append(order, "host-b")
bCalls++
if bCalls == 1 {
return transient
}
return nil
},
},
}}
assert.ErrorIs(t, prepared.Close(), transient)
assert.Equal(t, []string{"host-b", "host-a"}, order)
require.NoError(t, prepared.Close())
assert.Equal(t, []string{"host-b", "host-a", "host-b"}, order,
"only failed ownership is retained for retry")
require.NoError(t, prepared.Close(), "fully closed aggregate is idempotent")
}
func TestPrepareHTTPSyncsReturnsFailedUnwindOwnership(t *testing.T) {
remoteA := newMirrorTestRemote(t)
remoteA.manifestStatus = http.StatusNotFound
remoteA.writeSession(t, "a.jsonl",
time.Date(2026, 7, 11, 15, 0, 0, 0, time.UTC), "legacy a")
remoteB := newMirrorTestRemote(t)
remoteB.manifestStatus = http.StatusInternalServerError
remoteB.writeSession(t, "b.jsonl",
time.Date(2026, 7, 11, 15, 0, 0, 0, time.UTC), "host b")
tempParent := t.TempDir()
setPortableTempDir(t, tempParent)
dataDir := t.TempDir()
database, syncA := newMirrorSync(t, remoteA, dataDir)
syncA.Host = "host-a"
syncB := syncA
syncB.Host = "host-b"
syncB.URL = remoteB.ts.URL
syncB.DB = database
removeErr := errors.New("transient aggregate remove failure")
unlockErr := errors.New("transient aggregate unlock failure")
removeCalls := 0
unlockCalls := 0
var ownedRoot string
prepared, err := prepareHTTPSyncs(
context.Background(), []HTTPSync{syncB, syncA},
func(ctx context.Context, hs HTTPSync) (*PreparedHTTP, error) {
source, prepareErr := hs.Prepare(ctx)
if prepareErr != nil || hs.Host != "host-a" {
return source, prepareErr
}
ownedRoot = source.Root()
source.removeRoot = func(path string) error {
removeCalls++
if removeCalls == 1 {
return removeErr
}
return os.RemoveAll(path)
}
source.releaseLock = func(lock *MirrorLockHandle) error {
unlockCalls++
if unlockCalls == 1 {
return unlockErr
}
return lock.Close()
}
return source, nil
},
)
require.Error(t, err)
require.NotNil(t, prepared,
"failed cleanup ownership must be returned with the preparation error")
assert.ErrorIs(t, err, removeErr)
assert.ErrorIs(t, err, unlockErr)
var primary *HostError
require.ErrorAs(t, err, &primary)
assert.Equal(t, "host-b", primary.Host)
assert.Equal(t, "prepare", primary.Operation)
assert.Contains(t, err.Error(), `HTTP host "host-a" cleanup prepared source`)
assert.DirExists(t, ownedRoot)
assertMirrorLocked(t, MirrorDir(dataDir, "host-a"))
require.NoError(t, prepared.Close())
assert.NoDirExists(t, ownedRoot)
assertMirrorUnlocked(t, MirrorDir(dataDir, "host-a"))
require.NoError(t, prepared.Close(), "cleanup retry must remain idempotent")
}
func TestPrepareHTTPSyncRetainsCurrentSourceWhenCleanupFails(t *testing.T) {
t.Run("single source", func(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusInternalServerError
_, hs := newMirrorSync(t, remote, t.TempDir())
unlockErr := errors.New("transient current-source unlock failure")
unlockCalls := 0
source, err := hs.prepare(context.Background(), func(source *PreparedHTTP) {
source.releaseLock = func(lock *MirrorLockHandle) error {
unlockCalls++
if unlockCalls == 1 {
return unlockErr
}
return lock.Close()
}
})
require.Error(t, err)
require.NotNil(t, source)
assert.ErrorIs(t, err, unlockErr)
var statusErr *StatusError
assert.ErrorAs(t, err, &statusErr)
assertMirrorLocked(t, MirrorDir(hs.DataDir, hs.Host))
require.NoError(t, source.Close())
assertMirrorUnlocked(t, MirrorDir(hs.DataDir, hs.Host))
})
t.Run("aggregate current source", func(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusInternalServerError
_, hs := newMirrorSync(t, remote, t.TempDir())
unlockErr := errors.New("transient aggregate current-source unlock failure")
unlockCalls := 0
prepared, err := prepareHTTPSyncs(
context.Background(), []HTTPSync{hs},
func(ctx context.Context, hs HTTPSync) (*PreparedHTTP, error) {
return hs.prepare(ctx, func(source *PreparedHTTP) {
source.releaseLock = func(lock *MirrorLockHandle) error {
unlockCalls++
if unlockCalls <= 2 {
return unlockErr
}
return lock.Close()
}
})
},
)
require.Error(t, err)
require.NotNil(t, prepared)
assert.ErrorIs(t, err, unlockErr)
var hostErr *HostError
require.ErrorAs(t, err, &hostErr)
assert.Equal(t, hs.Host, hostErr.Host)
assert.Equal(t, "prepare", hostErr.Operation)
assert.Contains(t, err.Error(), "cleanup prepared source")
assertMirrorLocked(t, MirrorDir(hs.DataDir, hs.Host))
require.NoError(t, prepared.Close())
assertMirrorUnlocked(t, MirrorDir(hs.DataDir, hs.Host))
})
}
func TestHTTPSyncRunHandlesPreparationCleanupOwnership(t *testing.T) {
t.Run("transient cleanup is completed before return", func(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusInternalServerError
_, hs := newMirrorSync(t, remote, t.TempDir())
unlockErr := errors.New("transient Run unlock failure")
unlockCalls := 0
base := hs
hs.runPrepare = func(ctx context.Context) (*PreparedHTTP, error) {
return base.prepare(ctx, func(source *PreparedHTTP) {
source.releaseLock = func(lock *MirrorLockHandle) error {
unlockCalls++
if unlockCalls == 1 {
return unlockErr
}
return lock.Close()
}
})
}
_, err := hs.Run(context.Background())
require.Error(t, err)
assert.ErrorIs(t, err, unlockErr)
var owner *PreparedCleanupError
assert.False(t, errors.As(err, &owner),
"successful Run retry must not return cleanup ownership")
assert.Equal(t, 2, unlockCalls)
assertMirrorUnlocked(t, MirrorDir(hs.DataDir, hs.Host))
})
t.Run("persistent cleanup ownership is retryable from error", func(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusInternalServerError
_, hs := newMirrorSync(t, remote, t.TempDir())
unlockErr := errors.New("persistent Run unlock failure")
unlockCalls := 0
base := hs
hs.runPrepare = func(ctx context.Context) (*PreparedHTTP, error) {
return base.prepare(ctx, func(source *PreparedHTTP) {
source.releaseLock = func(lock *MirrorLockHandle) error {
unlockCalls++
if unlockCalls <= 2 {
return unlockErr
}
return lock.Close()
}
})
}
_, err := hs.Run(context.Background())
require.Error(t, err)
assert.ErrorIs(t, err, unlockErr)
var statusErr *StatusError
assert.ErrorAs(t, err, &statusErr)
var owner *PreparedCleanupError
require.ErrorAs(t, err, &owner)
assert.Equal(t, 2, unlockCalls, "Run must not double-close before returning")
assertMirrorLocked(t, MirrorDir(hs.DataDir, hs.Host))
require.NoError(t, owner.RetryCleanup())
assert.Equal(t, 3, unlockCalls)
assertMirrorUnlocked(t, MirrorDir(hs.DataDir, hs.Host))
require.NoError(t, owner.RetryCleanup(), "cleanup retry is idempotent")
assert.Equal(t, 3, unlockCalls)
})
}
func TestCleanupRegistryBlocksLaterRunWithPreparedCleanupError(t *testing.T) {
remote := newMirrorTestRemote(t)
remote.manifestStatus = http.StatusInternalServerError
_, hs := newMirrorSync(t, remote, t.TempDir())
unlockErr := errors.New("persistent registry unlock failure")
unlockCalls := 0
base := hs
hs.runPrepare = func(ctx context.Context) (*PreparedHTTP, error) {
return base.prepare(ctx, func(source *PreparedHTTP) {
source.releaseLock = func(lock *MirrorLockHandle) error {
unlockCalls++
if unlockCalls <= 4 {
return unlockErr
}
return lock.Close()
}
})
}
var registry CleanupRegistry
callbacks := 0
_, err := registry.Run(func() (SyncStats, error) {
callbacks++
return hs.Run(context.Background())
})
require.Error(t, err)
var owner *PreparedCleanupError
require.ErrorAs(t, err, &owner)
assert.ErrorIs(t, err, unlockErr)
assert.Equal(t, 3, unlockCalls)
assertMirrorLocked(t, MirrorDir(hs.DataDir, hs.Host))
_, err = registry.Run(func() (SyncStats, error) {
callbacks++
return SyncStats{}, nil
})
var pending *PendingCleanupError
require.ErrorAs(t, err, &pending)
var retained *PreparedCleanupError
require.ErrorAs(t, err, &retained)
assert.Same(t, owner, retained)
assert.ErrorIs(t, err, owner)
assert.ErrorIs(t, err, unlockErr)
assert.Equal(t, 1, callbacks, "pending cleanup must block the later callback")
assert.Equal(t, 4, unlockCalls)
assertMirrorLocked(t, MirrorDir(hs.DataDir, hs.Host))
_, err = registry.Run(func() (SyncStats, error) {
callbacks++
return SyncStats{SessionsSynced: 1}, nil
})
require.NoError(t, err)
assert.Equal(t, 2, callbacks)
assert.Equal(t, 5, unlockCalls)
assertMirrorUnlocked(t, MirrorDir(hs.DataDir, hs.Host))
}
func TestPreparedHTTPCloseAttemptMakesSourceUnusableUntilCleanupCompletes(t *testing.T) {
unlockErr := errors.New("unlock remains pending")
unlockCalls := 0
prepared := &PreparedHTTP{
root: t.TempDir(),
lock: &MirrorLockHandle{},
releaseLock: func(*MirrorLockHandle) error {
unlockCalls++
if unlockCalls == 1 {
return unlockErr
}
return nil
},
}
require.ErrorIs(t, prepared.Close(), unlockErr)
_, importErr := prepared.ImportActive(context.Background())
assert.ErrorContains(t, importErr, "closed")
_, contributorErr := prepared.RebuildContributor()
assert.ErrorContains(t, contributorErr, "closed")
require.NoError(t, prepared.Close())
assert.Equal(t, 2, unlockCalls)
}
type failingArchiveBody struct{ err error }
func (r failingArchiveBody) Read([]byte) (int, error) { return 0, r.err }
func (failingArchiveBody) Close() error { return nil }
func TestCleanupRegistryRetainsFailedArchiveSpoolCleanup(t *testing.T) {
spoolRoot := t.TempDir()
readErr := errors.New("archive transfer failed")
removeErr := errors.New("spool removal failed")
removeCalls := 0
hs := HTTPSync{removeArchiveSpool: func(path string) error {
removeCalls++
if removeCalls <= 2 {
return removeErr
}
return os.RemoveAll(path)
}}
registry := new(CleanupRegistry)
callbacks := 0
_, err := registry.Run(func() (SyncStats, error) {
callbacks++
archive, downloadErr := hs.downloadArchive(
context.Background(),
&http.Response{
StatusCode: http.StatusOK,
Body: failingArchiveBody{err: readErr},
},
"Downloading archive", spoolRoot,
)
assert.Nil(t, archive)
return SyncStats{}, downloadErr
})
require.Error(t, err)
assert.ErrorIs(t, err, readErr)
assert.ErrorIs(t, err, removeErr)
var owner *downloadedArchiveCleanupError
require.ErrorAs(t, err, &owner)
assert.Equal(t, 2, removeCalls, "registry retries cleanup before retaining it")
_, err = registry.Run(func() (SyncStats, error) {
callbacks++
return SyncStats{SessionsSynced: 1}, nil
})
require.NoError(t, err)
assert.Equal(t, 2, callbacks)
assert.Equal(t, 3, removeCalls)
entries, readDirErr := os.ReadDir(spoolRoot)
require.NoError(t, readDirErr)
assert.Empty(t, entries)
}
func TestPrepareHTTPSyncsCacheInvalidationFailureDoesNotMutateMirror(t *testing.T) {
remote := newMirrorTestRemote(t)
base := time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC)
path := remote.writeSession(t, "a.jsonl", base, "original")
dataDir := t.TempDir()
database, hs := newMirrorSync(t, remote, dataDir)
hs.Host = "readonly-host"
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
require.NoError(t, prepared.Close())
local, err := safeRemappedRemotePath(MirrorDir(dataDir, hs.Host), path)
require.NoError(t, err)
beforeBytes, err := os.ReadFile(local)
require.NoError(t, err)
beforeInfo, err := os.Stat(local)
require.NoError(t, err)
requestsBefore := len(remote.archiveRequests)
remote.writeSession(t, "a.jsonl", base.Add(time.Second),
"replacement with a different size")
readonly, err := db.OpenReadOnly(database.Path())
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, readonly.Close()) })
hs.DB = readonly
preparedSet, err := PrepareHTTPSyncs(context.Background(), []HTTPSync{hs})
require.Error(t, err)
assert.Nil(t, preparedSet)
var hostErr *HostError
require.ErrorAs(t, err, &hostErr)
assert.Equal(t, "readonly-host", hostErr.Host)
assert.ErrorIs(t, err, db.ErrReadOnly)
afterBytes, readErr := os.ReadFile(local)
require.NoError(t, readErr)
afterInfo, statErr := os.Stat(local)
require.NoError(t, statErr)
assert.Equal(t, beforeBytes, afterBytes)
assert.Equal(t, beforeInfo.ModTime(), afterInfo.ModTime())
assert.Len(t, remote.archiveRequests, requestsBefore,
"cache failure must stop before archive download or extraction")
assertMirrorUnlocked(t, MirrorDir(dataDir, hs.Host))
}
func TestPrepareHTTPSyncSameMtimeSizeChangeRecoversAfterAbortedRebuild(t *testing.T) {
remote := newMirrorTestRemote(t)
base := time.Date(2026, 7, 11, 13, 0, 0, 0, time.UTC)
changed := remote.writeSession(t, "changed.jsonl", base,
"the original message is intentionally much longer")
deleted := remote.writeSession(t, "deleted.jsonl", base, "retained archive row")
dataDir := t.TempDir()
database, hs := newMirrorSync(t, remote, dataDir)
stats, err := hs.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 2, stats.SessionsSynced)
initial, err := database.ListSessions(
context.Background(), db.SessionFilter{Limit: 10},
)
require.NoError(t, err)
var changedSessionID string
for _, session := range initial.Sessions {
messages, messageErr := database.GetMessages(
context.Background(), session.ID, 0, 10, true,
)
require.NoError(t, messageErr)
if len(messages) == 1 && strings.Contains(
messages[0].Content, "intentionally much longer",
) {
changedSessionID = session.ID
}
}
require.NotEmpty(t, changedSessionID)
require.NoError(t, database.ReplaceRemoteSkippedFiles(
hs.Host, map[string]int64{changed: base.UnixNano()},
))
remote.writeSession(t, "changed.jsonl", base, "new")
require.NoError(t, os.Remove(deleted))
prepared, err := hs.Prepare(context.Background())
require.NoError(t, err)
changedLocal, err := safeRemappedRemotePath(prepared.Root(), changed)
require.NoError(t, err)
changedBytes, err := os.ReadFile(changedLocal)
require.NoError(t, err)
assert.Contains(t, string(changedBytes), "new")
deletedLocal, err := safeRemappedRemotePath(prepared.Root(), deleted)
require.NoError(t, err)
assert.NoFileExists(t, deletedLocal)
cache, err := database.LoadRemoteSkippedFiles(hs.Host)
require.NoError(t, err)
assert.Empty(t, cache, "cache invalidation commits before mirror mutation")
require.NoError(t, prepared.Close(), "simulate rebuild abort before import")
stats, err = hs.Run(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, stats.SessionsSynced)
page, err := database.ListSessions(context.Background(), db.SessionFilter{Limit: 10})
require.NoError(t, err)
require.Len(t, page.Sessions, 2,
"remote deletion must not delete the persistent archive row")
messages, err := database.GetMessages(
context.Background(), changedSessionID, 0, 10, true,
)
require.NoError(t, err)
require.Len(t, messages, 1)
assert.Equal(t, "new", messages[0].Content)
}
func tarWithoutEndMarker(t *testing.T, name, body string) []byte {
t.Helper()
archive := buildHTTPTestTar(t, map[string]string{name: body})
require.GreaterOrEqual(t, len(archive), tarEndMarkerSize)
return archive[:len(archive)-tarEndMarkerSize]
}
func setPortableTempDir(t *testing.T, dir string) {
t.Helper()
for _, name := range []string{"TMPDIR", "TMP", "TEMP"} {
t.Setenv(name, dir)
}
}
func assertMirrorLocked(t *testing.T, mirrorRoot string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
lock, err := AcquireMirrorLock(ctx, mirrorRoot)
if lock != nil {
require.NoError(t, lock.Close())
}
assert.Error(t, err)
}
func assertMirrorUnlocked(t *testing.T, mirrorRoot string) {
t.Helper()
lock, err := AcquireMirrorLock(context.Background(), mirrorRoot)
require.NoError(t, err)
require.NoError(t, lock.Close())
}