476 lines
21 KiB
Go
476 lines
21 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
)
|
|
|
|
func TestCommittedRestorePlanUsesOnlyDeclaredObjects(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte(`{"segments":[1]}`),
|
|
})
|
|
fake.SeedObject(storage.FakeObject{Key: current.SessionPrefix + "artifacts/stale.md", Data: []byte("stale")})
|
|
fake.SeedObject(storage.FakeObject{Key: artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, "20260519T010204Z-e5f6a7b8"), "transcripts/other.json"), Data: []byte("other")})
|
|
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
got := actionRelPaths(plan.Actions)
|
|
want := []string{"manifest.json", "transcripts/full.json"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("action paths = %#v, want %#v", got, want)
|
|
}
|
|
for index := range want {
|
|
if got[index] != want[index] {
|
|
t.Fatalf("action paths = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreDoesNotRequirePolishRunDiagnostics(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte(`{"segments":[]}`),
|
|
})
|
|
diagnostic := filepath.Join(cfg.Pipeline.Workspace.Root, "runs", current.RunID, "polish", "outputs", "transcripts", "errors.yml")
|
|
current.Manifest.Stages["polish"] = &manifest.StageRecord{
|
|
Name: "polish", Status: manifest.StatusSucceeded,
|
|
Logs: []string{diagnostic},
|
|
}
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
for _, action := range plan.Actions {
|
|
if strings.Contains(action.LocalRelativePath, "errors.yml") {
|
|
t.Fatalf("diagnostic became a required restore target: %#v", action)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreDownloadsCarriedOutputFromDeclaredRun(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte("carried\n"),
|
|
})
|
|
declaration := ¤t.Commit.Artifacts[1]
|
|
oldKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, "20260512T010203Z-11223344"), "published/transcripts/full.json")
|
|
fake.SeedObject(storage.FakeObject{Key: oldKey, Data: []byte("carried\n")})
|
|
oldObject := fake.Objects[oldKey]
|
|
declaration.DestinationKey = oldKey
|
|
declaration.Generation = oldObject.ETag
|
|
if err := current.Commit.ValidateForSessionPrefix(current.SessionPrefix); err != nil {
|
|
t.Fatalf("carried commit validation: %v", err)
|
|
}
|
|
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
action := restoreActionForPath(t, plan, "transcripts/full.json")
|
|
if action.RemoteKey != oldKey {
|
|
t.Fatalf("restore remote key = %q, want exact carried key %q", action.RemoteKey, oldKey)
|
|
}
|
|
if _, err := executeRestorePlan(context.Background(), cfg, current, plan, nil, fake); err != nil {
|
|
t.Fatalf("executeRestorePlan() error = %v", err)
|
|
}
|
|
if got := fakeDownloadCount(fake, oldKey); got != 1 {
|
|
t.Fatalf("carried object downloads = %d, want one", got)
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreReusesVerifiedManifestCandidate(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", nil)
|
|
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
if _, err := executeRestorePlan(context.Background(), cfg, current, plan, nil, fake); err != nil {
|
|
t.Fatalf("executeRestorePlan() error = %v", err)
|
|
}
|
|
if got := fakeReadCount(fake, current.CurrentManifestKey); got != 1 {
|
|
t.Fatalf("manifest reads = %d, want one discovery read reused by restore", got)
|
|
}
|
|
if got := fakeDownloadCount(fake, current.CurrentManifestKey); got != 0 {
|
|
t.Fatalf("manifest downloads = %d, want no temporary download", got)
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreRejectsChangedGenerationForVerifiedManifestCandidate(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", nil)
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
fake.SeedObject(storage.FakeObject{Key: current.CurrentManifestKey, Data: []byte("changed manifest"), ETag: "changed-generation"})
|
|
|
|
_, err = executeRestorePlan(context.Background(), cfg, current, plan, nil, fake)
|
|
if err == nil || !strings.Contains(err.Error(), "generation mismatch") {
|
|
t.Fatalf("executeRestorePlan() error = %v, want generation mismatch", err)
|
|
}
|
|
if got := fakeReadCount(fake, current.CurrentManifestKey); got != 1 {
|
|
t.Fatalf("manifest reads = %d, want no second transfer for rejected candidate", got)
|
|
}
|
|
}
|
|
|
|
func TestCommittedStatusReportsOnlyDeclaredPublishedOutputs(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
cfg.Pipeline.Publish = &config.PublishConfig{Outputs: []config.PublishOutputRule{{
|
|
Source: "narratio.transcript.final_trimmed",
|
|
Dest: "transcripts/full.json",
|
|
}}}
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte("declared\n"),
|
|
})
|
|
current.Commit.Artifacts[1].Source = "narratio.transcript.final_trimmed"
|
|
fake.SeedObject(storage.FakeObject{Key: current.SessionPrefix + "transcripts/full.json", Data: []byte("mutable stale copy\n")})
|
|
|
|
availability := remotePublishedOutputAvailabilityForCurrent(context.Background(), cfg, fake, nil, current)
|
|
key := publishedOutputRemoteStateKey("narratio.transcript.final_trimmed", "transcripts/full.json")
|
|
if availability[key] != "remote=published" {
|
|
t.Fatalf("availability = %#v, want committed published output", availability)
|
|
}
|
|
}
|
|
|
|
func TestCommittedStatusReportsCarriedOutputOriginAndChecksExactKey(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
cfg.Pipeline.Publish = &config.PublishConfig{Outputs: []config.PublishOutputRule{{
|
|
Source: "narratio.transcript.final_trimmed",
|
|
Dest: "transcripts/full.json",
|
|
}}}
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte("carried\n"),
|
|
})
|
|
const originRun = "20260512T010203Z-11223344"
|
|
oldKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, originRun), "published/transcripts/original.json")
|
|
fake.SeedObject(storage.FakeObject{Key: oldKey, Data: []byte("carried\n")})
|
|
current.Commit.Artifacts[1].Source = "narratio.transcript.final_trimmed"
|
|
current.Commit.Artifacts[1].DestinationKey = oldKey
|
|
current.Manifest.Stages["publish"] = &manifest.StageRecord{
|
|
Name: "publish", Status: manifest.StatusSucceeded,
|
|
Metadata: map[string]any{"publish_output_outcomes": []any{map[string]any{
|
|
"source": "narratio.transcript.final_trimmed", "status": "carried_locked",
|
|
}}},
|
|
}
|
|
|
|
availability := remotePublishedOutputAvailabilityForCurrent(context.Background(), cfg, fake, nil, current)
|
|
key := publishedOutputRemoteStateKey("narratio.transcript.final_trimmed", "transcripts/full.json")
|
|
want := "outcome=carried_locked remote=published declared_dest=transcripts/original.json declared_key=" + oldKey + " origin_run=" + originRun
|
|
if availability[key] != want {
|
|
t.Fatalf("availability = %#v, want %q", availability, want)
|
|
}
|
|
if len(fake.ExistsKeys) == 0 || fake.ExistsKeys[len(fake.ExistsKeys)-1] != oldKey {
|
|
t.Fatalf("exists keys = %#v, want final exact declaration key %q", fake.ExistsKeys, oldKey)
|
|
}
|
|
var rendered bytes.Buffer
|
|
writePublishedOutputLine(&rendered, cfg.Pipeline.Publish.Outputs[0], nil, nil, availability)
|
|
for _, fragment := range []string{"dest=transcripts/full.json", "declared_dest=transcripts/original.json", "declared_key=" + oldKey, "origin_run=" + originRun} {
|
|
if !strings.Contains(rendered.String(), fragment) {
|
|
t.Fatalf("rendered output = %q, want %q", rendered.String(), fragment)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommittedPublishOutputOutcomesRecognizesAllStates(t *testing.T) {
|
|
statuses := []string{"published", "carried_locked", "locked_unpublished", "optional_missing", "unselected"}
|
|
items := make([]any, 0, len(statuses))
|
|
for _, status := range statuses {
|
|
items = append(items, map[string]any{"source": "source." + status, "status": status})
|
|
}
|
|
m := &manifest.Manifest{Stages: map[string]*manifest.StageRecord{
|
|
"publish": {Metadata: map[string]any{"publish_output_outcomes": items}},
|
|
}}
|
|
|
|
got := committedPublishOutputOutcomes(m)
|
|
for _, status := range statuses {
|
|
if got["source."+status] != status {
|
|
t.Fatalf("outcomes = %#v, missing %q", got, status)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreKeepsSelectedSnapshotWhenPointerChanges(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
first := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte("from first commit\n"),
|
|
})
|
|
plan, err := buildRestorePlan(context.Background(), cfg, first, fake, RestorePlanOptions{})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
_ = seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010204Z-e5f6a7b8", map[string][]byte{
|
|
"transcripts/full.json": []byte("from second commit\n"),
|
|
})
|
|
|
|
if _, err := executeRestorePlan(context.Background(), cfg, first, plan, nil, fake); err != nil {
|
|
t.Fatalf("executeRestorePlan() error = %v", err)
|
|
}
|
|
root := artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID)
|
|
mustReadEquals(t, filepath.Join(root, "transcripts", "full.json"), "from first commit\n")
|
|
restored, err := (&manifest.LocalStore{}).Load(context.Background(), filepath.Join(root, "manifest.json"))
|
|
if err != nil {
|
|
t.Fatalf("load restored manifest: %v", err)
|
|
}
|
|
if restored.RunID != first.RunID {
|
|
t.Fatalf("restored run id = %q, want %q", restored.RunID, first.RunID)
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreRejectsChangedDeclaredObjectBeforeManifestInstall(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte("committed bytes\n"),
|
|
})
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "published/transcripts/full.json")
|
|
fake.SeedObject(storage.FakeObject{Key: key, Data: []byte("changed bytes\n")})
|
|
|
|
if _, err := executeRestorePlan(context.Background(), cfg, current, plan, nil, fake); err == nil {
|
|
t.Fatal("executeRestorePlan() error = nil, want committed-object verification failure")
|
|
}
|
|
root := artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID)
|
|
if _, err := os.Stat(filepath.Join(root, "manifest.json")); !os.IsNotExist(err) {
|
|
t.Fatalf("manifest should not be installed after failed restore; stat err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreRejectsChangedDeclaredObjectGeneration(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte("committed bytes\n"),
|
|
})
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "published/transcripts/full.json")
|
|
fake.SeedObject(storage.FakeObject{Key: key, Data: []byte("committed bytes\n"), ETag: "replacement-generation"})
|
|
|
|
if _, err := executeRestorePlan(context.Background(), cfg, current, plan, nil, fake); err == nil {
|
|
t.Fatal("executeRestorePlan() error = nil, want generation verification failure")
|
|
}
|
|
root := artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID)
|
|
if _, err := os.Stat(filepath.Join(root, "manifest.json")); !os.IsNotExist(err) {
|
|
t.Fatalf("manifest should not be installed after failed restore; stat err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreRejectsChangedAudioBeforeInstall(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
generation string
|
|
wantError string
|
|
}{
|
|
{name: "content", data: []byte("tampered-audio"), wantError: "checksum mismatch"},
|
|
{name: "generation", data: []byte("original-audio"), generation: "replacement-generation", wantError: "generation mismatch"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
cfg.Pipeline.Spool.Root = filepath.Join(t.TempDir(), "spool")
|
|
cacheDisabled := false
|
|
cfg.Pipeline.Cache.S3Audio = &cacheDisabled
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"audio/alice.flac": []byte("original-audio"),
|
|
})
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{IncludeAudio: true})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "published/audio/alice.flac")
|
|
generation := test.generation
|
|
if generation == "" {
|
|
generation = fake.Objects[key].ETag
|
|
}
|
|
fake.SeedObject(storage.FakeObject{Key: key, Data: test.data, ETag: generation})
|
|
|
|
root := artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID)
|
|
destination := filepath.Join(root, "audio", "alice.flac")
|
|
if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll(audio directory) error = %v", err)
|
|
}
|
|
if err := os.WriteFile(destination, []byte("local-audio-remains"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile(existing audio) error = %v", err)
|
|
}
|
|
|
|
_, err = executeRestorePlan(context.Background(), cfg, current, plan, nil, fake)
|
|
if err == nil || !strings.Contains(err.Error(), test.wantError) {
|
|
t.Fatalf("executeRestorePlan() error = %v, want %q", err, test.wantError)
|
|
}
|
|
mustReadEquals(t, destination, "local-audio-remains")
|
|
if _, err := os.Stat(filepath.Join(root, "manifest.json")); !os.IsNotExist(err) {
|
|
t.Fatalf("manifest should not be installed after failed audio restore; stat err=%v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreRejectsMissingDeclaredObjectBeforeManifestInstall(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte("committed bytes\n"),
|
|
})
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
missingKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(current.SessionPrefix, current.RunID), "published/transcripts/full.json")
|
|
fake.DownloadHook = func(call storage.FakeDownloadCall) error {
|
|
if call.Key == missingKey {
|
|
return os.ErrNotExist
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if _, err := executeRestorePlan(context.Background(), cfg, current, plan, nil, fake); err == nil {
|
|
t.Fatal("executeRestorePlan() error = nil, want missing-object failure")
|
|
}
|
|
root := artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID)
|
|
if _, err := os.Stat(filepath.Join(root, "manifest.json")); !os.IsNotExist(err) {
|
|
t.Fatalf("manifest should not be installed after failed restore; stat err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCommittedRestoreForceRetainsDirectoryConflict(t *testing.T) {
|
|
cfg := restorePlanConfig(t)
|
|
fake := &storage.FakeBackend{}
|
|
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
|
"transcripts/full.json": []byte("committed bytes\n"),
|
|
})
|
|
root := artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID)
|
|
if err := os.MkdirAll(filepath.Join(root, "transcripts", "full.json"), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll() error = %v", err)
|
|
}
|
|
|
|
plan, err := buildRestorePlan(context.Background(), cfg, current, fake, RestorePlanOptions{Force: true})
|
|
if err != nil {
|
|
t.Fatalf("buildRestorePlan() error = %v", err)
|
|
}
|
|
if plan.ConflictCount != 1 {
|
|
t.Fatalf("ConflictCount = %d, want 1", plan.ConflictCount)
|
|
}
|
|
for _, action := range plan.Actions {
|
|
if action.LocalRelativePath == "transcripts/full.json" && action.ConflictKind != RestoreConflictDirectory {
|
|
t.Fatalf("ConflictKind = %q, want %q", action.ConflictKind, RestoreConflictDirectory)
|
|
}
|
|
}
|
|
}
|
|
|
|
func seedCommittedRestoreSnapshot(t *testing.T, cfg *config.Config, fake *storage.FakeBackend, runID string, outputs map[string][]byte) *RemoteCurrentState {
|
|
t.Helper()
|
|
sessionPrefix := artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.SessionID)
|
|
remoteManifest := manifest.New(cfg.Session.SessionID, time.Now().UTC())
|
|
remoteManifest.Campaign = cfg.Session.Campaign
|
|
remoteManifest.RunID = runID
|
|
manifestData, err := json.Marshal(remoteManifest)
|
|
if err != nil {
|
|
t.Fatalf("marshal manifest: %v", err)
|
|
}
|
|
manifestData = append(manifestData, '\n')
|
|
manifestKey := artifacts.S3RunSessionManifestKey(sessionPrefix, runID)
|
|
fake.SeedObject(storage.FakeObject{Key: manifestKey, Data: manifestData})
|
|
artifactsByKey := []artifacts.RemoteArtifact{remoteRestoreArtifact(fake, artifacts.RemoteArtifactTypeSessionManifest, "session.manifest", manifestKey)}
|
|
|
|
paths := make([]string, 0, len(outputs))
|
|
for relative := range outputs {
|
|
paths = append(paths, relative)
|
|
}
|
|
sort.Strings(paths)
|
|
for _, relative := range paths {
|
|
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, runID), "published/"+relative)
|
|
fake.SeedObject(storage.FakeObject{Key: key, Data: outputs[relative]})
|
|
sourceKey := strings.NewReplacer("/", "_", ".", "_", "-", "_").Replace(relative)
|
|
artifactsByKey = append(artifactsByKey, remoteRestoreArtifact(fake, artifacts.RemoteArtifactTypePublishedOutput, artifacts.ConfiguredArtifactSourceID(sourceKey), key))
|
|
}
|
|
commit := artifacts.RemoteCommitManifest{
|
|
FormatVersion: artifacts.RemoteCommitFormatVersion,
|
|
Campaign: cfg.Session.Campaign,
|
|
SessionID: cfg.Session.SessionID,
|
|
RunID: runID,
|
|
Artifacts: artifactsByKey,
|
|
}
|
|
commitData, err := artifacts.EncodeRemoteCommitManifest(commit)
|
|
if err != nil {
|
|
t.Fatalf("encode remote commit: %v", err)
|
|
}
|
|
commitKey := artifacts.S3RunCommitKey(sessionPrefix, runID)
|
|
fake.SeedObject(storage.FakeObject{Key: commitKey, Data: commitData})
|
|
commitObject := fake.Objects[commitKey]
|
|
pointerData, err := artifacts.EncodeCurrentCommitPointer(artifacts.CurrentCommitPointer{
|
|
FormatVersion: artifacts.RemoteCommitFormatVersion,
|
|
Campaign: cfg.Session.Campaign,
|
|
SessionID: cfg.Session.SessionID,
|
|
RunID: runID,
|
|
CommitKey: commitKey,
|
|
CommitSHA256: restoreCommitSHA256(commitData),
|
|
CommitSize: int64(len(commitData)),
|
|
CommitGeneration: commitObject.ETag,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encode current pointer: %v", err)
|
|
}
|
|
fake.SeedObject(storage.FakeObject{Key: artifacts.S3CurrentCommitPointerKey(sessionPrefix), Data: pointerData})
|
|
current, err := discoverRemoteCurrentState(context.Background(), cfg, fake)
|
|
if err != nil {
|
|
t.Fatalf("discoverRemoteCurrentState() error = %v", err)
|
|
}
|
|
return current
|
|
}
|
|
|
|
func remoteRestoreArtifact(fake *storage.FakeBackend, artifactType artifacts.RemoteArtifactType, source, key string) artifacts.RemoteArtifact {
|
|
object := fake.Objects[key]
|
|
return artifacts.RemoteArtifact{
|
|
Type: artifactType, Source: source, DestinationKey: key, SHA256: restoreCommitSHA256(object.Data), Size: int64(len(object.Data)), Generation: object.ETag,
|
|
}
|
|
}
|
|
|
|
func fakeReadCount(fake *storage.FakeBackend, key string) int {
|
|
count := 0
|
|
for _, call := range fake.Reads {
|
|
if call.Key == key {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func restoreCommitSHA256(data []byte) string {
|
|
sum := sha256.Sum256(data)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|