Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions cmd/backscroll/context_startup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/pablontiv/backscroll/internal/compat"
"github.com/pablontiv/backscroll/internal/config"
"github.com/pablontiv/backscroll/internal/recovery"
"github.com/pablontiv/backscroll/internal/storage"
)

Expand Down Expand Up @@ -67,7 +68,7 @@ func TestRootContextRunsStartupOnceAndSupportsAllFormats(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
startupCalls := 0
root := buildRootCmdWithStartup(&stdout, &stderr, func(_ context.Context, progress io.Writer, class startupCommandClass) startupResult {
root := buildRootCmdWithDependencies(&stdout, &stderr, func(_ context.Context, progress io.Writer, class startupCommandClass) startupResult {
startupCalls++
if class != startupSnapshotRead {
t.Fatalf("context startup class=%q want %q", class, startupSnapshotRead)
Expand All @@ -76,7 +77,7 @@ func TestRootContextRunsStartupOnceAndSupportsAllFormats(t *testing.T) {
t.Fatalf("write startup progress: %v", err)
}
return startupResult{Config: cfg}
})
}, newStartupSyncService(), recovery.Execute)
root.SetArgs(tc.args)

if err := root.Execute(); err != nil {
Expand Down Expand Up @@ -112,10 +113,10 @@ func TestRootContextEmitsCommandDiagnosticsInEveryFormat(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
startupCalls := 0
root := buildRootCmdWithStartup(&stdout, &stderr, func(context.Context, io.Writer, startupCommandClass) startupResult {
root := buildRootCmdWithDependencies(&stdout, &stderr, func(context.Context, io.Writer, startupCommandClass) startupResult {
startupCalls++
return startupResult{Config: cfg}
})
}, newStartupSyncService(), recovery.Execute)
root.SetArgs(tc.args)

err := root.Execute()
Expand Down Expand Up @@ -166,17 +167,17 @@ func TestRootContextBusyFollowerUsesCommittedSnapshot(t *testing.T) {
}
return originalPrepare(ctx, gotCfg, class)
}
coordinator.sync = func(context.Context, *config.Config, io.Writer) error {
coordinator.syncService.open = func(context.Context, string) (*storage.Database, error) {
t.Fatal("busy snapshot follower must not synchronize")
return nil
return nil, nil
}

var stdout, stderr bytes.Buffer
startupCalls := 0
root := buildRootCmdWithStartup(&stdout, &stderr, func(ctx context.Context, progress io.Writer, class startupCommandClass) startupResult {
root := buildRootCmdWithDependencies(&stdout, &stderr, func(ctx context.Context, progress io.Writer, class startupCommandClass) startupResult {
startupCalls++
return coordinator.coordinate(ctx, cfg, progress, class)
})
}, coordinator.syncService, recovery.Execute)
root.SetArgs([]string{"context", "--uuid", "anchor", "--before", "0", "--after", "0", "--max-tokens", "16384", "--json"})

if err := root.Execute(); err != nil {
Expand Down
30 changes: 11 additions & 19 deletions cmd/backscroll/empty_pi_replay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,16 @@ func TestMarkedEmptyPiRacyCleanComparesCanonicalContentHash(t *testing.T) {
hashes: map[string]string{path: initialHash},
}

oldActiveInputs, oldNewRegistry := maybeAutoSyncActiveInputs, maybeAutoSyncNewRegistry
t.Cleanup(func() {
maybeAutoSyncActiveInputs = oldActiveInputs
maybeAutoSyncNewRegistry = oldNewRegistry
})
maybeAutoSyncActiveInputs = func([]string) ([]input_config.InputDefinition, input_config.InputMode, error) {
syncService := newStartupSyncService()
syncService.activeInputs = func([]string) ([]input_config.InputDefinition, input_config.InputMode, error) {
return []input_config.InputDefinition{{
ID: "marked-empty-pi",
Source: "session",
Active: true,
Decode: input_config.DecodeConfig{Format: "pi"},
}}, input_config.ModeDeclarative, nil
}
maybeAutoSyncNewRegistry = func() *readers.Registry {
syncService.newRegistry = func() *readers.Registry {
registry := readers.NewRegistry()
registry.Register(reader)
return registry
Expand All @@ -95,7 +91,7 @@ func TestMarkedEmptyPiRacyCleanComparesCanonicalContentHash(t *testing.T) {
t.Fatal(err)
}

if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("immutable racy-clean sync: %v", err)
}
if reader.hashCalls != 1 || reader.parseCalls != 0 {
Expand Down Expand Up @@ -129,7 +125,7 @@ func TestMarkedEmptyPiRacyCleanComparesCanonicalContentHash(t *testing.T) {
}
reader.hashes[path] = "raw-changed"

if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("changed racy-clean sync: %v", err)
}
if reader.hashCalls != 2 || reader.parseCalls != 1 {
Expand All @@ -154,20 +150,16 @@ func TestEmptyPiReplayCapDrainsAndConverges(t *testing.T) {
reader := &emptyPiReplayReader{hashes: make(map[string]string)}
oldTime := time.Now().Add(-time.Hour)

oldActiveInputs, oldNewRegistry := maybeAutoSyncActiveInputs, maybeAutoSyncNewRegistry
t.Cleanup(func() {
maybeAutoSyncActiveInputs = oldActiveInputs
maybeAutoSyncNewRegistry = oldNewRegistry
})
maybeAutoSyncActiveInputs = func([]string) ([]input_config.InputDefinition, input_config.InputMode, error) {
syncService := newStartupSyncService()
syncService.activeInputs = func([]string) ([]input_config.InputDefinition, input_config.InputMode, error) {
return []input_config.InputDefinition{{
ID: "empty-pi-replay",
Source: "session",
Active: true,
Decode: input_config.DecodeConfig{Format: "pi"},
}}, input_config.ModeDeclarative, nil
}
maybeAutoSyncNewRegistry = func() *readers.Registry {
syncService.newRegistry = func() *readers.Registry {
registry := readers.NewRegistry()
registry.Register(reader)
return registry
Expand Down Expand Up @@ -211,7 +203,7 @@ func TestEmptyPiReplayCapDrainsAndConverges(t *testing.T) {
t.Fatal(err)
}

if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("first replay: %v", err)
}
if reader.parseCalls != 200 {
Expand All @@ -237,7 +229,7 @@ func TestEmptyPiReplayCapDrainsAndConverges(t *testing.T) {
t.Fatal(err)
}

if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("second replay: %v", err)
}
if reader.parseCalls != 201 {
Expand All @@ -259,7 +251,7 @@ func TestEmptyPiReplayCapDrainsAndConverges(t *testing.T) {
t.Fatal(err)
}

if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("convergence replay: %v", err)
}
if reader.parseCalls != 201 {
Expand Down
15 changes: 9 additions & 6 deletions cmd/backscroll/index_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,6 @@ func TestAutoSyncFailuresBlockCachedConsumers(t *testing.T) {
setup: func(t *testing.T, root string) {
writeInputManifest(t, root, "claude", root, []string{"*.jsonl"}, nil)
writeFile(t, filepath.Join(root, "session.jsonl"), `{"type":"message","message":{"role":"user","content":"fresh"}}`+"\n")
orig := maybeAutoSyncSyncFiles
maybeAutoSyncSyncFiles = func(context.Context, *storage.Database, []storage.IndexedFile) error {
return fmt.Errorf("injected sync failure")
}
t.Cleanup(func() { maybeAutoSyncSyncFiles = orig })
},
wantError: "sync files",
},
Expand Down Expand Up @@ -395,8 +390,16 @@ func TestAutoSyncFailuresBlockCachedConsumers(t *testing.T) {
setIndexPolicyEnv(t, dbPath, t.TempDir())
tc.setup(t, root)

syncService := newStartupSyncService()
if tc.name == "sync" {
syncService.syncFiles = func(context.Context, *storage.Database, []storage.IndexedFile) error {
return fmt.Errorf("injected sync failure")
}
}
var stdout, stderr bytes.Buffer
err := run(&stdout, &stderr, mode.argv)
cmd := buildRootCmdWithCoordinator(&stdout, &stderr, newStartupCoordinatorWithSyncService(syncService))
cmd.SetArgs(mode.argv)
err := cmd.Execute()
if err == nil {
t.Fatalf("auto-sync %s failure succeeded; stdout=%q stderr=%q", tc.name, stdout.String(), stderr.String())
}
Expand Down
36 changes: 14 additions & 22 deletions cmd/backscroll/origin_replay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,8 @@ func TestOriginParserSyncReplayIsBoundedAndConverges(t *testing.T) {
}

reader := &originReplayClaudeReader{}
oldActiveInputs, oldNewRegistry := maybeAutoSyncActiveInputs, maybeAutoSyncNewRegistry
t.Cleanup(func() {
maybeAutoSyncActiveInputs = oldActiveInputs
maybeAutoSyncNewRegistry = oldNewRegistry
})
maybeAutoSyncActiveInputs = func([]string) ([]input_config.InputDefinition, input_config.InputMode, error) {
syncService := newStartupSyncService()
syncService.activeInputs = func([]string) ([]input_config.InputDefinition, input_config.InputMode, error) {
return []input_config.InputDefinition{{
ID: "origin-replay-claude",
Source: "session",
Expand All @@ -109,14 +105,14 @@ func TestOriginParserSyncReplayIsBoundedAndConverges(t *testing.T) {
Decode: input_config.DecodeConfig{Format: "claude"},
}}, input_config.ModeDeclarative, nil
}
maybeAutoSyncNewRegistry = func() *readers.Registry {
syncService.newRegistry = func() *readers.Registry {
registry := readers.NewRegistry()
registry.Register(reader)
return registry
}

cfg := config.Config{DatabasePath: filepath.Join(tmp, "index.db")}
if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("initial sync: %v", err)
}
if reader.parseCalls != liveFiles {
Expand Down Expand Up @@ -219,7 +215,7 @@ func TestOriginParserSyncReplayIsBoundedAndConverges(t *testing.T) {
reader.replayUUIDLessRows = true
reader.parsePaths = nil
beforeReplay := reader.parseCalls
if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("first origin replay: %v", err)
}
if got := reader.parseCalls - beforeReplay; got != 200 {
Expand Down Expand Up @@ -270,7 +266,7 @@ func TestOriginParserSyncReplayIsBoundedAndConverges(t *testing.T) {

reader.parsePaths = nil
beforeReplay = reader.parseCalls
if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("second origin replay: %v", err)
}
if got := reader.parseCalls - beforeReplay; got != 1 {
Expand Down Expand Up @@ -301,7 +297,7 @@ func TestOriginParserSyncReplayIsBoundedAndConverges(t *testing.T) {
}

beforeReplay = reader.parseCalls
if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("converged origin replay: %v", err)
}
if got := reader.parseCalls - beforeReplay; got != 0 {
Expand All @@ -326,12 +322,8 @@ func TestOriginPerennialZeroMessageReplayPreservesHistoryAndConverges(t *testing
}

reader := &originReplayClaudeReader{}
oldActiveInputs, oldNewRegistry := maybeAutoSyncActiveInputs, maybeAutoSyncNewRegistry
t.Cleanup(func() {
maybeAutoSyncActiveInputs = oldActiveInputs
maybeAutoSyncNewRegistry = oldNewRegistry
})
maybeAutoSyncActiveInputs = func([]string) ([]input_config.InputDefinition, input_config.InputMode, error) {
syncService := newStartupSyncService()
syncService.activeInputs = func([]string) ([]input_config.InputDefinition, input_config.InputMode, error) {
return []input_config.InputDefinition{{
ID: "origin-zero-claude",
Source: "session",
Expand All @@ -343,14 +335,14 @@ func TestOriginPerennialZeroMessageReplayPreservesHistoryAndConverges(t *testing
Decode: input_config.DecodeConfig{Format: "claude"},
}}, input_config.ModeDeclarative, nil
}
maybeAutoSyncNewRegistry = func() *readers.Registry {
syncService.newRegistry = func() *readers.Registry {
registry := readers.NewRegistry()
registry.Register(reader)
return registry
}

cfg := config.Config{DatabasePath: filepath.Join(tmp, "index.db")}
if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("initial sync: %v", err)
}

Expand All @@ -376,7 +368,7 @@ func TestOriginPerennialZeroMessageReplayPreservesHistoryAndConverges(t *testing
}

reader.parseErr = fmt.Errorf("injected parse failure")
if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err == nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err == nil {
t.Fatal("parse failure unexpectedly succeeded")
}
db, err = storage.Open(cfg.DatabasePath)
Expand Down Expand Up @@ -405,7 +397,7 @@ func TestOriginPerennialZeroMessageReplayPreservesHistoryAndConverges(t *testing
reader.zeroRecords = true
reader.parsePaths = nil
beforeReplay := reader.parseCalls
if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("zero-message replay: %v", err)
}
if got := reader.parseCalls - beforeReplay; got != 1 {
Expand Down Expand Up @@ -440,7 +432,7 @@ func TestOriginPerennialZeroMessageReplayPreservesHistoryAndConverges(t *testing
}

beforeReplay = reader.parseCalls
if err := maybeAutoSync(&cfg, &bytes.Buffer{}); err != nil {
if err := syncService.sync(context.Background(), &cfg, &bytes.Buffer{}, startupPhaseTiming{}); err != nil {
t.Fatalf("converged zero-message replay: %v", err)
}
if got := reader.parseCalls - beforeReplay; got != 0 {
Expand Down
16 changes: 10 additions & 6 deletions cmd/backscroll/recover.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"fmt"
"io"
"strings"
Expand All @@ -10,10 +11,13 @@ import (
"github.com/spf13/cobra"
)

var recoverExecute = recovery.Execute
var recoverPostInstallSync = maybeAutoSyncContext

func newRecoverCmd(stdout, stderr io.Writer) *cobra.Command {
func newRecoverCmd(stdout, stderr io.Writer, syncService *startupSyncService, execute func(context.Context, recovery.Options) (recovery.Report, error)) *cobra.Command {
if syncService == nil {
syncService = newStartupSyncService()
}
if execute == nil {
execute = recovery.Execute
}
var from string
var dryRun bool
fromValue := singleUseStringValue{target: &from}
Expand All @@ -40,7 +44,7 @@ func newRecoverCmd(stdout, stderr io.Writer) *cobra.Command {
}
cfg = loaded
}
report, err := recoverExecute(cmd.Context(), recovery.Options{
report, err := execute(cmd.Context(), recovery.Options{
ActivePath: cfg.DatabasePath,
FromPath: from,
DryRun: dryRun,
Expand All @@ -52,7 +56,7 @@ func newRecoverCmd(stdout, stderr io.Writer) *cobra.Command {
return fmt.Errorf("recovery failed: %w", err)
}
if !dryRun {
if err := recoverPostInstallSync(cmd.Context(), cfg, stderr); err != nil {
if err := syncService.sync(cmd.Context(), cfg, stderr, startup.timing); err != nil {
installedPath := report.ActivePath
if installedPath == "" {
installedPath = cfg.DatabasePath
Expand Down
Loading
Loading