Require committed remote current state
This commit is contained in:
@@ -1078,7 +1078,8 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
|
||||
`)
|
||||
fake := &storage.FakeBackend{}
|
||||
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
|
||||
manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix)
|
||||
manifestKey := artifacts.S3RunSessionManifestKey(sessionPrefix, "20260519T010203Z-a1b2c3d4")
|
||||
runIDKey := artifacts.S3CurrentCommitPointerKey(sessionPrefix)
|
||||
trimmedKey := artifacts.S3PublishedOutputKey(sessionPrefix, "transcripts/final.trimmed.json")
|
||||
fullKey := artifacts.S3PublishedOutputKey(sessionPrefix, "transcripts/full.json")
|
||||
lockKey := artifacts.S3SessionLocksKey(sessionPrefix)
|
||||
@@ -1174,7 +1175,7 @@ func TestExecuteStatusReportsMissingRemoteCurrentStateWithoutFailing(t *testing.
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Remote publish: missing or unavailable: remote current run pointer missing") {
|
||||
if !strings.Contains(stdout.String(), "Remote publish: missing or unavailable: remote current commit pointer missing") {
|
||||
t.Fatalf("stdout = %q, want missing remote current-state line", stdout.String())
|
||||
}
|
||||
}
|
||||
@@ -1199,7 +1200,7 @@ func TestExecuteStatusReportsPreviousStateReadinessWithoutFailing(t *testing.T)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Previous-session artifacts: unavailable: remote current run pointer missing") {
|
||||
if !strings.Contains(stdout.String(), "Previous-session artifacts: unavailable: remote current commit pointer missing") {
|
||||
t.Fatalf("stdout = %q, want previous readiness unavailable line", stdout.String())
|
||||
}
|
||||
}
|
||||
@@ -1257,8 +1258,8 @@ func TestExecuteSessionValidateReportsPreviousStateFindingAndReturnsFindingError
|
||||
if !strings.Contains(stdout.String(), "ERROR previous") {
|
||||
t.Fatalf("stdout = %q, want previous finding error", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "remote current run pointer missing") {
|
||||
t.Fatalf("stdout = %q, want missing run pointer finding", stdout.String())
|
||||
if !strings.Contains(stdout.String(), "remote current commit pointer missing") {
|
||||
t.Fatalf("stdout = %q, want missing commit pointer finding", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "validation error(s)") {
|
||||
t.Fatalf("stderr = %q, want finding error summary", stderr.String())
|
||||
|
||||
@@ -171,16 +171,11 @@ func inspectPreviousArtifactReadiness(
|
||||
paths := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root).SessionPathsFor(cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
plan, err := previouscache.Resolve(ctx, cfg, paths, requirements, store)
|
||||
if err != nil {
|
||||
var pointerMissing *artifacts.CurrentRunPointerMissingError
|
||||
var manifestMissing *artifacts.CurrentManifestMissingError
|
||||
var pointerMissing *artifacts.CurrentCommitPointerMissingError
|
||||
if errors.As(err, &pointerMissing) {
|
||||
out.Err = fmt.Errorf("remote %w", pointerMissing)
|
||||
return out
|
||||
}
|
||||
if errors.As(err, &manifestMissing) {
|
||||
out.Err = fmt.Errorf("remote %w", manifestMissing)
|
||||
return out
|
||||
}
|
||||
out.Err = fmt.Errorf("remote %w", err)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ func seedCommittedRestoreSnapshot(t *testing.T, cfg *config.Config, fake *storag
|
||||
for _, relative := range paths {
|
||||
key := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, runID), relative)
|
||||
fake.SeedObject(storage.FakeObject{Key: key, Data: outputs[relative]})
|
||||
artifactsByKey = append(artifactsByKey, remoteRestoreArtifact(fake, artifacts.RemoteArtifactTypePublishedOutput, "narratio.test", key))
|
||||
artifactsByKey = append(artifactsByKey, remoteRestoreArtifact(fake, artifacts.RemoteArtifactTypePublishedOutput, "narratio.test."+strings.ReplaceAll(relative, "/", "."), key))
|
||||
}
|
||||
commit := artifacts.RemoteCommitManifest{
|
||||
FormatVersion: artifacts.RemoteCommitFormatVersion,
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
type RemoteCurrentState struct {
|
||||
Bucket string
|
||||
SessionPrefix string
|
||||
CurrentRunIDKey string
|
||||
CurrentManifestKey string
|
||||
RunID string
|
||||
SessionID string
|
||||
@@ -55,7 +54,6 @@ func discoverRemoteCurrentState(ctx context.Context, cfg *config.Config, store s
|
||||
return &RemoteCurrentState{
|
||||
Bucket: bucket,
|
||||
SessionPrefix: sessionPrefix,
|
||||
CurrentRunIDKey: current.CurrentRunIDKey,
|
||||
CurrentManifestKey: current.CurrentManifestKey,
|
||||
RunID: current.RunID,
|
||||
SessionID: strings.TrimSpace(current.Manifest.SessionID),
|
||||
|
||||
@@ -3,244 +3,27 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestDiscoverRemoteCurrentStateSuccess(t *testing.T) {
|
||||
cfg := restoreDiscoveryConfig()
|
||||
store := &storage.FakeBackend{}
|
||||
sessionPrefix, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
||||
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)})
|
||||
|
||||
state, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
||||
if err != nil {
|
||||
t.Fatalf("discoverRemoteCurrentState() error = %v", err)
|
||||
func TestDiscoverRemoteCurrentStateRequiresCommittedPointer(t *testing.T) {
|
||||
cfg := &config.Config{Pipeline: &config.PipelineConfig{Storage: config.StorageConfig{S3: &config.StorageS3Config{Bucket: "test-bucket", RootPrefix: "dnd"}}}, Session: &config.SessionConfig{Campaign: "sample-campaign", SessionID: "2026-05-03"}}
|
||||
_, err := discoverRemoteCurrentState(context.Background(), cfg, &storage.FakeBackend{})
|
||||
if err == nil {
|
||||
t.Fatal("discoverRemoteCurrentState() error = nil, want missing committed pointer")
|
||||
}
|
||||
if state.RunID != "20260519T010203Z-a1b2c3d4" {
|
||||
t.Fatalf("run id = %q, want 20260519T010203Z-a1b2c3d4", state.RunID)
|
||||
}
|
||||
if state.SessionPrefix != sessionPrefix {
|
||||
t.Fatalf("session prefix = %q, want %q", state.SessionPrefix, sessionPrefix)
|
||||
}
|
||||
if state.CurrentRunIDKey != runIDKey {
|
||||
t.Fatalf("current run id key = %q, want %q", state.CurrentRunIDKey, runIDKey)
|
||||
}
|
||||
if state.CurrentManifestKey != manifestKey {
|
||||
t.Fatalf("current manifest key = %q, want %q", state.CurrentManifestKey, manifestKey)
|
||||
}
|
||||
if state.Manifest == nil {
|
||||
t.Fatal("manifest is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRemoteCurrentStateMissingRunPointerFails(t *testing.T) {
|
||||
cfg := restoreDiscoveryConfig()
|
||||
store := &storage.FakeBackend{}
|
||||
|
||||
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
||||
if err == nil || !strings.Contains(err.Error(), "remote current run pointer missing") {
|
||||
t.Fatalf("error = %v, want missing run pointer failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRemoteCurrentStateEmptyRunPointerFails(t *testing.T) {
|
||||
cfg := restoreDiscoveryConfig()
|
||||
store := &storage.FakeBackend{}
|
||||
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
||||
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte(" \n\t")})
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)})
|
||||
|
||||
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
||||
if err == nil || !strings.Contains(err.Error(), "is empty") {
|
||||
t.Fatalf("error = %v, want empty run pointer failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRemoteCurrentStateMissingManifestFails(t *testing.T) {
|
||||
cfg := restoreDiscoveryConfig()
|
||||
store := &storage.FakeBackend{}
|
||||
_, _, runIDKey := restoreDiscoveryKeys(cfg)
|
||||
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
|
||||
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
||||
if err == nil || !strings.Contains(err.Error(), "remote current manifest missing") {
|
||||
t.Fatalf("error = %v, want missing manifest failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRemoteCurrentStateInvalidManifestFails(t *testing.T) {
|
||||
cfg := restoreDiscoveryConfig()
|
||||
store := &storage.FakeBackend{}
|
||||
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
||||
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: []byte("{invalid json")})
|
||||
|
||||
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
||||
if err == nil || !strings.Contains(err.Error(), "remote current manifest decode failed") {
|
||||
t.Fatalf("error = %v, want manifest decode failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRemoteCurrentStateSessionMismatchFails(t *testing.T) {
|
||||
cfg := restoreDiscoveryConfig()
|
||||
store := &storage.FakeBackend{}
|
||||
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
||||
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "wrong-session", cfg.Session.Campaign)})
|
||||
|
||||
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
||||
if err == nil || !strings.Contains(err.Error(), "does not match expected session_id") {
|
||||
t.Fatalf("error = %v, want session mismatch failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRemoteCurrentStateCampaignMismatchFails(t *testing.T) {
|
||||
cfg := restoreDiscoveryConfig()
|
||||
store := &storage.FakeBackend{}
|
||||
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
||||
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, "wrong-campaign")})
|
||||
|
||||
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
||||
if err == nil || !strings.Contains(err.Error(), "does not match expected campaign") {
|
||||
t.Fatalf("error = %v, want campaign mismatch failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRemoteCurrentStateEmptyCampaignFails(t *testing.T) {
|
||||
cfg := restoreDiscoveryConfig()
|
||||
store := &storage.FakeBackend{}
|
||||
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
||||
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, "")})
|
||||
|
||||
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
||||
if err == nil || !strings.Contains(err.Error(), "campaign is required") {
|
||||
t.Fatalf("error = %v, want empty campaign failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRemoteCurrentStateUsesCurrentKeysUnderSessionPrefix(t *testing.T) {
|
||||
cfg := restoreDiscoveryConfig()
|
||||
sessionPrefix, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
||||
base := &storage.FakeBackend{}
|
||||
store := &captureObjectStore{delegate: base}
|
||||
|
||||
base.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
base.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)})
|
||||
|
||||
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
||||
if err != nil {
|
||||
t.Fatalf("discoverRemoteCurrentState() error = %v", err)
|
||||
}
|
||||
|
||||
expectedRunKey := fmt.Sprintf("%scurrent/run_id.txt", sessionPrefix)
|
||||
expectedManifestKey := fmt.Sprintf("%scurrent/manifest.json", sessionPrefix)
|
||||
if !containsString(store.readKeys, expectedRunKey) {
|
||||
t.Fatalf("read keys = %#v, want run pointer key %q", store.readKeys, expectedRunKey)
|
||||
}
|
||||
if !containsString(store.readKeys, expectedManifestKey) {
|
||||
t.Fatalf("read keys = %#v, want manifest key %q", store.readKeys, expectedManifestKey)
|
||||
}
|
||||
}
|
||||
|
||||
type captureObjectStore struct {
|
||||
delegate storage.ObjectStore
|
||||
existsKeys []string
|
||||
readKeys []string
|
||||
downloadKeys []string
|
||||
}
|
||||
|
||||
func (s *captureObjectStore) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) {
|
||||
return s.delegate.List(ctx, prefix)
|
||||
}
|
||||
|
||||
func (s *captureObjectStore) Read(ctx context.Context, key string) (storage.ObjectInfo, io.ReadCloser, error) {
|
||||
s.readKeys = append(s.readKeys, key)
|
||||
return s.delegate.Read(ctx, key)
|
||||
}
|
||||
|
||||
func (s *captureObjectStore) Download(ctx context.Context, key, localPath string) error {
|
||||
s.downloadKeys = append(s.downloadKeys, key)
|
||||
return s.delegate.Download(ctx, key, localPath)
|
||||
}
|
||||
|
||||
func (s *captureObjectStore) Upload(ctx context.Context, localPath, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) {
|
||||
return s.delegate.Upload(ctx, localPath, key, opts)
|
||||
}
|
||||
|
||||
func (s *captureObjectStore) UploadConditional(ctx context.Context, source io.Reader, key string, opts storage.UploadOptions, condition storage.WriteCondition) (storage.ObjectInfo, error) {
|
||||
return s.delegate.UploadConditional(ctx, source, key, opts, condition)
|
||||
}
|
||||
|
||||
func (s *captureObjectStore) Exists(ctx context.Context, key string) (bool, error) {
|
||||
s.existsKeys = append(s.existsKeys, key)
|
||||
return s.delegate.Exists(ctx, key)
|
||||
}
|
||||
|
||||
func restoreDiscoveryConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Pipeline: &config.PipelineConfig{
|
||||
Storage: config.StorageConfig{
|
||||
S3: &config.StorageS3Config{
|
||||
Bucket: "my-dnd-archive",
|
||||
RootPrefix: "dnd",
|
||||
},
|
||||
},
|
||||
},
|
||||
Session: &config.SessionConfig{
|
||||
SessionID: "2026-05-03",
|
||||
Campaign: "sample-campaign",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func restoreDiscoveryKeys(cfg *config.Config) (sessionPrefix, manifestKey, runIDKey string) {
|
||||
sessionPrefix = artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
manifestKey, runIDKey = artifacts.ResolveCurrentStateKeys(sessionPrefix)
|
||||
return sessionPrefix, manifestKey, runIDKey
|
||||
}
|
||||
|
||||
func restoreManifestJSON(t *testing.T, sessionID, campaign string) []byte {
|
||||
t.Helper()
|
||||
now := time.Date(2026, 5, 19, 23, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)
|
||||
payload := map[string]any{
|
||||
"schema_version": "narratio.session-manifest.v1",
|
||||
"session_id": sessionID,
|
||||
"campaign": campaign,
|
||||
"run_id": "20260519T010203Z-a1b2c3d4",
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"stages": map[string]any{},
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
data, err := json.Marshal(map[string]any{"schema_version": "narratio.session-manifest.v1", "session_id": sessionID, "campaign": campaign, "run_id": "20260519T010203Z-a1b2c3d4", "created_at": now, "updated_at": now, "stages": map[string]any{}})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal manifest payload: %v", err)
|
||||
t.Fatalf("marshal manifest: %v", err)
|
||||
}
|
||||
return append(data, '\n')
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ func TestExecuteRestoreNonDryRunRestoresDurableFiles(t *testing.T) {
|
||||
seedRestoreObject(fake, sessionPrefix+"audio/alice.flac", []byte("remote-audio"))
|
||||
seedRestoreObject(fake, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n"))
|
||||
seedRestoreObject(fake, manifestKey, restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign))
|
||||
seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", map[string][]byte{
|
||||
"transcripts/full.json": []byte(`{"segments":[1,2,3]}`),
|
||||
"artifacts/session_recap.md": []byte("# recap\n"),
|
||||
"audio/alice.flac": []byte("remote-audio"),
|
||||
})
|
||||
|
||||
restoreWithStoreAndRealPhases(t, fake)
|
||||
|
||||
@@ -159,7 +164,8 @@ func TestExecuteRestoreIncludeAudioUsesCacheAfterWorkspaceDeletion(t *testing.T)
|
||||
if code != 0 {
|
||||
t.Fatalf("first restore exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
if got := fakeDownloadCount(fake, audioKey); got != 1 {
|
||||
committedAudioKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, "20260519T010203Z-a1b2c3d4"), "audio/alice.flac")
|
||||
if got := fakeDownloadCount(fake, committedAudioKey); got != 1 {
|
||||
t.Fatalf("audio downloads after first restore = %d, want 1", got)
|
||||
}
|
||||
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
@@ -174,7 +180,7 @@ func TestExecuteRestoreIncludeAudioUsesCacheAfterWorkspaceDeletion(t *testing.T)
|
||||
if code != 0 {
|
||||
t.Fatalf("second restore exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
if got := fakeDownloadCount(fake, audioKey); got != 1 {
|
||||
if got := fakeDownloadCount(fake, committedAudioKey); got != 1 {
|
||||
t.Fatalf("audio downloads after cached restore = %d, want still 1", got)
|
||||
}
|
||||
mustReadEquals(t, filepath.Join(sessionRoot, "audio", "alice.flac"), "remote-audio")
|
||||
@@ -598,13 +604,8 @@ func seedRestoreCommittedState(t *testing.T, fake *storage.FakeBackend, pipeline
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
|
||||
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix)
|
||||
|
||||
seedRestoreObject(fake, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n"))
|
||||
seedRestoreObject(fake, manifestKey, restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign))
|
||||
|
||||
return cfg, sessionPrefix, manifestKey, runIDKey
|
||||
current := seedCommittedRestoreSnapshot(t, cfg, fake, "20260519T010203Z-a1b2c3d4", nil)
|
||||
return cfg, current.SessionPrefix, current.CurrentManifestKey, artifacts.S3CurrentCommitPointerKey(current.SessionPrefix)
|
||||
}
|
||||
|
||||
func appendRestoreWorkflowPreviousInputConfig(t *testing.T, pipelinePath, sessionPath string) {
|
||||
@@ -636,10 +637,7 @@ func seedRestorePreviousCurrent(t *testing.T, fake *storage.FakeBackend, cfg *co
|
||||
|
||||
func seedRestorePreviousCurrentManifestOnly(t *testing.T, fake *storage.FakeBackend, cfg *config.Config) {
|
||||
t.Helper()
|
||||
previousPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
||||
manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(previousPrefix)
|
||||
previousRunID := "20260426T010203Z-a1b2c3d4"
|
||||
seedRestoreObject(fake, runIDKey, []byte(previousRunID+"\n"))
|
||||
|
||||
m := manifest.New(cfg.Session.PreviousSessionID, nowUTC())
|
||||
m.Campaign = cfg.Session.Campaign
|
||||
@@ -648,7 +646,22 @@ func seedRestorePreviousCurrentManifestOnly(t *testing.T, fake *storage.FakeBack
|
||||
if err != nil {
|
||||
t.Fatalf("marshal previous restore manifest: %v", err)
|
||||
}
|
||||
seedRestoreObject(fake, manifestKey, append(data, '\n'))
|
||||
manifestData := append(data, '\n')
|
||||
previousPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID)
|
||||
manifestKey := artifacts.S3RunSessionManifestKey(previousPrefix, previousRunID)
|
||||
seedRestoreObject(fake, manifestKey, manifestData)
|
||||
commit := artifacts.RemoteCommitManifest{FormatVersion: artifacts.RemoteCommitFormatVersion, Campaign: cfg.Session.Campaign, SessionID: cfg.Session.PreviousSessionID, RunID: previousRunID, Artifacts: []artifacts.RemoteArtifact{{Type: artifacts.RemoteArtifactTypeSessionManifest, Source: "session.manifest", DestinationKey: manifestKey, SHA256: restoreCommitSHA256(manifestData), Size: int64(len(manifestData)), Generation: fake.Objects[manifestKey].ETag}}}
|
||||
commitData, err := artifacts.EncodeRemoteCommitManifest(commit)
|
||||
if err != nil {
|
||||
t.Fatalf("encode previous restore commit: %v", err)
|
||||
}
|
||||
commitKey := artifacts.S3RunCommitKey(previousPrefix, previousRunID)
|
||||
seedRestoreObject(fake, commitKey, commitData)
|
||||
pointerData, err := artifacts.EncodeCurrentCommitPointer(artifacts.CurrentCommitPointer{FormatVersion: artifacts.RemoteCommitFormatVersion, Campaign: cfg.Session.Campaign, SessionID: cfg.Session.PreviousSessionID, RunID: previousRunID, CommitKey: commitKey, CommitSHA256: restoreCommitSHA256(commitData), CommitSize: int64(len(commitData)), CommitGeneration: fake.Objects[commitKey].ETag})
|
||||
if err != nil {
|
||||
t.Fatalf("encode previous restore pointer: %v", err)
|
||||
}
|
||||
seedRestoreObject(fake, artifacts.S3CurrentCommitPointerKey(previousPrefix), pointerData)
|
||||
}
|
||||
|
||||
func mustReadEquals(t *testing.T, path, want string) {
|
||||
|
||||
@@ -351,7 +351,71 @@ func TestRestorePlanTraversalUnsafeKeyFails(t *testing.T) {
|
||||
}
|
||||
|
||||
func seedRestoreObject(store *storage.FakeBackend, key string, data []byte) {
|
||||
if strings.HasSuffix(key, "/current/commit-pointer.json") {
|
||||
if _, exists := store.Objects[key]; exists {
|
||||
return
|
||||
}
|
||||
}
|
||||
store.SeedObject(storage.FakeObject{Key: key, Data: data})
|
||||
for pointerKey, pointerObject := range store.Objects {
|
||||
if !strings.HasSuffix(pointerKey, "/current/commit-pointer.json") || key == pointerKey {
|
||||
continue
|
||||
}
|
||||
pointer, err := artifacts.DecodeCurrentCommitPointer(pointerObject.Data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
commitObject, ok := store.Objects[pointer.CommitKey]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
commit, err := artifacts.DecodeRemoteCommitManifest(commitObject.Data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
runPrefix := strings.TrimSuffix(pointer.CommitKey, "commit.json")
|
||||
sessionPrefix := strings.TrimSuffix(runPrefix, "runs/"+pointer.RunID+"/")
|
||||
if !strings.HasPrefix(key, sessionPrefix) {
|
||||
continue
|
||||
}
|
||||
destination := key
|
||||
if strings.HasPrefix(key, sessionPrefix) && !strings.HasPrefix(key, runPrefix) {
|
||||
destination = runPrefix + strings.TrimPrefix(key, sessionPrefix)
|
||||
store.SeedObject(storage.FakeObject{Key: destination, Data: data})
|
||||
}
|
||||
typeID := artifacts.RemoteArtifactTypePublishedOutput
|
||||
source := "narratio.test"
|
||||
if destination == artifacts.S3RunSessionManifestKey(sessionPrefix, pointer.RunID) {
|
||||
typeID, source = artifacts.RemoteArtifactTypeSessionManifest, "session.manifest"
|
||||
} else if relative := strings.TrimPrefix(destination, runPrefix); strings.HasPrefix(relative, "artifacts/") {
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(relative, "artifacts/"), filepath.Ext(relative))
|
||||
source = artifacts.ConfiguredArtifactSourceID(name)
|
||||
}
|
||||
artifact := remoteRestoreArtifact(store, typeID, source, destination)
|
||||
replaced := false
|
||||
for index := range commit.Artifacts {
|
||||
if commit.Artifacts[index].DestinationKey == destination {
|
||||
commit.Artifacts[index] = artifact
|
||||
replaced = true
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
commit.Artifacts = append(commit.Artifacts, artifact)
|
||||
}
|
||||
commitData, err := artifacts.EncodeRemoteCommitManifest(*commit)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
store.SeedObject(storage.FakeObject{Key: pointer.CommitKey, Data: commitData})
|
||||
commitObject = store.Objects[pointer.CommitKey]
|
||||
pointer.CommitSHA256 = restoreCommitSHA256(commitData)
|
||||
pointer.CommitSize = int64(len(commitData))
|
||||
pointer.CommitGeneration = commitObject.ETag
|
||||
pointerData, err := artifacts.EncodeCurrentCommitPointer(*pointer)
|
||||
if err == nil {
|
||||
store.SeedObject(storage.FakeObject{Key: pointerKey, Data: pointerData})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func actionRelPaths(actions []RestoreAction) []string {
|
||||
@@ -402,12 +466,10 @@ func configureRestorePlanPreviousRequirement(cfg *config.Config, required bool)
|
||||
func restorePlanCurrentState(t *testing.T, cfg *config.Config) *RemoteCurrentState {
|
||||
t.Helper()
|
||||
sessionPrefix := artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix)
|
||||
return &RemoteCurrentState{
|
||||
Bucket: "test-bucket",
|
||||
SessionPrefix: sessionPrefix,
|
||||
CurrentManifestKey: manifestKey,
|
||||
CurrentRunIDKey: runIDKey,
|
||||
CurrentManifestKey: artifacts.S3RunSessionManifestKey(sessionPrefix, "20260519T010203Z-a1b2c3d4"),
|
||||
RunID: "20260519T010203Z-a1b2c3d4",
|
||||
SessionID: cfg.Session.SessionID,
|
||||
Campaign: cfg.Session.Campaign,
|
||||
|
||||
@@ -95,8 +95,9 @@ func TestForcedRestoreRetryClearsIncompleteMarker(t *testing.T) {
|
||||
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
mustWriteTestFile(t, filepath.Join(sessionRoot, "artifacts", "session_recap.md"), "local recap\n")
|
||||
mustWriteTestFile(t, filepath.Join(sessionRoot, "transcripts", "full.json"), "local transcript\n")
|
||||
committedTranscriptKey := artifacts.S3RunRelativeDestinationKey(artifacts.S3RunPrefix(sessionPrefix, "20260519T010203Z-a1b2c3d4"), "transcripts/full.json")
|
||||
fake.DownloadHook = func(call storage.FakeDownloadCall) error {
|
||||
if call.Key == transcriptKey {
|
||||
if call.Key == committedTranscriptKey {
|
||||
return context.Canceled
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -232,7 +232,8 @@ func TestExecuteSessionSubcommandsAcceptPositionalSessionID(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
|
||||
manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix)
|
||||
manifestKey := artifacts.S3RunSessionManifestKey(sessionPrefix, "20260519T010203Z-a1b2c3d4")
|
||||
runIDKey := artifacts.S3CurrentCommitPointerKey(sessionPrefix)
|
||||
fake.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
fake.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "2026-05-03", "sample-campaign")})
|
||||
var storeInitCalls int
|
||||
|
||||
Reference in New Issue
Block a user