package backup import ( "archive/tar" "archive/zip" "context" "crypto/sha256" "encoding/hex" "errors" "io" "os" "path/filepath" "runtime" "strings" "testing" "time" "github.com/aritmolab/thothii/tools/tht/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/safeio" ) func TestPreflightReturnsValidatedMetadataAndCallsAllTargetChecksWithoutExtracting(t *testing.T) { installation := preflightTestInstallation(t) archive := filepath.Join(t.TempDir(), "valid.zip") writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}}, }) checks := make([]string, 0, 3) result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, PreflightDependencies{ FreeBytes: func(string) (uint64, error) { return 1024, nil }, CheckOwnershipPermissions: func(context.Context, config.Installation, Manifest) error { checks = append(checks, "ownership") return nil }, CheckVolumeMapping: func(context.Context, config.Installation, Manifest) error { checks = append(checks, "volumes") return nil }, CheckImageConfigCompatibility: func(context.Context, config.Installation, Manifest) error { checks = append(checks, "images") return nil }, }) if err != nil { t.Fatal(err) } defer result.CloseArchive() if result.Manifest.InstallationID != "local-dev" || result.Manifest.Entries[0].Path != "configuration/operator.env" { t.Fatalf("validated metadata = %#v", result) } if result.ArchivePath != archive || result.RequiredBytes != 4 || len(result.Entries) != 1 { t.Fatalf("archive metadata = %#v", result) } if strings.Join(checks, ",") != "ownership,volumes,images" { t.Fatalf("target checks = %v", checks) } if _, err := os.Stat(filepath.Join(installation.ProjectDirectory, "configuration", "operator.env")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("preflight extracted into a final target: %v", err) } } func TestPreflightStagesOnTheAccountedInstallationFilesystemInsteadOfTMPDIR(t *testing.T) { installation := preflightTestInstallation(t) if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil { t.Fatal(err) } archive := filepath.Join(t.TempDir(), "valid.zip") writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}}, }) blockedTMPDIR := filepath.Join(t.TempDir(), "not-a-directory") if err := os.WriteFile(blockedTMPDIR, []byte("blocked"), 0o600); err != nil { t.Fatal(err) } t.Setenv("TMPDIR", blockedTMPDIR) var capacityTargets []string dependencies := permissivePreflightDependencies() dependencies.FreeBytes = func(target string) (uint64, error) { capacityTargets = append(capacityTargets, target) return 1 << 30, nil } result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, dependencies) if err != nil { t.Fatal(err) } defer result.CloseArchive() staged, err := result.StageArchive(context.Background()) if err != nil { t.Fatal(err) } defer staged.Close() if len(capacityTargets) == 0 || capacityTargets[0] != installation.ControlDirectory() { t.Fatalf("free-space targets = %q, want installation control directory %q", capacityTargets, installation.ControlDirectory()) } stagingRoot := filepath.Join(installation.ControlDirectory(), "restore-staging") if filepath.Dir(staged.path) != result.stagingRoot { t.Fatalf("staging archive directory = %q, want direct child directory %q", filepath.Dir(staged.path), stagingRoot) } } func TestPreflightRejectsAdversarialArchiveEntriesAndManifestIdentity(t *testing.T) { installation := preflightTestInstallation(t) tests := []struct { name string make func(string) }{ { name: "traversal", make: func(path string) { writePreflightArchive(t, path, preflightArchiveSpec{rawEntries: []preflightRawArchiveEntry{{path: "../outside", body: []byte("x")}}}) }, }, { name: "absolute", make: func(path string) { writePreflightArchive(t, path, preflightArchiveSpec{rawEntries: []preflightRawArchiveEntry{{path: "/outside", body: []byte("x")}}}) }, }, { name: "windows absolute", make: func(path string) { writePreflightArchive(t, path, preflightArchiveSpec{rawEntries: []preflightRawArchiveEntry{{path: `C:\outside`, body: []byte("x")}}}) }, }, { name: "normalized traversal", make: func(path string) { writePreflightArchive(t, path, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "target", body: []byte("x")}}, rawManifest: []byte(`{"schema_version":1,"installation_id":"local-dev","created_at":"2026-08-16T10:00:00Z","source_revision":"` + testRevision + `","includes_secrets":false,"compose_project":"thothii-test","images":[],"volumes":[],"entries":[{"path":"foo/../target","kind":"file","owner":"installation","sha256":"` + digestForPreflight([]byte("x")) + `","size":1,"archived":true}]}`), }) }, }, { name: "symlink", make: func(path string) { writePreflightArchive(t, path, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "link", body: []byte("target"), symlink: true}}, }) }, }, { name: "duplicate", make: func(path string) { writePreflightArchive(t, path, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "file", body: []byte("one")}}, rawEntries: []preflightRawArchiveEntry{{path: "file", body: []byte("two")}}, }) }, }, { name: "checksum mismatch", make: func(path string) { writePreflightArchive(t, path, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "file", body: []byte("actual"), checksum: "sha256:" + strings.Repeat("0", 64)}}, }) }, }, { name: "unknown schema", make: func(path string) { writePreflightArchive(t, path, preflightArchiveSpec{schemaVersion: CurrentSchemaVersion + 1}) }, }, { name: "wrong installation", make: func(path string) { writePreflightArchive(t, path, preflightArchiveSpec{installationID: "another-installation"}) }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { archive := filepath.Join(t.TempDir(), test.name+".zip") test.make(archive) _, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()) if err == nil { t.Fatal("Preflight accepted adversarial archive") } }) } } func TestPreflightRequiresExplicitProtectionForExternalSecretPayloadsWithoutLeakingIt(t *testing.T) { installation := preflightTestInstallation(t) secret := []byte("never-print-this-secret") archive := filepath.Join(t.TempDir(), "secrets.zip") writePreflightArchive(t, archive, preflightArchiveSpec{ includeSecrets: true, entries: []preflightArchiveEntry{{path: "external-secrets/000", body: secret, kind: EntryExternalSecret, sensitive: true}}, }) _, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: false}, permissivePreflightDependencies()) if err == nil || strings.Contains(err.Error(), string(secret)) || !strings.Contains(err.Error(), "confirmation") { t.Fatalf("secret policy error = %v", err) } result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true, AllowExternalSecrets: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } defer result.CloseArchive() } func TestPreflightRejectsInsufficientDiskAndEachTargetCompatibilityFailure(t *testing.T) { installation := preflightTestInstallation(t) archive := filepath.Join(t.TempDir(), "checked.zip") writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "large", body: []byte("123456789")}}}) tests := []struct { name string deps func(error) PreflightDependencies want string }{ {name: "disk", deps: func(error) PreflightDependencies { deps := permissivePreflightDependencies() deps.FreeBytes = func(string) (uint64, error) { return 1, nil } return deps }, want: "free disk"}, {name: "ownership", deps: func(want error) PreflightDependencies { deps := permissivePreflightDependencies() deps.CheckOwnershipPermissions = func(context.Context, config.Installation, Manifest) error { return want } return deps }, want: "ownership"}, {name: "volume mapping", deps: func(want error) PreflightDependencies { deps := permissivePreflightDependencies() deps.CheckVolumeMapping = func(context.Context, config.Installation, Manifest) error { return want } return deps }, want: "volume mapping"}, {name: "image config", deps: func(want error) PreflightDependencies { deps := permissivePreflightDependencies() deps.CheckImageConfigCompatibility = func(context.Context, config.Installation, Manifest) error { return want } return deps }, want: "image config"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { want := errors.New(test.want + " rejected") _, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, test.deps(want)) if err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("Preflight() error = %v, want %q", err, test.want) } }) } } func TestPreflightRejectsTraversalAndSymlinkInsideVolumeTar(t *testing.T) { installation := preflightTestInstallation(t) for _, test := range []struct { name string header tar.Header }{ {name: "traversal", header: tar.Header{Name: "../outside", Mode: 0o600, Size: 1, Typeflag: tar.TypeReg}}, {name: "symlink", header: tar.Header{Name: "link", Mode: 0o777, Typeflag: tar.TypeSymlink, Linkname: "../../outside"}}, } { t.Run(test.name, func(t *testing.T) { var payload strings.Builder writer := tar.NewWriter(&stringWriter{value: &payload}) if err := writer.WriteHeader(&test.header); err != nil { t.Fatal(err) } if test.header.Size > 0 { if _, err := writer.Write([]byte("x")); err != nil { t.Fatal(err) } } if err := writer.Close(); err != nil { t.Fatal(err) } archive := filepath.Join(t.TempDir(), test.name+".zip") writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "volumes/sessions.tar", body: []byte(payload.String()), kind: EntryVolume}}}) if _, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()); err == nil { t.Fatal("Preflight accepted unsafe TAR member") } }) } } func TestPreflightAcceptsCanonicalTarRootDirectoryAndRelativeMembers(t *testing.T) { installation := preflightTestInstallation(t) var payload strings.Builder writer := tar.NewWriter(&stringWriter{value: &payload}) for _, header := range []tar.Header{ {Name: "./", Mode: 0o755, Typeflag: tar.TypeDir}, {Name: "./payload", Mode: 0o600, Size: 1, Typeflag: tar.TypeReg}, } { if err := writer.WriteHeader(&header); err != nil { t.Fatal(err) } if header.Size > 0 { if _, err := writer.Write([]byte("x")); err != nil { t.Fatal(err) } } } if err := writer.Close(); err != nil { t.Fatal(err) } archive := filepath.Join(t.TempDir(), "canonical-volume.zip") writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{ path: "volumes/sessions.tar", body: []byte(payload.String()), kind: EntryVolume, }}}) result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } result.CloseArchive() } func TestPreflightRejectsNonDirectoryTarRootMarker(t *testing.T) { installation := preflightTestInstallation(t) var payload strings.Builder writer := tar.NewWriter(&stringWriter{value: &payload}) header := tar.Header{Name: ".", Mode: 0o600, Size: 1, Typeflag: tar.TypeReg} if err := writer.WriteHeader(&header); err != nil { t.Fatal(err) } if _, err := writer.Write([]byte("x")); err != nil { t.Fatal(err) } if err := writer.Close(); err != nil { t.Fatal(err) } archive := filepath.Join(t.TempDir(), "unsafe-root-volume.zip") writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{ path: "volumes/sessions.tar", body: []byte(payload.String()), kind: EntryVolume, }}}) if _, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()); err == nil { t.Fatal("Preflight accepted a non-directory TAR root marker") } } func TestPreflightRejectsArchivesThatExceedConfiguredProcessingLimits(t *testing.T) { installation := preflightTestInstallation(t) tests := []struct { name string spec preflightArchiveSpec limits PreflightLimits wantErr string }{ { name: "member count", spec: preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "one", body: []byte("one")}}}, limits: PreflightLimits{MaxMembers: 1, MaxUncompressedBytes: 1 << 20, MaxCompressionRatio: 100}, wantErr: "member limit", }, { name: "uncompressed bytes", spec: preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "large", body: []byte("123456789")}}}, limits: PreflightLimits{MaxMembers: 10, MaxUncompressedBytes: 1, MaxCompressionRatio: 100}, wantErr: "uncompressed-size limit", }, { name: "compression ratio", spec: preflightArchiveSpec{entries: []preflightArchiveEntry{{ path: "compressed", body: []byte(strings.Repeat("A", 4096)), method: zip.Deflate, }}}, limits: PreflightLimits{MaxMembers: 10, MaxUncompressedBytes: 1 << 20, MaxCompressionRatio: 2}, wantErr: "compression-ratio limit", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { archive := filepath.Join(t.TempDir(), "limited.zip") writePreflightArchive(t, archive, test.spec) _, err := Preflight(context.Background(), installation, PreflightRequest{ Archive: archive, Confirm: true, Limits: test.limits, }, permissivePreflightDependencies()) if err == nil || !strings.Contains(err.Error(), test.wantErr) { t.Fatalf("Preflight() error = %v, want %q", err, test.wantErr) } }) } } func TestPreflightRevalidationRefusesAnArchivePathThatWasReplaced(t *testing.T) { installation := preflightTestInstallation(t) archive := filepath.Join(t.TempDir(), "checked.zip") writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("validated")}}, }) result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } defer result.CloseArchive() replacement := filepath.Join(t.TempDir(), "replacement.zip") writePreflightArchive(t, replacement, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("unchecked")}}, }) if runtime.GOOS == "windows" { // The retained validated handle denies replacement on Windows; that is the // stronger invariant, so revalidation must still succeed for the unchanged path. if err := os.Rename(replacement, archive); err == nil { t.Fatal("Windows replaced a retained archive path") } if _, err := result.RevalidateArchive(); err != nil { t.Fatalf("RevalidateArchive() on retained Windows archive = %v", err) } return } if err := os.Rename(replacement, archive); err != nil { t.Fatal(err) } if _, err := result.RevalidateArchive(); err == nil || !strings.Contains(err.Error(), "changed") { t.Fatalf("RevalidateArchive() error = %v, want replaced path refusal", err) } } func TestPreflightStagesArchiveIntoImmutablePrivateBytes(t *testing.T) { installation := preflightTestInstallation(t) if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil { t.Fatal(err) } archive := filepath.Join(t.TempDir(), "checked.zip") writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("before")}}, }) result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } defer result.CloseArchive() staged, err := result.StageArchive(context.Background()) if err != nil { t.Fatal(err) } defer staged.Close() writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("after!")}}, }) reader, err := zip.NewReader(staged.file, result.ArchiveSize) if err != nil { t.Fatal(err) } if len(reader.File) < 2 { t.Fatalf("staged archive members = %d, want manifest and payload", len(reader.File)) } var payload *zip.File for _, member := range reader.File { if member.Name == "configuration/operator.env" { payload = member break } } if payload == nil { t.Fatal("staged archive is missing the configured payload") } stream, err := payload.Open() if err != nil { t.Fatal(err) } defer stream.Close() body, err := io.ReadAll(stream) if err != nil { t.Fatal(err) } if string(body) != "before" { t.Fatalf("staged payload = %q, want preflighted bytes", body) } } func TestStageArchiveCleansPrivateFileAfterStreamingFailure(t *testing.T) { installation := preflightTestInstallation(t) if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil { t.Fatal(err) } archive := filepath.Join(t.TempDir(), "checked.zip") writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("before")}}, }) result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } defer result.CloseArchive() result.freeBytes = func(string) (uint64, error) { result.archive.digest = "after-preflight-mismatch" return 1024, nil } if _, err := result.StageArchive(context.Background()); err == nil || !strings.Contains(err.Error(), "changed") { t.Fatalf("StageArchive() error = %v, want streaming size refusal", err) } entries, err := os.ReadDir(result.stagingRoot) if err != nil { t.Fatal(err) } if len(entries) != 0 { t.Fatalf("private staging leftovers = %v, want none", entries) } } func TestPreflightStagingRejectsInPlaceArchiveHashMutation(t *testing.T) { installation := preflightTestInstallation(t) archive := filepath.Join(t.TempDir(), "checked.zip") writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("before")}}, }) result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()) if err != nil { t.Fatal(err) } defer result.CloseArchive() writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("after!")}}, }) if _, err := result.StageArchive(context.Background()); err == nil || !strings.Contains(err.Error(), "changed") { t.Fatalf("StageArchive() error = %v, want changed archive refusal", err) } } func TestPreflightRejectsArchiveEntryWithModeDifferentFromManifest(t *testing.T) { installation := preflightTestInstallation(t) archive := filepath.Join(t.TempDir(), "mode-mismatch.zip") manifestMode := uint32(0o600) writePreflightArchive(t, archive, preflightArchiveSpec{ entries: []preflightArchiveEntry{{ path: "configuration/operator.env", body: []byte("safe"), mode: 0o644, manifestMode: &manifestMode, }}, }) _, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()) if err == nil || !strings.Contains(err.Error(), "mode mismatch") { t.Fatalf("Preflight() error = %v, want mode mismatch", err) } } type preflightArchiveSpec struct { installationID string schemaVersion int includeSecrets bool entries []preflightArchiveEntry rawEntries []preflightRawArchiveEntry rawManifest []byte volumes []VolumeMetadata } type preflightArchiveEntry struct { path string body []byte checksum string kind string sensitive bool symlink bool mode os.FileMode manifestMode *uint32 method uint16 owner string logicalName string sourcePath string } type preflightRawArchiveEntry struct { path string body []byte } func preflightTestInstallation(t *testing.T) config.Installation { t.Helper() root, err := filepath.EvalSymlinks(t.TempDir()) if err != nil { t.Fatal(err) } installation := config.Installation{ Path: filepath.Join(root, "deploy", "local-dev", "thothii-installation.yaml"), ProjectDirectory: root, } controlParent := filepath.Dir(installation.ControlDirectory()) for _, directory := range []string{controlParent, installation.ControlDirectory()} { if err := os.MkdirAll(directory, 0o700); err != nil { t.Fatal(err) } if err := safeio.ProtectPrivateDirectory(directory); err != nil { t.Fatalf("protect preflight fixture directory %q: %v", directory, err) } } return installation } func permissivePreflightDependencies() PreflightDependencies { return PreflightDependencies{ FreeBytes: func(string) (uint64, error) { return 1 << 30, nil }, CheckOwnershipPermissions: func(context.Context, config.Installation, Manifest) error { return nil }, CheckVolumeMapping: func(context.Context, config.Installation, Manifest) error { return nil }, CheckImageConfigCompatibility: func(context.Context, config.Installation, Manifest) error { return nil }, } } func writePreflightArchive(t *testing.T, archivePath string, spec preflightArchiveSpec) { t.Helper() if spec.installationID == "" { spec.installationID = "local-dev" } if spec.schemaVersion == 0 { spec.schemaVersion = CurrentSchemaVersion } manifest := Manifest{ SchemaVersion: spec.schemaVersion, InstallationID: spec.installationID, CreatedAt: time.Date(2026, 8, 16, 10, 0, 0, 0, time.UTC), SourceRevision: testRevision, IncludesSecrets: spec.includeSecrets, ComposeProject: "thothii-test", Volumes: append([]VolumeMetadata(nil), spec.volumes...), } for _, entry := range spec.entries { checksum := entry.checksum if checksum == "" { checksum = digestForPreflight(entry.body) } kind := entry.kind if kind == "" { kind = EntryFile } sourcePath := entry.sourcePath owner := entry.owner if owner == "" { owner = "installation" } if kind == EntryExternalSecret { if sourcePath == "" { sourcePath = "/protected/secret" } if entry.owner == "" { owner = "external-secret" } } mode := uint32(entry.mode.Perm()) if mode == 0 { mode = 0o600 } if entry.manifestMode != nil { mode = *entry.manifestMode } manifest.Entries = append(manifest.Entries, Entry{Path: entry.path, Kind: kind, Owner: owner, LogicalName: entry.logicalName, SourcePath: sourcePath, SHA256: checksum, Size: int64(len(entry.body)), Mode: mode, Archived: true, Sensitive: entry.sensitive}) } manifestBytes, err := manifest.JSON() if err != nil { if spec.schemaVersion != CurrentSchemaVersion || spec.installationID != "local-dev" { manifestBytes = rawPreflightManifest(spec) } else { t.Fatal(err) } } if spec.rawManifest != nil { manifestBytes = spec.rawManifest } file, err := os.Create(archivePath) if err != nil { t.Fatal(err) } defer file.Close() writer := zip.NewWriter(file) for _, entry := range spec.entries { method := entry.method if method == 0 { method = zip.Store } header := &zip.FileHeader{Name: entry.path, Method: method} mode := entry.mode if mode == 0 { mode = 0o600 } header.SetMode(mode) if entry.symlink { header.SetMode(os.ModeSymlink | 0o777) } created, err := writer.CreateHeader(header) if err != nil { t.Fatal(err) } if _, err := created.Write(entry.body); err != nil { t.Fatal(err) } } for _, entry := range spec.rawEntries { created, err := writer.CreateHeader(&zip.FileHeader{Name: entry.path, Method: zip.Store}) if err != nil { t.Fatal(err) } if _, err := created.Write(entry.body); err != nil { t.Fatal(err) } } manifestHeader := &zip.FileHeader{Name: ManifestPath, Method: zip.Store} manifestHeader.SetMode(0o600) created, err := writer.CreateHeader(manifestHeader) if err != nil { t.Fatal(err) } if _, err := created.Write(manifestBytes); err != nil { t.Fatal(err) } if err := writer.Close(); err != nil { t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } } func rawPreflightManifest(spec preflightArchiveSpec) []byte { return []byte(`{"schema_version":999,"installation_id":"` + spec.installationID + `","created_at":"2026-08-16T10:00:00Z","source_revision":"` + testRevision + `","includes_secrets":false,"compose_project":"thothii-test","images":[],"volumes":[],"entries":[]}`) } func digestForPreflight(value []byte) string { digest := sha256.Sum256(value) return "sha256:" + hex.EncodeToString(digest[:]) } type stringWriter struct { value *strings.Builder } func (writer *stringWriter) Write(value []byte) (int, error) { return writer.value.Write(value) }