feat(cli): add transactional installation backups

This commit is contained in:
2026-08-16 00:55:22 +02:00
parent 3e0c864c85
commit 11fbf0a138
8 changed files with 2088 additions and 0 deletions
+75
View File
@@ -12,6 +12,7 @@ import (
"strings"
"testing"
"github.com/aritmolab/thothii/tools/tht/internal/backup"
"github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/tht/internal/doctor"
@@ -20,6 +21,80 @@ import (
"github.com/aritmolab/thothii/tools/tht/internal/testsupport"
)
func TestBackupCommandParsesSafeTransactionalOptions(t *testing.T) {
request, err := parseBackupArgs([]string{"--output", "backup.zip", "--include-secrets", "--yes", "--drain"})
if err != nil {
t.Fatal(err)
}
if request.Output != "backup.zip" || !request.IncludeSecrets || !request.Confirm || !request.Drain {
t.Fatalf("backup request = %#v", request)
}
for _, args := range [][]string{
{"--include-secrets"},
{"--yes"},
{"--output"},
{"--drain", "--drain"},
{"--unknown"},
} {
if _, err := parseBackupArgs(args); err == nil {
t.Errorf("parseBackupArgs(%v) succeeded", args)
}
}
}
func TestBackupCommandDispatchesWithoutDockerAndPrintsCustodyWarning(t *testing.T) {
installation := config.Installation{Path: "/tmp/thothii-installation.yaml"}
var received backup.CreateRequest
executor := func(_ context.Context, got config.Installation, request backup.CreateRequest) (backup.Result, error) {
if got.Path != installation.Path {
t.Fatalf("installation = %#v", got)
}
received = request
return backup.Result{Path: "/tmp/backup.zip", Warning: "archive contains external secret files; protect its custody"}, nil
}
var stdout, stderr bytes.Buffer
code := backupCommandWith(context.Background(), installation, []string{"--include-secrets", "--yes"}, executor, &stdout, &stderr)
if code != 0 {
t.Fatalf("backup exit = %d, stderr = %s", code, stderr.String())
}
if !received.IncludeSecrets || !received.Confirm || !strings.Contains(stdout.String(), "/tmp/backup.zip") || !strings.Contains(stderr.String(), "protect its custody") {
t.Fatalf("request=%#v stdout=%q stderr=%q", received, stdout.String(), stderr.String())
}
}
func TestBackupCommandMapsSafetyRefusalsToUsageExit(t *testing.T) {
executor := func(context.Context, config.Installation, backup.CreateRequest) (backup.Result, error) {
return backup.Result{}, backup.ErrActiveSessions
}
var stdout, stderr bytes.Buffer
if code := backupCommandWith(context.Background(), config.Installation{}, nil, executor, &stdout, &stderr); code != 2 {
t.Fatalf("backup safety refusal exit = %d, stderr = %q", code, stderr.String())
}
}
func TestPiMutationsUseTheSharedInstallationLifecycleLock(t *testing.T) {
for _, test := range []struct {
args []string
want bool
}{
{args: []string{"status"}, want: false},
{args: []string{"doctor"}, want: false},
{args: []string{"test"}, want: false},
{args: []string{"logs"}, want: false},
{args: []string{"configure"}, want: true},
{args: []string{"restart"}, want: true},
{args: []string{"update"}, want: true},
{args: []string{"rollback"}, want: true},
{args: []string{"maintenance", "recover"}, want: true},
{args: []string{"maintenance", "status"}, want: false},
} {
if got := piMutationRequiresLifecycleLock(test.args); got != test.want {
t.Errorf("piMutationRequiresLifecycleLock(%v) = %v, want %v", test.args, got, test.want)
}
}
}
func TestInstallationRunnerMapsProfileToSessionInventoryScope(t *testing.T) {
for _, test := range []struct {
profile string