fix(deployer): wire pre-deploy backup into the unified dispatch path
auto_backup_before_deploy silently did nothing — MaybeBackupBeforeDeploy's only caller was the legacy executeDeploy pipeline, removed in the workload-first cutover. Reconnect it as maybeBackupBeforeDeploy(), invoked from DispatchPlugin after the source resolves and before it runs, so the setting fires for every source kind. Fail-open: a nil backuper, a settings-load error, or a backup failure skips the snapshot without blocking the deploy. Adds predeploy_backup_test.go asserting the wiring.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/alexei/tinyforge/internal/store"
|
||||
"github.com/alexei/tinyforge/internal/workload/plugin"
|
||||
)
|
||||
|
||||
// fakeBackuper records pre-deploy backup calls so the dispatch wiring can be
|
||||
// asserted. err (when set) simulates a backup failure.
|
||||
type fakeBackuper struct {
|
||||
count atomic.Int32
|
||||
lastType atomic.Value // string
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeBackuper) CreateBackup(backupType string) (store.Backup, error) {
|
||||
f.count.Add(1)
|
||||
f.lastType.Store(backupType)
|
||||
if f.err != nil {
|
||||
return store.Backup{}, f.err
|
||||
}
|
||||
return store.Backup{ID: "b1", Filename: "tinyforge-pre-deploy.db"}, nil
|
||||
}
|
||||
|
||||
func setAutoBackup(t *testing.T, d *Deployer, enabled bool) {
|
||||
t.Helper()
|
||||
s, err := d.store.GetSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("get settings: %v", err)
|
||||
}
|
||||
s.AutoBackupBeforeDeploy = enabled
|
||||
if err := d.store.UpdateSettings(s); err != nil {
|
||||
t.Fatalf("update settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Regression: the pre-deploy backup hook was orphaned after the cutover (no
|
||||
// caller on DispatchPlugin), making auto_backup_before_deploy a silent no-op.
|
||||
func TestDispatchPlugin_PreDeployBackup_FiresWhenEnabled(t *testing.T) {
|
||||
resetFake(t)
|
||||
d := newTestDeployer(t)
|
||||
b := &fakeBackuper{}
|
||||
d.SetPreDeployBackuper(b)
|
||||
setAutoBackup(t, d, true)
|
||||
|
||||
if err := d.DispatchPlugin(context.Background(), sampleWorkload(), plugin.DeploymentIntent{}); err != nil {
|
||||
t.Fatalf("dispatch: %v", err)
|
||||
}
|
||||
if got := b.count.Load(); got != 1 {
|
||||
t.Fatalf("CreateBackup called %d times, want 1", got)
|
||||
}
|
||||
if bt, _ := b.lastType.Load().(string); bt != "pre-deploy" {
|
||||
t.Fatalf("backup type = %q, want pre-deploy", bt)
|
||||
}
|
||||
if got := dispatchTestSource.deployCount.Load(); got != 1 {
|
||||
t.Fatalf("Deploy ran %d times, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchPlugin_PreDeployBackup_SkippedWhenDisabled(t *testing.T) {
|
||||
resetFake(t)
|
||||
d := newTestDeployer(t)
|
||||
b := &fakeBackuper{}
|
||||
d.SetPreDeployBackuper(b)
|
||||
setAutoBackup(t, d, false)
|
||||
|
||||
if err := d.DispatchPlugin(context.Background(), sampleWorkload(), plugin.DeploymentIntent{}); err != nil {
|
||||
t.Fatalf("dispatch: %v", err)
|
||||
}
|
||||
if got := b.count.Load(); got != 0 {
|
||||
t.Fatalf("CreateBackup called %d times, want 0 (setting off)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchPlugin_PreDeployBackup_NilBackuperNoPanic(t *testing.T) {
|
||||
resetFake(t)
|
||||
d := newTestDeployer(t)
|
||||
setAutoBackup(t, d, true) // enabled, but no backuper wired
|
||||
|
||||
if err := d.DispatchPlugin(context.Background(), sampleWorkload(), plugin.DeploymentIntent{}); err != nil {
|
||||
t.Fatalf("dispatch must not panic/fail with a nil backuper: %v", err)
|
||||
}
|
||||
if got := dispatchTestSource.deployCount.Load(); got != 1 {
|
||||
t.Fatalf("Deploy ran %d times, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchPlugin_PreDeployBackup_FailOpen(t *testing.T) {
|
||||
resetFake(t)
|
||||
d := newTestDeployer(t)
|
||||
b := &fakeBackuper{err: errors.New("disk full")}
|
||||
d.SetPreDeployBackuper(b)
|
||||
setAutoBackup(t, d, true)
|
||||
|
||||
// A failed backup is logged but must NOT block the deploy.
|
||||
if err := d.DispatchPlugin(context.Background(), sampleWorkload(), plugin.DeploymentIntent{}); err != nil {
|
||||
t.Fatalf("deploy must succeed when backup fails (fail-open): %v", err)
|
||||
}
|
||||
if got := dispatchTestSource.deployCount.Load(); got != 1 {
|
||||
t.Fatalf("Deploy ran %d times, want 1 (despite backup failure)", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user