Align internal publish terminology across stage, app, and artifacts
This commit is contained in:
@@ -75,7 +75,7 @@ func All() []Stage {
|
||||
normalizeStage{},
|
||||
trimStage{},
|
||||
analyzeStage{},
|
||||
archiveStage{},
|
||||
publishStage{},
|
||||
placeholderStage{name: "notify"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,10 +197,10 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
|
||||
}
|
||||
if s.Name() == "publish" {
|
||||
if result.Metadata["stage"] != "publish" {
|
||||
t.Fatalf("archive metadata = %#v, want stage=publish", result.Metadata)
|
||||
t.Fatalf("publish metadata = %#v, want stage=publish", result.Metadata)
|
||||
}
|
||||
if result.Metadata["uploaded"] != true {
|
||||
t.Fatalf("archive metadata = %#v, want uploaded=true", result.Metadata)
|
||||
t.Fatalf("publish metadata = %#v, want uploaded=true", result.Metadata)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -222,10 +222,10 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
|
||||
t.Fatalf("scriptorium run calls = %d, want 0 when scriptorium config is absent", len(sc.RunRequests))
|
||||
}
|
||||
if len(st.Requests) != 0 {
|
||||
t.Fatalf("storage archive calls = %d, want 0", len(st.Requests))
|
||||
t.Fatalf("storage publish calls = %d, want 0", len(st.Requests))
|
||||
}
|
||||
if _, ok := st.Objects["dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/"+m.RunID+"/manifest.json"]; !ok {
|
||||
t.Fatalf("archive upload missing manifest key in fake object store")
|
||||
t.Fatalf("publish upload missing manifest key in fake object store")
|
||||
}
|
||||
if len(nf.Requests) != 1 {
|
||||
t.Fatalf("notify calls = %d, want 1", len(nf.Requests))
|
||||
|
||||
@@ -193,7 +193,7 @@ func TestHydratePreviousSessionArtifactsDoesNotUseLocalPreviousWorkspaceState(t
|
||||
writeFile(t, filepath.Join(seed.PreviousSessionRoot, "artifacts", "session_recap.md"), "# local stale recap\n")
|
||||
|
||||
_, err := hydratePreviousSessionArtifacts(context.Background(), env, sessionPaths, requirements)
|
||||
if err == nil || !strings.Contains(err.Error(), "object missing from archive") {
|
||||
if err == nil || !strings.Contains(err.Error(), "object missing from published candidate keys") {
|
||||
t.Fatalf("error = %v, want remote-object-missing failure", err)
|
||||
}
|
||||
}
|
||||
@@ -273,7 +273,7 @@ func seedPreviousCurrentState(
|
||||
campaign := strings.TrimSpace(env.Config.Session.Campaign)
|
||||
rootPrefix := strings.TrimSpace(env.Config.Pipeline.Storage.S3.RootPrefix)
|
||||
previousSessionPrefix := artifacts.S3SessionPrefix(rootPrefix, campaign, previousSessionID)
|
||||
manifestKey, runPointerKey := artifacts.ResolveArchiveCurrentStateKeys(previousSessionPrefix)
|
||||
manifestKey, runPointerKey := artifacts.ResolveCurrentStateKeys(previousSessionPrefix)
|
||||
|
||||
previousRunID := "20260510T010203Z-a1b2c3d4"
|
||||
previousSessionRoot := filepath.Join(t.TempDir(), "work", campaign, previousSessionID)
|
||||
|
||||
@@ -19,13 +19,13 @@ import (
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
type archiveStage struct{}
|
||||
type archiveUploadFile struct {
|
||||
type publishStage struct{}
|
||||
type publishUploadFile struct {
|
||||
RelativePath string
|
||||
LocalPath string
|
||||
}
|
||||
|
||||
var archivePrerequisiteStages = []string{
|
||||
var publishPrerequisiteStages = []string{
|
||||
"prepare",
|
||||
"transcribe",
|
||||
"merge",
|
||||
@@ -35,9 +35,9 @@ var archivePrerequisiteStages = []string{
|
||||
"analyze",
|
||||
}
|
||||
|
||||
func (archiveStage) Name() string { return "publish" }
|
||||
func (publishStage) Name() string { return "publish" }
|
||||
|
||||
func (archiveStage) Declares() IODecl {
|
||||
func (publishStage) Declares() IODecl {
|
||||
return IODecl{
|
||||
Inputs: []artifacts.Ref{
|
||||
{Kind: "manifest", Category: "input", RelativePath: "manifest.json"},
|
||||
@@ -45,23 +45,23 @@ func (archiveStage) Declares() IODecl {
|
||||
}
|
||||
}
|
||||
|
||||
func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*StageResult, error) {
|
||||
func (publishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*StageResult, error) {
|
||||
if env == nil || env.Config == nil || env.Config.Pipeline == nil || env.Config.Session == nil {
|
||||
return nil, fmt.Errorf("publish: resolved config must include pipeline and session")
|
||||
}
|
||||
|
||||
if archiveDisabled(env) {
|
||||
if publishDisabled(env) {
|
||||
return &StageResult{
|
||||
Metadata: map[string]any{
|
||||
"stage": "publish",
|
||||
"skipped": true,
|
||||
"archive_enabled": false,
|
||||
"publish_enabled": false,
|
||||
"audio_upload_skipped": true,
|
||||
"current_pointer_written": false,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if archiveRunUploadDisabled(env) {
|
||||
if publishRunUploadDisabled(env) {
|
||||
return &StageResult{
|
||||
Metadata: map[string]any{
|
||||
"stage": "publish",
|
||||
@@ -80,7 +80,7 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
return nil, fmt.Errorf("publish: remote object store backend is required when publish run upload is enabled")
|
||||
}
|
||||
|
||||
runRoot, err := resolveArchiveRunRoot(env, m)
|
||||
runRoot, err := resolvePublishRunRoot(env, m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("publish: resolve run root: %w", err)
|
||||
}
|
||||
@@ -92,15 +92,15 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
return nil, fmt.Errorf("publish: run root %q is not a directory", runRoot)
|
||||
}
|
||||
|
||||
runPrefix, err := artifacts.ResolveArchiveRunPrefix(env.Config, m)
|
||||
runPrefix, err := artifacts.ResolvePublishRunPrefix(env.Config, m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("publish: resolve s3 run prefix: %w", err)
|
||||
}
|
||||
sessionPrefix, err := artifacts.ResolveArchiveSessionPrefix(env.Config, m)
|
||||
sessionPrefix, err := artifacts.ResolvePublishSessionPrefix(env.Config, m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("publish: resolve s3 session prefix: %w", err)
|
||||
}
|
||||
bucket := artifacts.ResolveArchiveBucket(env.Config, m)
|
||||
bucket := artifacts.ResolvePublishBucket(env.Config, m)
|
||||
if bucket == "" {
|
||||
return nil, fmt.Errorf("publish: resolve s3 bucket: bucket is required")
|
||||
}
|
||||
@@ -109,21 +109,21 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
return nil, fmt.Errorf("publish: run id is required")
|
||||
}
|
||||
|
||||
manifestSource, err := resolveArchiveRunManifestSource(runRoot)
|
||||
manifestSource, err := resolvePublishRunManifestSource(runRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("publish: resolve run manifest source: %w", err)
|
||||
}
|
||||
|
||||
runFiles, err := collectArchiveRunFiles(runRoot, manifestSource)
|
||||
runFiles, err := collectPublishRunFiles(runRoot, manifestSource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("publish: collect run files: %w", err)
|
||||
}
|
||||
sessionPaths := archiveSessionPaths(env, m)
|
||||
previousFiles, err := collectArchivePreviousFiles(sessionPaths.PreviousDir)
|
||||
sessionPaths := publishSessionPaths(env, m)
|
||||
previousFiles, err := collectPublishPreviousFiles(sessionPaths.PreviousDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("publish: collect previous files: %w", err)
|
||||
}
|
||||
runtimeCatalog, err := buildArchiveRuntimeArtifactCatalog(sessionPaths, env.Config.Pipeline.Scriptorium)
|
||||
runtimeCatalog, err := buildPublishRuntimeArtifactCatalog(sessionPaths, env.Config.Pipeline.Scriptorium)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("publish: build runtime artifact catalog: %w", err)
|
||||
}
|
||||
@@ -149,12 +149,12 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
}
|
||||
|
||||
publishedUploaded := make([]string, 0, len(publishOutputs))
|
||||
for _, promotion := range publishOutputs {
|
||||
key := artifacts.S3PublishedOutputKey(sessionPrefix, promotion.Dest)
|
||||
if _, err := env.ObjectStore.Upload(ctx, promotion.LocalPath, key, storage.UploadOptions{}); err != nil {
|
||||
return nil, fmt.Errorf("publish: upload published output source %q to %q: %w", promotion.Source, key, err)
|
||||
for _, publishedOutput := range publishOutputs {
|
||||
key := artifacts.S3PublishedOutputKey(sessionPrefix, publishedOutput.Dest)
|
||||
if _, err := env.ObjectStore.Upload(ctx, publishedOutput.LocalPath, key, storage.UploadOptions{}); err != nil {
|
||||
return nil, fmt.Errorf("publish: upload published output source %q to %q: %w", publishedOutput.Source, key, err)
|
||||
}
|
||||
publishedUploaded = append(publishedUploaded, promotion.Dest)
|
||||
publishedUploaded = append(publishedUploaded, publishedOutput.Dest)
|
||||
}
|
||||
|
||||
previousUploaded := make([]string, 0, len(previousFiles))
|
||||
@@ -166,8 +166,8 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
previousUploaded = append(previousUploaded, file.RelativePath)
|
||||
}
|
||||
|
||||
currentManifestKey, currentRunPointerKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix)
|
||||
manifestTempPath, err := writeCurrentManifestSnapshot(m, archiveMetadataPreview(
|
||||
currentManifestKey, currentRunPointerKey := artifacts.ResolveCurrentStateKeys(sessionPrefix)
|
||||
manifestTempPath, err := writeCurrentManifestSnapshot(m, publishMetadataPreview(
|
||||
bucket,
|
||||
runPrefix,
|
||||
sessionPrefix,
|
||||
@@ -250,7 +250,7 @@ type publishSkippedUnselectedOutput struct {
|
||||
Required bool
|
||||
}
|
||||
|
||||
func archiveDisabled(env *Env) bool {
|
||||
func publishDisabled(env *Env) bool {
|
||||
cfg := env.Config.Pipeline.Publish
|
||||
if cfg == nil {
|
||||
return true
|
||||
@@ -258,7 +258,7 @@ func archiveDisabled(env *Env) bool {
|
||||
return cfg.Enabled != nil && !*cfg.Enabled
|
||||
}
|
||||
|
||||
func archiveRunUploadDisabled(env *Env) bool {
|
||||
func publishRunUploadDisabled(env *Env) bool {
|
||||
cfg := env.Config.Pipeline.Publish
|
||||
if cfg == nil {
|
||||
return true
|
||||
@@ -270,7 +270,7 @@ func validatePublishPrerequisites(m *manifest.Manifest) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("manifest is required")
|
||||
}
|
||||
for _, stageName := range archivePrerequisiteStages {
|
||||
for _, stageName := range publishPrerequisiteStages {
|
||||
sr := m.Stages[stageName]
|
||||
if sr == nil {
|
||||
return fmt.Errorf("prerequisite stage %q has not succeeded", stageName)
|
||||
@@ -282,7 +282,7 @@ func validatePublishPrerequisites(m *manifest.Manifest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveArchiveRunRoot(env *Env, m *manifest.Manifest) (string, error) {
|
||||
func resolvePublishRunRoot(env *Env, m *manifest.Manifest) (string, error) {
|
||||
sessionID := strings.TrimSpace(env.Config.Session.SessionID)
|
||||
if sessionID == "" && m != nil {
|
||||
sessionID = strings.TrimSpace(m.SessionID)
|
||||
@@ -313,7 +313,7 @@ func resolveArchiveRunRoot(env *Env, m *manifest.Manifest) (string, error) {
|
||||
return canonical, nil
|
||||
}
|
||||
|
||||
func resolveArchiveSessionRoot(env *Env, m *manifest.Manifest) (string, error) {
|
||||
func resolvePublishSessionRoot(env *Env, m *manifest.Manifest) (string, error) {
|
||||
sessionID := strings.TrimSpace(env.Config.Session.SessionID)
|
||||
if sessionID == "" && m != nil {
|
||||
sessionID = strings.TrimSpace(m.SessionID)
|
||||
@@ -331,7 +331,7 @@ func resolveArchiveSessionRoot(env *Env, m *manifest.Manifest) (string, error) {
|
||||
return filepath.Clean(artifacts.SessionWorkDirForCampaign(env.Config.Pipeline.Workspace.Root, campaign, sessionID)), nil
|
||||
}
|
||||
|
||||
func archiveSessionPaths(env *Env, m *manifest.Manifest) artifacts.SessionPaths {
|
||||
func publishSessionPaths(env *Env, m *manifest.Manifest) artifacts.SessionPaths {
|
||||
sessionID := strings.TrimSpace(env.Config.Session.SessionID)
|
||||
if sessionID == "" && m != nil {
|
||||
sessionID = strings.TrimSpace(m.SessionID)
|
||||
@@ -357,8 +357,8 @@ func resolvePublishOutputs(
|
||||
skippedOptionalOutputs := make([]string, 0)
|
||||
skippedUnselectedOutputs := make([]publishSkippedUnselectedOutput, 0)
|
||||
lockedOutputs := make([]publishLockedOutput, 0)
|
||||
lockSet := archiveLockSet(locks)
|
||||
selectedSet := archiveSelectedArtifactSet(selectedArtifactKeys)
|
||||
lockSet := publishLockSet(locks)
|
||||
selectedSet := publishSelectedArtifactSet(selectedArtifactKeys)
|
||||
configuredOutputs := configuredOutputPathMapFromCatalog(catalog)
|
||||
for _, rule := range rules {
|
||||
source := strings.TrimSpace(rule.Source)
|
||||
@@ -424,7 +424,7 @@ func resolvePublishOutputs(
|
||||
return out, skippedOptionalOutputs, skippedUnselectedOutputs, lockedOutputs, nil
|
||||
}
|
||||
|
||||
func archiveSelectedArtifactSet(selected []string) map[string]struct{} {
|
||||
func publishSelectedArtifactSet(selected []string) map[string]struct{} {
|
||||
if len(selected) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -439,7 +439,7 @@ func archiveSelectedArtifactSet(selected []string) map[string]struct{} {
|
||||
return out
|
||||
}
|
||||
|
||||
func archiveLockSet(locks []config.PublishLockRule) map[string]config.PublishLockRule {
|
||||
func publishLockSet(locks []config.PublishLockRule) map[string]config.PublishLockRule {
|
||||
out := make(map[string]config.PublishLockRule, len(locks))
|
||||
for _, lock := range locks {
|
||||
source := strings.TrimSpace(lock.Source)
|
||||
@@ -471,7 +471,7 @@ func configuredOutputPathMapFromCatalog(catalog *artifacts.ArtifactCatalog) map[
|
||||
return out
|
||||
}
|
||||
|
||||
func buildArchiveRuntimeArtifactCatalog(
|
||||
func buildPublishRuntimeArtifactCatalog(
|
||||
paths artifacts.SessionPaths,
|
||||
scriptoriumCfg *config.ScriptoriumConfig,
|
||||
) (*artifacts.ArtifactCatalog, error) {
|
||||
@@ -538,8 +538,8 @@ func resolveConfiguredArtifactLocalPath(paths artifacts.SessionPaths, configured
|
||||
return filepath.Join(paths.Root, rel), nil
|
||||
}
|
||||
|
||||
func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile, error) {
|
||||
files := make([]archiveUploadFile, 0, 64)
|
||||
func collectPublishRunFiles(runRoot, manifestPath string) ([]publishUploadFile, error) {
|
||||
files := make([]publishUploadFile, 0, 64)
|
||||
err := filepath.WalkDir(runRoot, func(path string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
@@ -553,7 +553,7 @@ func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile,
|
||||
return fmt.Errorf("relative dir from %q to %q: %w", runRoot, path, err)
|
||||
}
|
||||
relDir = filepath.ToSlash(relDir)
|
||||
// Preserve existing behavior: audio is not uploaded in archive run record.
|
||||
// Preserve existing behavior: audio is not uploaded in publish run record.
|
||||
if relDir == "audio" || strings.HasPrefix(relDir, "audio/") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
@@ -564,7 +564,7 @@ func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile,
|
||||
return fmt.Errorf("relative path from %q to %q: %w", runRoot, path, err)
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
files = append(files, archiveUploadFile{
|
||||
files = append(files, publishUploadFile{
|
||||
RelativePath: rel,
|
||||
LocalPath: path,
|
||||
})
|
||||
@@ -584,11 +584,11 @@ func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile,
|
||||
if manifestInfo.IsDir() {
|
||||
return nil, fmt.Errorf("manifest path %q is a directory", manifestPath)
|
||||
}
|
||||
files = append(files, archiveUploadFile{
|
||||
files = append(files, publishUploadFile{
|
||||
RelativePath: "manifest.json",
|
||||
LocalPath: manifestPath,
|
||||
})
|
||||
seen := map[string]archiveUploadFile{}
|
||||
seen := map[string]publishUploadFile{}
|
||||
for _, file := range files {
|
||||
seen[file.RelativePath] = file
|
||||
}
|
||||
@@ -603,7 +603,7 @@ func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile,
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func collectArchivePreviousFiles(previousDir string) ([]archiveUploadFile, error) {
|
||||
func collectPublishPreviousFiles(previousDir string) ([]publishUploadFile, error) {
|
||||
previousDir = filepath.Clean(strings.TrimSpace(previousDir))
|
||||
if previousDir == "" {
|
||||
return nil, fmt.Errorf("previous directory is required")
|
||||
@@ -619,7 +619,7 @@ func collectArchivePreviousFiles(previousDir string) ([]archiveUploadFile, error
|
||||
return nil, fmt.Errorf("previous path %q is not a directory", previousDir)
|
||||
}
|
||||
|
||||
files := make([]archiveUploadFile, 0, 16)
|
||||
files := make([]publishUploadFile, 0, 16)
|
||||
err = filepath.WalkDir(previousDir, func(path string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
@@ -632,7 +632,7 @@ func collectArchivePreviousFiles(previousDir string) ([]archiveUploadFile, error
|
||||
return fmt.Errorf("relative path from %q to %q: %w", previousDir, path, err)
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
files = append(files, archiveUploadFile{
|
||||
files = append(files, publishUploadFile{
|
||||
RelativePath: filepath.ToSlash(filepath.Join(config.PathPreviousDirSegment, rel)),
|
||||
LocalPath: path,
|
||||
})
|
||||
@@ -648,7 +648,7 @@ func collectArchivePreviousFiles(previousDir string) ([]archiveUploadFile, error
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func resolveArchiveRunManifestSource(runRoot string) (string, error) {
|
||||
func resolvePublishRunManifestSource(runRoot string) (string, error) {
|
||||
path := filepath.Join(filepath.Clean(runRoot), "manifest.json")
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
@@ -674,7 +674,7 @@ func directoryExists(path string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
func writeCurrentManifestSnapshot(m *manifest.Manifest, archiveMetadata map[string]any) (string, error) {
|
||||
func writeCurrentManifestSnapshot(m *manifest.Manifest, publishMetadata map[string]any) (string, error) {
|
||||
if m == nil {
|
||||
return "", fmt.Errorf("manifest is required")
|
||||
}
|
||||
@@ -707,7 +707,7 @@ func writeCurrentManifestSnapshot(m *manifest.Manifest, archiveMetadata map[stri
|
||||
now := time.Now().UTC()
|
||||
clone.MarkStageSucceeded("publish", now, nil)
|
||||
if sr := clone.Stages["publish"]; sr != nil {
|
||||
sr.Metadata = archiveMetadata
|
||||
sr.Metadata = publishMetadata
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(&clone, "", " ")
|
||||
@@ -747,7 +747,7 @@ func writeCurrentRunIDPointer(runID string) (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func archiveMetadataPreview(
|
||||
func publishMetadataPreview(
|
||||
bucket, runPrefix, sessionPrefix string,
|
||||
runUploaded []string,
|
||||
publishedUploaded []string,
|
||||
@@ -17,11 +17,11 @@ import (
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
func TestArchiveSkipsWhenDisabled(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishSkipsWhenDisabled(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.Config.Pipeline.Publish.Enabled = boolPtr(false)
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
@@ -29,15 +29,15 @@ func TestArchiveSkipsWhenDisabled(t *testing.T) {
|
||||
t.Fatalf("metadata = %#v, want skipped=true", result.Metadata)
|
||||
}
|
||||
if len(env.ObjectStore.(*storage.FakeBackend).Uploads) != 0 {
|
||||
t.Fatalf("unexpected uploads when archive disabled")
|
||||
t.Fatalf("unexpected uploads when publish disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveSkipsRunUploadWhenDisabled(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishSkipsRunUploadWhenDisabled(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.Config.Pipeline.Publish.UploadRun = boolPtr(false)
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
@@ -49,11 +49,11 @@ func TestArchiveSkipsRunUploadWhenDisabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveFailsWhenPrerequisiteNotSucceeded(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishFailsWhenPrerequisiteNotSucceeded(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
m.Stages["trim"].Status = manifest.StatusFailed
|
||||
|
||||
_, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
_, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err == nil || !strings.Contains(err.Error(), `prerequisite stage "trim"`) {
|
||||
t.Fatalf("Run() error = %v, want prerequisite failure", err)
|
||||
}
|
||||
@@ -62,11 +62,11 @@ func TestArchiveFailsWhenPrerequisiteNotSucceeded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishUploadsRunRecordPublishedOutputsAndCurrentPointer(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
@@ -94,10 +94,10 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) {
|
||||
trimmedKey := sessionPrefix + "transcripts/final.trimmed.json"
|
||||
recapKey := sessionPrefix + "artifacts/session_recap.md"
|
||||
if _, ok := fake.Objects[trimmedKey]; !ok {
|
||||
t.Fatalf("missing promoted key %q", trimmedKey)
|
||||
t.Fatalf("missing published output key %q", trimmedKey)
|
||||
}
|
||||
if _, ok := fake.Objects[recapKey]; !ok {
|
||||
t.Fatalf("missing promoted key %q", recapKey)
|
||||
t.Fatalf("missing published output key %q", recapKey)
|
||||
}
|
||||
|
||||
currentManifestKey := sessionPrefix + "current/manifest.json"
|
||||
@@ -136,8 +136,8 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveUploadsPreviousCacheWhenPresent(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishUploadsPreviousCacheWhenPresent(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
sessionRoot := artifacts.SessionWorkDirForCampaign(
|
||||
env.Config.Pipeline.Workspace.Root,
|
||||
@@ -147,7 +147,7 @@ func TestArchiveUploadsPreviousCacheWhenPresent(t *testing.T) {
|
||||
writeStageTestFile(t, filepath.Join(sessionRoot, "previous", "manifest.json"), "{\"session_id\":\"2026-04-12\"}\n")
|
||||
writeStageTestFile(t, filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md"), "# previous recap\n")
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
@@ -155,20 +155,20 @@ func TestArchiveUploadsPreviousCacheWhenPresent(t *testing.T) {
|
||||
previousManifestKey := m.S3SessionPrefix + "previous/manifest.json"
|
||||
previousRecapKey := m.S3SessionPrefix + "previous/artifacts/session_recap.md"
|
||||
if _, ok := fake.Objects[previousManifestKey]; !ok {
|
||||
t.Fatalf("missing archived previous manifest key %q", previousManifestKey)
|
||||
t.Fatalf("missing published previous manifest key %q", previousManifestKey)
|
||||
}
|
||||
if _, ok := fake.Objects[previousRecapKey]; !ok {
|
||||
t.Fatalf("missing archived previous artifact key %q", previousRecapKey)
|
||||
t.Fatalf("missing published previous artifact key %q", previousRecapKey)
|
||||
}
|
||||
if result.Metadata["previous_files_uploaded"] != 2 {
|
||||
t.Fatalf("metadata previous_files_uploaded = %#v, want 2", result.Metadata["previous_files_uploaded"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveToleratesMissingPreviousCache(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishToleratesMissingPreviousCache(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
@@ -177,35 +177,35 @@ func TestArchiveToleratesMissingPreviousCache(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveUsesCustomPromotionRules(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishUsesCustomOutputRules(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{
|
||||
{Source: "narratio.transcript.final_trimmed", Dest: "published/trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.artifact.session_recap", Dest: "published/recap.md", Required: boolPtr(true)},
|
||||
}
|
||||
|
||||
_, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
_, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"published/trimmed.json"]; !ok {
|
||||
t.Fatalf("missing custom promoted trimmed key")
|
||||
t.Fatalf("missing custom published trimmed key")
|
||||
}
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"published/recap.md"]; !ok {
|
||||
t.Fatalf("missing custom promoted recap key")
|
||||
t.Fatalf("missing custom published recap key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishSkipsOptionalMissingOutput(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{
|
||||
{Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(false)},
|
||||
}
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
@@ -216,8 +216,8 @@ func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishSkipsRequiredUnselectedConfiguredOutput(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.SelectedArtifactKeys = []string{"player_handout"}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -226,15 +226,15 @@ func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) {
|
||||
}
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"transcripts/final.trimmed.json"]; !ok {
|
||||
t.Fatalf("missing built-in promoted trimmed key")
|
||||
t.Fatalf("missing built-in published trimmed key")
|
||||
}
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok {
|
||||
t.Fatalf("unexpected unselected recap promotion upload")
|
||||
t.Fatalf("unexpected unselected recap published output upload")
|
||||
}
|
||||
skipped := result.Metadata["skipped_unselected_outputs"].([]map[string]any)
|
||||
if len(skipped) != 1 {
|
||||
@@ -251,8 +251,8 @@ func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveSelectedConfiguredPromotionStillFailsWhenMissing(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishSelectedConfiguredOutputStillFailsWhenMissing(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.SelectedArtifactKeys = []string{"session_recap"}
|
||||
sessionRoot := artifacts.SessionWorkDirForCampaign(
|
||||
env.Config.Pipeline.Workspace.Root,
|
||||
@@ -263,26 +263,26 @@ func TestArchiveSelectedConfiguredPromotionStillFailsWhenMissing(t *testing.T) {
|
||||
t.Fatalf("remove recap: %v", err)
|
||||
}
|
||||
|
||||
_, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
_, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err == nil || !strings.Contains(err.Error(), `required output source unavailable: "narratio.artifact.session_recap"`) {
|
||||
t.Fatalf("Run() error = %v, want required selected output failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveLockedSelectedPromotionSkipsAsLocked(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishLockedSelectedOutputSkipsAsLocked(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.SelectedArtifactKeys = []string{"session_recap"}
|
||||
env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{
|
||||
{Source: "narratio.artifact.session_recap", Reason: "reviewed"},
|
||||
}
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok {
|
||||
t.Fatalf("unexpected locked recap promotion upload")
|
||||
t.Fatalf("unexpected locked recap published output upload")
|
||||
}
|
||||
if result.Metadata["locked_output_count"] != 1 {
|
||||
t.Fatalf("locked_output_count = %#v, want 1", result.Metadata["locked_output_count"])
|
||||
@@ -293,8 +293,8 @@ func TestArchiveLockedSelectedPromotionSkipsAsLocked(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishLockedUnselectedConfiguredOutputSkipsAsUnselected(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.SelectedArtifactKeys = []string{"player_handout"}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -305,7 +305,7 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.
|
||||
{Source: "narratio.artifact.session_recap", Reason: "reviewed"},
|
||||
}
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
@@ -318,25 +318,25 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishSkipsLockedRequiredOutputAndCommits(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{
|
||||
{Source: "narratio.transcript.final_trimmed", Reason: "human reviewed"},
|
||||
}
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json"
|
||||
if _, ok := fake.Objects[trimmedKey]; ok {
|
||||
t.Fatalf("locked promotion key %q should not be uploaded", trimmedKey)
|
||||
t.Fatalf("locked published output key %q should not be uploaded", trimmedKey)
|
||||
}
|
||||
recapKey := m.S3SessionPrefix + "artifacts/session_recap.md"
|
||||
if _, ok := fake.Objects[recapKey]; !ok {
|
||||
t.Fatalf("unlocked promotion key %q should be uploaded", recapKey)
|
||||
t.Fatalf("unlocked published output key %q should be uploaded", recapKey)
|
||||
}
|
||||
runTrimmedKey := m.S3RunPrefix + "trim/outputs/transcripts/final.trimmed.json"
|
||||
if _, ok := fake.Objects[runTrimmedKey]; !ok {
|
||||
@@ -365,7 +365,7 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
|
||||
locked[0]["required"] != true ||
|
||||
locked[0]["local_path"] == "" ||
|
||||
locked[0]["provenance"] == "" {
|
||||
t.Fatalf("locked promotion metadata = %#v", locked[0])
|
||||
t.Fatalf("locked published output metadata = %#v", locked[0])
|
||||
}
|
||||
|
||||
currentManifestKey := m.S3SessionPrefix + "current/manifest.json"
|
||||
@@ -374,8 +374,8 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
|
||||
t.Fatalf("unmarshal current manifest: %v", err)
|
||||
}
|
||||
stages := current["stages"].(map[string]any)
|
||||
archive := stages["publish"].(map[string]any)
|
||||
meta := archive["metadata"].(map[string]any)
|
||||
publishRecord := stages["publish"].(map[string]any)
|
||||
meta := publishRecord["metadata"].(map[string]any)
|
||||
if meta["locked_output_count"] != float64(1) {
|
||||
t.Fatalf("current manifest locked_output_count = %#v, want 1", meta["locked_output_count"])
|
||||
}
|
||||
@@ -385,8 +385,8 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishLockedRequiredMissingOutputSucceeds(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{
|
||||
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
|
||||
}
|
||||
@@ -394,7 +394,7 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
|
||||
{Source: "narratio.transcript.base", Reason: "manual merge is locked"},
|
||||
}
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
result, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
@@ -402,22 +402,22 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
mergedKey := m.S3SessionPrefix + "transcripts/base.json"
|
||||
if _, ok := fake.Objects[mergedKey]; ok {
|
||||
t.Fatalf("locked missing promotion key %q should not be uploaded", mergedKey)
|
||||
t.Fatalf("locked missing published output key %q should not be uploaded", mergedKey)
|
||||
}
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"current/run_id.txt"]; !ok {
|
||||
t.Fatalf("current run pointer should be written for locked missing promotion")
|
||||
t.Fatalf("current run pointer should be written for locked missing output")
|
||||
}
|
||||
locked := result.Metadata["locked_outputs"].([]map[string]any)
|
||||
if len(locked) != 1 {
|
||||
t.Fatalf("locked_outputs = %#v, want one item", locked)
|
||||
}
|
||||
if locked[0]["local_path"] != "" || locked[0]["provenance"] != "" {
|
||||
t.Fatalf("locked missing promotion metadata = %#v, want empty local path/provenance", locked[0])
|
||||
t.Fatalf("locked missing published output metadata = %#v, want empty local path/provenance", locked[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishLockDoesNotOverwriteExistingOutput(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{
|
||||
{Source: "narratio.transcript.final_trimmed", Reason: "already published"},
|
||||
}
|
||||
@@ -425,40 +425,40 @@ func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
|
||||
trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json"
|
||||
fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte("previously published\n")})
|
||||
|
||||
if _, err := (archiveStage{}).Run(context.Background(), env, m); err != nil {
|
||||
if _, err := (publishStage{}).Run(context.Background(), env, m); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
got := string(fake.Objects[trimmedKey].Data)
|
||||
if got != "previously published\n" {
|
||||
t.Fatalf("locked promotion object contents = %q, want existing object preserved", got)
|
||||
t.Fatalf("locked published output object contents = %q, want existing object preserved", got)
|
||||
}
|
||||
for _, upload := range fake.Uploads {
|
||||
if upload.Key == trimmedKey {
|
||||
t.Fatalf("locked promotion key %q was uploaded", trimmedKey)
|
||||
t.Fatalf("locked published output key %q was uploaded", trimmedKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishFailsWhenRequiredOutputMissing(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{
|
||||
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
|
||||
}
|
||||
|
||||
_, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
_, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err == nil || !strings.Contains(err.Error(), "required output source unavailable") {
|
||||
t.Fatalf("Run() error = %v, want required output source unavailable failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveFailsWhenCanonicalRunRootMissing(t *testing.T) {
|
||||
env, m, runRoot := archiveFixture(t)
|
||||
func TestPublishFailsWhenCanonicalRunRootMissing(t *testing.T) {
|
||||
env, m, runRoot := publishFixture(t)
|
||||
if err := os.RemoveAll(runRoot); err != nil {
|
||||
t.Fatalf("remove run root: %v", err)
|
||||
}
|
||||
|
||||
_, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
_, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing run-root error, got nil")
|
||||
}
|
||||
@@ -467,8 +467,8 @@ func TestArchiveFailsWhenCanonicalRunRootMissing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishDoesNotWriteCurrentPointerWhenOutputUploadFails(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json"
|
||||
|
||||
@@ -480,26 +480,26 @@ func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T)
|
||||
originalUpload := fake.Upload
|
||||
_ = originalUpload
|
||||
// Use UploadErr toggle by checking call sequence in postcondition.
|
||||
// First failure point is promotion upload; simulate by setting error immediately before promotion key write.
|
||||
// First failure point is published output upload; simulate by setting error immediately before published output key write.
|
||||
// We cannot hook FakeBackend per-key without changing public behavior; use dedicated backend wrapper instead.
|
||||
env.ObjectStore = &promotionFailingStore{delegate: fake, failKey: failingKey}
|
||||
env.ObjectStore = &publishedOutputFailingStore{delegate: fake, failKey: failingKey}
|
||||
|
||||
_, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
_, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err == nil || !strings.Contains(err.Error(), "upload published output source") {
|
||||
t.Fatalf("Run() error = %v, want published output upload failure", err)
|
||||
}
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"current/run_id.txt"]; ok {
|
||||
t.Fatalf("unexpected current pointer write on promotion failure")
|
||||
t.Fatalf("unexpected current pointer write on published output failure")
|
||||
}
|
||||
fake.UploadErr = origUploadErr
|
||||
}
|
||||
|
||||
func TestArchiveDoesNotWriteCurrentPointerWhenCurrentManifestUploadFails(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishDoesNotWriteCurrentPointerWhenCurrentManifestUploadFails(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
env.ObjectStore = &promotionFailingStore{delegate: fake, failKey: m.S3SessionPrefix + "current/manifest.json"}
|
||||
env.ObjectStore = &publishedOutputFailingStore{delegate: fake, failKey: m.S3SessionPrefix + "current/manifest.json"}
|
||||
|
||||
_, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
_, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err == nil || !strings.Contains(err.Error(), "current manifest") {
|
||||
t.Fatalf("Run() error = %v, want current manifest upload failure", err)
|
||||
}
|
||||
@@ -508,17 +508,17 @@ func TestArchiveDoesNotWriteCurrentPointerWhenCurrentManifestUploadFails(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveFailsWithoutObjectStore(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
func TestPublishFailsWithoutObjectStore(t *testing.T) {
|
||||
env, m, _ := publishFixture(t)
|
||||
env.ObjectStore = nil
|
||||
|
||||
_, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
_, err := publishStage{}.Run(context.Background(), env, m)
|
||||
if err == nil || !strings.Contains(err.Error(), "object store") {
|
||||
t.Fatalf("Run() error = %v, want object store backend failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
|
||||
func publishFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
|
||||
t.Helper()
|
||||
|
||||
root := t.TempDir()
|
||||
@@ -549,7 +549,7 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
|
||||
m.S3Bucket = "my-dnd-archive"
|
||||
m.S3SessionPrefix = artifacts.S3SessionPrefix("dnd", campaign, sessionID)
|
||||
m.S3RunPrefix = artifacts.S3RunPrefix(m.S3SessionPrefix, runID)
|
||||
for _, name := range archivePrerequisiteStages {
|
||||
for _, name := range publishPrerequisiteStages {
|
||||
m.MarkStageSucceeded(name, time.Date(2026, 5, 16, 1, 2, 3, 0, time.UTC), nil)
|
||||
}
|
||||
|
||||
@@ -591,27 +591,27 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
|
||||
return env, m, runRoot
|
||||
}
|
||||
|
||||
type promotionFailingStore struct {
|
||||
type publishedOutputFailingStore struct {
|
||||
delegate *storage.FakeBackend
|
||||
failKey string
|
||||
}
|
||||
|
||||
func (s *promotionFailingStore) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) {
|
||||
func (s *publishedOutputFailingStore) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) {
|
||||
return s.delegate.List(ctx, prefix)
|
||||
}
|
||||
|
||||
func (s *promotionFailingStore) Download(ctx context.Context, key, localPath string) error {
|
||||
func (s *publishedOutputFailingStore) Download(ctx context.Context, key, localPath string) error {
|
||||
return s.delegate.Download(ctx, key, localPath)
|
||||
}
|
||||
|
||||
func (s *promotionFailingStore) Upload(ctx context.Context, localPath, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) {
|
||||
func (s *publishedOutputFailingStore) Upload(ctx context.Context, localPath, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) {
|
||||
if strings.TrimSpace(key) == strings.TrimSpace(s.failKey) {
|
||||
return storage.ObjectInfo{}, errors.New("forced upload failure")
|
||||
}
|
||||
return s.delegate.Upload(ctx, localPath, key, opts)
|
||||
}
|
||||
|
||||
func (s *promotionFailingStore) Exists(ctx context.Context, key string) (bool, error) {
|
||||
func (s *publishedOutputFailingStore) Exists(ctx context.Context, key string) (bool, error) {
|
||||
return s.delegate.Exists(ctx, key)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user