Update run reporting for catalog workflows

This commit is contained in:
2026-06-19 15:50:09 +00:00
parent 52078e2195
commit 91b72478d5
9 changed files with 147 additions and 226 deletions

View File

@@ -80,8 +80,8 @@ func processDestinationSelection(ctx context.Context, request runDestinationRequ
}
if isFixedPathDestination(request.destination) {
plan.PathMapping = config.PathMappingFixed
if request.options.DryRun && isDestructiveFixedPathAction(plan.Action) {
warning := fixedPathReplacementWarning(plan)
if request.options.DryRun && isFixedPathWorkflowAction(plan.Action) {
warning := fixedPathWorkflowWarning(plan)
request.recorder.addPipelineWarning(request.pipelineIndex, warning)
}
}
@@ -139,6 +139,7 @@ func (recorder *runReportRecorder) recordDestinationFailure(pipelineIndex int, f
recorder.failures.add(failure.pipelineID, failure.destinationID, failure.backend, storage.DisplayPath(failure.bundlePath), failure.err)
recorder.summary.recordFailure()
if includeAction {
recorder.summary.recordFailureAction(action.Action)
recorder.addPipelineAction(pipelineIndex, action)
}
}
@@ -159,5 +160,8 @@ func completePlanIdentity(plan publish.Plan, pipeline config.Pipeline, destinati
if plan.DestinationBundlePath == "" {
plan.DestinationBundlePath = selection.DestinationBundlePath
}
if plan.Workflow == "" {
plan.Workflow = destination.Workflow
}
return plan
}

View File

@@ -6,7 +6,7 @@ import (
)
func shouldNotify(action publish.Action) bool {
return action == publish.ActionPublishNew || action == publish.ActionReplaceOlder || action == publish.ActionReplaceConflict || action == publish.ActionReplaceNewer || action == publish.ActionReplaceTakeover || action == publish.ActionForceReplace
return action == publish.ActionPublishNew || action == publish.ActionUpsertAdditive || action == publish.ActionReplaceCatalog || action == publish.ActionForceReplace
}
func notifyEvent(plan publish.Plan) notify.Event {

View File

@@ -60,7 +60,7 @@ func writeRunActionLine(w io.Writer, action RunActionRecord) {
fmt.Fprintf(w, " - bundle=%s destination=%s backend=%s%s action=error reason=%q\n", action.BundlePath, destinationID, action.Backend, pathMappingRecordSummary(action), action.Reason)
return
}
fmt.Fprintf(w, " - bundle=%s destination=%s backend=%s%s action=%s%s outputs=%s reason=%q\n", action.BundlePath, action.DestinationID, action.Backend, pathMappingRecordSummary(action), action.Action, takeoverModeRecordSummary(action), outputRecordSummary(action.Outputs), action.Reason)
fmt.Fprintf(w, " - bundle=%s destination=%s backend=%s%s action=%s%s outputs=%s reason=%q\n", action.BundlePath, action.DestinationID, action.Backend, pathMappingRecordSummary(action), action.Action, workflowRecordSummary(action), outputRecordSummary(action.Outputs), action.Reason)
}
func pathMappingRecordSummary(action RunActionRecord) string {
@@ -70,11 +70,11 @@ func pathMappingRecordSummary(action RunActionRecord) string {
return fmt.Sprintf(" path_mapping=fixed target=%s", action.DestinationPath)
}
func takeoverModeRecordSummary(action RunActionRecord) string {
if action.TakeoverMode == "" {
func workflowRecordSummary(action RunActionRecord) string {
if action.Workflow == "" {
return ""
}
return fmt.Sprintf(" takeover_mode=%s", action.TakeoverMode)
return fmt.Sprintf(" workflow=%s", action.Workflow)
}
func outputRecordSummary(outputs []RunOutputRecord) string {
@@ -143,8 +143,8 @@ type RunActionRecord struct {
BundlePath string `json:"bundle_path"`
DestinationPath string `json:"destination_path"`
PathMapping string `json:"path_mapping,omitempty"`
Workflow string `json:"workflow,omitempty"`
Action string `json:"action"`
TakeoverMode string `json:"takeover_mode,omitempty"`
PrimaryURL string `json:"primary_url,omitempty"`
Reason string `json:"reason,omitempty"`
Outputs []RunOutputRecord `json:"outputs"`
@@ -166,6 +166,13 @@ func runActionFromPlan(backend string, plan publish.Plan, planErr error) RunActi
if destinationID == "" {
destinationID = "unknown"
}
action := "error"
outputs := []RunOutputRecord{}
switch plan.Action {
case publish.ActionFailUnmanaged, publish.ActionFailConflict:
action = string(plan.Action)
outputs = runOutputsFromPlan(plan.Outputs)
}
return RunActionRecord{
PipelineID: plan.PipelineID,
DestinationID: destinationID,
@@ -174,10 +181,11 @@ func runActionFromPlan(backend string, plan publish.Plan, planErr error) RunActi
BundlePath: storage.DisplayPath(plan.BundlePath),
DestinationPath: storage.DisplayPath(plan.DestinationBundlePath),
PathMapping: plan.PathMapping,
Action: "error",
Workflow: plan.Workflow,
Action: action,
PrimaryURL: plan.PrimaryURL,
Reason: planErr.Error(),
Outputs: []RunOutputRecord{},
Outputs: outputs,
}
}
return RunActionRecord{
@@ -188,21 +196,14 @@ func runActionFromPlan(backend string, plan publish.Plan, planErr error) RunActi
BundlePath: storage.DisplayPath(plan.BundlePath),
DestinationPath: storage.DisplayPath(plan.DestinationBundlePath),
PathMapping: plan.PathMapping,
Workflow: plan.Workflow,
Action: string(plan.Action),
TakeoverMode: takeoverModeForAction(plan),
PrimaryURL: plan.PrimaryURL,
Reason: plan.Reason,
Outputs: runOutputsFromPlan(plan.Outputs),
}
}
func takeoverModeForAction(plan publish.Plan) string {
if plan.Action != publish.ActionReplaceTakeover {
return ""
}
return plan.TakeoverMode
}
func errorAction(pipelineID, destinationID, backend, bundlePath string, err error) RunActionRecord {
return RunActionRecord{
PipelineID: pipelineID,

View File

@@ -62,15 +62,22 @@ func fixedPathSelectionWarning(pipelineID, destinationID string, selections []de
return OutputWarning{Message: fmt.Sprintf("pipeline=%s destination=%s path_mapping=fixed candidates=%d selected_bundle=%s destination_bundle=.", pipelineID, destinationID, candidateCount, selected)}
}
func isDestructiveFixedPathAction(action publish.Action) bool {
return action == publish.ActionReplaceOlder || action == publish.ActionReplaceConflict || action == publish.ActionReplaceNewer || action == publish.ActionReplaceTakeover || action == publish.ActionForceReplace
func isFixedPathWorkflowAction(action publish.Action) bool {
return action == publish.ActionUpsertAdditive || action == publish.ActionReplaceCatalog || action == publish.ActionForceReplace
}
func fixedPathReplacementWarning(plan publish.Plan) OutputWarning {
if plan.Action == publish.ActionReplaceTakeover {
return OutputWarning{Message: fmt.Sprintf("pipeline=%s destination=%s path_mapping=fixed action=%s takeover_mode=%s replaces destination root for selected_bundle=%s reason=%q", plan.PipelineID, plan.DestinationID, plan.Action, plan.TakeoverMode, storage.DisplayPath(plan.BundlePath), plan.Reason)}
func fixedPathWorkflowWarning(plan publish.Plan) OutputWarning {
switch plan.Action {
case publish.ActionReplaceCatalog:
if plan.ClearDestinationRoot {
return OutputWarning{Message: fmt.Sprintf("pipeline=%s destination=%s path_mapping=fixed workflow=%s action=%s clears destination root before writing selected_bundle=%s", plan.PipelineID, plan.DestinationID, plan.Workflow, plan.Action, storage.DisplayPath(plan.BundlePath))}
}
return OutputWarning{Message: fmt.Sprintf("pipeline=%s destination=%s path_mapping=fixed workflow=%s action=%s replaces current-owner catalog outputs for selected_bundle=%s", plan.PipelineID, plan.DestinationID, plan.Workflow, plan.Action, storage.DisplayPath(plan.BundlePath))}
case publish.ActionUpsertAdditive:
return OutputWarning{Message: fmt.Sprintf("pipeline=%s destination=%s path_mapping=fixed workflow=%s action=%s upserts planned outputs at destination root for selected_bundle=%s", plan.PipelineID, plan.DestinationID, plan.Workflow, plan.Action, storage.DisplayPath(plan.BundlePath))}
default:
return OutputWarning{Message: fmt.Sprintf("pipeline=%s destination=%s path_mapping=fixed workflow=%s action=%s writes selected_bundle=%s", plan.PipelineID, plan.DestinationID, plan.Workflow, plan.Action, storage.DisplayPath(plan.BundlePath))}
}
return OutputWarning{Message: fmt.Sprintf("pipeline=%s destination=%s path_mapping=fixed action=%s replaces destination root for selected_bundle=%s", plan.PipelineID, plan.DestinationID, plan.Action, storage.DisplayPath(plan.BundlePath))}
}
func destinationIDs(destinations []config.Destination) []string {

View File

@@ -10,12 +10,12 @@ type runSummary struct {
dryRun bool
planned int
publishNew int
replaceOlder int
replaceConflict int
replaceNewer int
replaceTakeover int
upsertAdditive int
replaceCatalog int
skipSame int
forceReplace int
skipped int
failUnmanaged int
failConflict int
failures int
fixedPath int
}
@@ -25,18 +25,23 @@ func (s *runSummary) recordPlan(action publish.Action) {
switch action {
case publish.ActionPublishNew:
s.publishNew++
case publish.ActionReplaceOlder:
s.replaceOlder++
case publish.ActionReplaceConflict:
s.replaceConflict++
case publish.ActionReplaceNewer:
s.replaceNewer++
case publish.ActionReplaceTakeover:
s.replaceTakeover++
case publish.ActionUpsertAdditive:
s.upsertAdditive++
case publish.ActionReplaceCatalog:
s.replaceCatalog++
case publish.ActionForceReplace:
s.forceReplace++
case publish.ActionSkipSame, publish.ActionSkipDestinationNewer:
s.skipped++
s.skipSame++
}
}
func (s *runSummary) recordFailureAction(action string) {
switch action {
case string(publish.ActionFailUnmanaged):
s.failUnmanaged++
case string(publish.ActionFailConflict):
s.failConflict++
}
}
@@ -52,19 +57,19 @@ type RunSummaryCounters struct {
Status string `json:"status"`
Planned int `json:"planned"`
PublishNew int `json:"publish_new"`
ReplaceOlder int `json:"replace_older"`
ReplaceConflict int `json:"replace_conflict"`
ReplaceNewer int `json:"replace_newer"`
ReplaceTakeover int `json:"replace_takeover"`
UpsertAdditive int `json:"upsert_additive"`
ReplaceCatalog int `json:"replace_catalog"`
SkipSame int `json:"skip_same"`
ForceReplace int `json:"force_replace"`
Skipped int `json:"skipped"`
FailUnmanaged int `json:"fail_unmanaged"`
FailConflict int `json:"fail_conflict"`
Failed int `json:"failed"`
DryRun bool `json:"dry_run"`
FixedPath int `json:"fixed_path"`
}
func (s RunSummaryCounters) Line() string {
return fmt.Sprintf("Final status: %s planned=%d publish_new=%d replace_older=%d replace_conflict=%d replace_newer=%d replace_takeover=%d force_replace=%d skipped=%d failed=%d dry_run=%t fixed_path=%d", s.Status, s.Planned, s.PublishNew, s.ReplaceOlder, s.ReplaceConflict, s.ReplaceNewer, s.ReplaceTakeover, s.ForceReplace, s.Skipped, s.Failed, s.DryRun, s.FixedPath)
return fmt.Sprintf("Final status: %s planned=%d publish_new=%d upsert_additive=%d replace_catalog=%d skip_same=%d force_replace=%d fail_unmanaged=%d fail_conflict=%d failed=%d dry_run=%t fixed_path=%d", s.Status, s.Planned, s.PublishNew, s.UpsertAdditive, s.ReplaceCatalog, s.SkipSame, s.ForceReplace, s.FailUnmanaged, s.FailConflict, s.Failed, s.DryRun, s.FixedPath)
}
func (s runSummary) Result() RunSummaryCounters {
@@ -76,12 +81,12 @@ func (s runSummary) Result() RunSummaryCounters {
Status: status,
Planned: s.planned,
PublishNew: s.publishNew,
ReplaceOlder: s.replaceOlder,
ReplaceConflict: s.replaceConflict,
ReplaceNewer: s.replaceNewer,
ReplaceTakeover: s.replaceTakeover,
UpsertAdditive: s.upsertAdditive,
ReplaceCatalog: s.replaceCatalog,
SkipSame: s.skipSame,
ForceReplace: s.forceReplace,
Skipped: s.skipped,
FailUnmanaged: s.failUnmanaged,
FailConflict: s.failConflict,
Failed: s.failures,
DryRun: s.dryRun,
FixedPath: s.fixedPath,

View File

@@ -41,8 +41,8 @@ func TestRunDryRunPrintsConfigSummary(t *testing.T) {
for _, want := range []string{
"Configured pipelines: 1",
"- pipeline=reports source=local bundles=1 destinations=archive",
"bundle=. destination=archive backend=local action=publish_new outputs=report.md,summary.txt",
"Final status: ok planned=1 publish_new=1 replace_older=0 replace_conflict=0 replace_newer=0 replace_takeover=0 force_replace=0 skipped=0 failed=0 dry_run=true",
"bundle=. destination=archive backend=local action=publish_new workflow=additive outputs=report.md,summary.txt",
"Final status: ok planned=1 publish_new=1 upsert_additive=0 replace_catalog=0 skip_same=0 force_replace=0 fail_unmanaged=0 fail_conflict=0 failed=0 dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("Run() output = %q, want substring %q", output, want)
@@ -496,7 +496,6 @@ func TestRunFixedPathDryRunReportsSelection(t *testing.T) {
}
func TestRunFixedPathDryRunWarnsForReplacement(t *testing.T) {
t.Skip("fixed-path replacement reporting is covered by the catalog reporting work")
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
writeSourceBundle(t, sourceRoot, "old", testBundleOptions{
@@ -507,7 +506,7 @@ func TestRunFixedPathDryRunWarnsForReplacement(t *testing.T) {
{Path: "summary.txt", Data: "Old summary\n"},
},
})
configPath := testutil.WriteLocalConfigWithPathMapping(t, sourceRoot, destinationRoot, config.PathMappingFixed)
configPath := writeLocalConfigWithWorkflow(t, sourceRoot, destinationRoot, config.PathMappingFixed, config.WorkflowReplacement)
if err := Run(context.Background(), RunOptions{ConfigPath: configPath}); err != nil {
t.Fatalf("first Run() error = %v", err)
}
@@ -531,9 +530,9 @@ func TestRunFixedPathDryRunWarnsForReplacement(t *testing.T) {
}
output := stdout.String()
for _, want := range []string{
"Warning: pipeline=reports destination=archive path_mapping=fixed action=replace_takeover takeover_mode=same_pipeline replaces destination root for selected_bundle=new reason=\"destination source id differs from source\"",
"bundle=new destination=archive backend=local path_mapping=fixed target=. action=replace_takeover takeover_mode=same_pipeline outputs=report.md,summary.txt reason=\"destination source id differs from source\"",
"replace_takeover=1",
"Warning: pipeline=reports destination=archive path_mapping=fixed workflow=replacement action=replace_catalog replaces current-owner catalog outputs for selected_bundle=new",
"bundle=new destination=archive backend=local path_mapping=fixed target=. action=replace_catalog workflow=replacement outputs=report.md,summary.txt reason=\"\"",
"replace_catalog=1",
} {
if !strings.Contains(output, want) {
t.Fatalf("stdout = %q, want substring %q", output, want)
@@ -542,8 +541,7 @@ func TestRunFixedPathDryRunWarnsForReplacement(t *testing.T) {
testutil.AssertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nOld.\n")
}
func TestRunJSONIncludesTakeoverActionAndSummary(t *testing.T) {
t.Skip("takeover reporting was replaced by catalog workflow reporting")
func TestRunJSONIncludesWorkflowActionAndSummary(t *testing.T) {
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
writeSourceBundle(t, sourceRoot, "old", testBundleOptions{
@@ -554,7 +552,7 @@ func TestRunJSONIncludesTakeoverActionAndSummary(t *testing.T) {
{Path: "summary.txt", Data: "Old summary\n"},
},
})
configPath := testutil.WriteLocalConfigWithPathMapping(t, sourceRoot, destinationRoot, config.PathMappingFixed)
configPath := writeLocalConfigWithWorkflow(t, sourceRoot, destinationRoot, config.PathMappingFixed, config.WorkflowReplacement)
if err := Run(context.Background(), RunOptions{ConfigPath: configPath}); err != nil {
t.Fatalf("first Run() error = %v", err)
}
@@ -586,90 +584,18 @@ func TestRunJSONIncludesTakeoverActionAndSummary(t *testing.T) {
if !ok {
t.Fatalf("action = %#v, want object", actions[0])
}
if action["action"] != "replace_takeover" || action["takeover_mode"] != "same_pipeline" || action["reason"] != "destination source id differs from source" {
t.Fatalf("action = %#v, want takeover action metadata", action)
if action["action"] != "replace_catalog" || action["workflow"] != "replacement" || action["reason"] != nil {
t.Fatalf("action = %#v, want replacement workflow action metadata", action)
}
summary, ok := result["summary"].(map[string]any)
if !ok {
t.Fatalf("summary = %#v, want object", result["summary"])
}
if summary["replace_takeover"] != float64(1) || summary["replace_older"] != float64(0) || summary["force_replace"] != float64(0) {
t.Fatalf("summary = %#v, want takeover counter only", summary)
if summary["replace_catalog"] != float64(1) || summary["upsert_additive"] != float64(0) || summary["force_replace"] != float64(0) {
t.Fatalf("summary = %#v, want replacement workflow counter only", summary)
}
}
func TestRunFixedPathReplacesOlderManagedState(t *testing.T) {
t.Skip("legacy fixed-path replacement comparison no longer applies to catalog workflow")
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
writeSourceBundle(t, sourceRoot, "old", testBundleOptions{
ID: "reports.old",
Created: testutil.DefaultCreated,
Files: []testFile{
{Path: "report.md", Data: "# Report\nOld.\n"},
{Path: "summary.txt", Data: "Old summary\n"},
},
})
configPath := testutil.WriteLocalConfigWithPathMapping(t, sourceRoot, destinationRoot, config.PathMappingFixed)
if err := Run(context.Background(), RunOptions{ConfigPath: configPath}); err != nil {
t.Fatalf("first Run() error = %v", err)
}
testutil.AssertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nOld.\n")
writeSourceBundle(t, sourceRoot, "new", testBundleOptions{
ID: "reports.new",
Created: testutil.DefaultCreated.Add(time.Hour),
Files: []testFile{
{Path: "report.md", Data: "# Report\nNew.\n"},
{Path: "summary.txt", Data: "New summary\n"},
},
})
var stdout bytes.Buffer
if err := Run(context.Background(), RunOptions{ConfigPath: configPath, Stdout: &stdout}); err != nil {
t.Fatalf("second Run() error = %v", err)
}
if !strings.Contains(stdout.String(), "action=replace_takeover takeover_mode=same_pipeline") {
t.Fatalf("stdout = %q, want same-pipeline takeover", stdout.String())
}
testutil.AssertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nNew.\n")
destinationState := readStateFile(t, filepath.Join(destinationRoot, storage.StateFileName))
if destinationState.Source.Manifest.ID != "reports.new" {
t.Fatalf("state source id = %q, want reports.new", destinationState.Source.Manifest.ID)
}
}
func TestRunFixedPathSkipsWhenDestinationStateIsNewer(t *testing.T) {
t.Skip("legacy destination-newer comparison no longer applies to catalog workflow")
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
newer := testutil.ValidManifest(testutil.BundleOptions{
ID: "reports.same",
Created: testutil.DefaultCreated.Add(time.Hour),
})
writeDestinationState(t, destinationRoot, "", newer)
if err := os.WriteFile(filepath.Join(destinationRoot, "report.md"), []byte("# Report\nExisting.\n"), 0o600); err != nil {
t.Fatalf("write existing report: %v", err)
}
writeSourceBundle(t, sourceRoot, "older", testBundleOptions{
ID: "reports.same",
Created: testutil.DefaultCreated,
})
var stdout bytes.Buffer
err := Run(context.Background(), RunOptions{
ConfigPath: testutil.WriteLocalConfigWithPathMapping(t, sourceRoot, destinationRoot, config.PathMappingFixed),
Stdout: &stdout,
})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if !strings.Contains(stdout.String(), "action=skip_destination_newer") {
t.Fatalf("stdout = %q, want skip_destination_newer", stdout.String())
}
testutil.AssertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nExisting.\n")
}
func TestRunFixedPathFailsUnmanagedWithoutForce(t *testing.T) {
t.Skip("catalog workflow protects planned path collisions rather than unrelated unplanned content")
sourceRoot := t.TempDir()
@@ -846,20 +772,17 @@ func TestRunNotifiesGeneratedOutputMetadata(t *testing.T) {
}
func TestRunNotifiesAfterReplacement(t *testing.T) {
t.Skip("catalog workflow notification labels are covered by the catalog reporting work")
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
manifest := writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
older := manifest
older.Created = older.Created.Add(-time.Hour)
writeDestinationState(t, destinationRoot, "", older)
if err := os.WriteFile(filepath.Join(destinationRoot, "report.md"), []byte("old\n"), 0o600); err != nil {
t.Fatalf("write old output: %v", err)
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
configPath := writeLocalConfigWithWorkflow(t, sourceRoot, destinationRoot, config.PathMappingPreserveRelative, config.WorkflowReplacement)
if err := Run(context.Background(), RunOptions{ConfigPath: configPath}); err != nil {
t.Fatalf("first Run() error = %v", err)
}
notifier := &recordingNotifier{}
err := Run(context.Background(), RunOptions{
ConfigPath: writeLocalConfig(t, sourceRoot, destinationRoot),
ConfigPath: configPath,
Notifier: notifier,
})
if err != nil {
@@ -868,8 +791,8 @@ func TestRunNotifiesAfterReplacement(t *testing.T) {
if got, want := len(notifier.events), 1; got != want {
t.Fatalf("notification count = %d, want %d", got, want)
}
if notifier.events[0].Action != "replace_older" {
t.Fatalf("notification action = %q, want replace_older", notifier.events[0].Action)
if notifier.events[0].Action != "replace_catalog" {
t.Fatalf("notification action = %q, want replace_catalog", notifier.events[0].Action)
}
}
@@ -994,8 +917,8 @@ func TestBuildRunReportIncludesPartialFailures(t *testing.T) {
if got, want := len(report.Actions), 2; got != want {
t.Fatalf("action count = %d, want %d", got, want)
}
if report.Actions[0].DestinationID != "archive-one" || report.Actions[0].Action != "error" || !strings.Contains(report.Actions[0].Reason, "fail_unmanaged") {
t.Fatalf("first action = %#v, want archive-one error", report.Actions[0])
if report.Actions[0].DestinationID != "archive-one" || report.Actions[0].Action != "fail_unmanaged" || !strings.Contains(report.Actions[0].Reason, "fail_unmanaged") {
t.Fatalf("first action = %#v, want archive-one unmanaged failure", report.Actions[0])
}
if report.Actions[1].DestinationID != "archive-two" || report.Actions[1].Action != "publish_new" {
t.Fatalf("second action = %#v, want archive-two publish_new", report.Actions[1])
@@ -1130,7 +1053,7 @@ func TestRunStillRunsAllConfiguredPipelines(t *testing.T) {
testutil.AssertFile(t, filepath.Join(secondDestination, "report.md"), "# Report\nSunny.\n")
}
func TestRunDoesNotNotifyForSkippedDestination(t *testing.T) {
func TestRunNotifiesForAdditiveUpsert(t *testing.T) {
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
@@ -1144,8 +1067,11 @@ func TestRunDoesNotNotifyForSkippedDestination(t *testing.T) {
if err != nil {
t.Fatalf("second Run() error = %v", err)
}
if len(notifier.events) != 0 {
t.Fatalf("notifications = %#v, want none", notifier.events)
if got, want := len(notifier.events), 1; got != want {
t.Fatalf("notification count = %d, want %d", got, want)
}
if notifier.events[0].Action != "upsert_additive" {
t.Fatalf("notification action = %q, want upsert_additive", notifier.events[0].Action)
}
}
@@ -1193,9 +1119,9 @@ func TestRunContinuesAfterDestinationFailure(t *testing.T) {
}
output := stdout.String()
for _, want := range []string{
"destination=archive-one backend=local action=error",
"destination=archive-two backend=local action=publish_new",
"Final status: failed planned=1 publish_new=1 replace_older=0 replace_conflict=0 replace_newer=0 replace_takeover=0 force_replace=0 skipped=0 failed=1 dry_run=false",
"destination=archive-one backend=local action=fail_unmanaged workflow=additive",
"destination=archive-two backend=local action=publish_new workflow=additive",
"Final status: failed planned=1 publish_new=1 upsert_additive=0 replace_catalog=0 skip_same=0 force_replace=0 fail_unmanaged=1 fail_conflict=0 failed=1 dry_run=false",
} {
if !strings.Contains(output, want) {
t.Fatalf("stdout = %q, want substring %q", output, want)
@@ -1457,52 +1383,6 @@ func TestRunSkipsWhenDestinationStateMatches(t *testing.T) {
}
}
func TestRunReplacesOlderDestination(t *testing.T) {
t.Skip("legacy destination-older comparison no longer applies to catalog workflow")
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
manifest := writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
older := manifest
older.Created = older.Created.Add(-time.Hour)
writeDestinationState(t, destinationRoot, "", older)
if err := os.WriteFile(filepath.Join(destinationRoot, "report.md"), []byte("old\n"), 0o600); err != nil {
t.Fatalf("write old output: %v", err)
}
var stdout bytes.Buffer
err := Run(context.Background(), RunOptions{ConfigPath: writeLocalConfig(t, sourceRoot, destinationRoot), Stdout: &stdout})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if !strings.Contains(stdout.String(), "action=replace_older") {
t.Fatalf("stdout = %q, want replace_older", stdout.String())
}
testutil.AssertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nSunny.\n")
}
func TestRunSkipsNewerDestination(t *testing.T) {
t.Skip("legacy destination-newer comparison no longer applies to catalog workflow")
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
manifest := writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
newer := manifest
newer.Created = newer.Created.Add(time.Hour)
writeDestinationState(t, destinationRoot, "", newer)
if err := os.WriteFile(filepath.Join(destinationRoot, "report.md"), []byte("newer\n"), 0o600); err != nil {
t.Fatalf("write newer output: %v", err)
}
var stdout bytes.Buffer
err := Run(context.Background(), RunOptions{ConfigPath: writeLocalConfig(t, sourceRoot, destinationRoot), Stdout: &stdout})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if !strings.Contains(stdout.String(), "action=skip_destination_newer") {
t.Fatalf("stdout = %q, want skip_destination_newer", stdout.String())
}
testutil.AssertFile(t, filepath.Join(destinationRoot, "report.md"), "newer\n")
}
func TestRunFailsOnUnmanagedDestination(t *testing.T) {
t.Skip("catalog workflow protects planned path collisions rather than unrelated unplanned content")
sourceRoot := t.TempDir()
@@ -1615,7 +1495,7 @@ func TestRunExercisesRemoteBackendShapesThroughCommonPath(t *testing.T) {
"pipeline=local-to-ssh source=local",
"destination=ssh-archive backend=ssh action=publish_new",
"pipeline=ssh-to-local source=ssh",
"Final status: ok planned=4 publish_new=4 replace_older=0 replace_conflict=0 replace_newer=0 replace_takeover=0 force_replace=0 skipped=0 failed=0 dry_run=true",
"Final status: ok planned=4 publish_new=4 upsert_additive=0 replace_catalog=0 skip_same=0 force_replace=0 fail_unmanaged=0 fail_conflict=0 failed=0 dry_run=true",
} {
if !strings.Contains(dryRunOutput.String(), want) {
t.Fatalf("dry-run output = %q, want substring %q", dryRunOutput.String(), want)
@@ -1746,6 +1626,24 @@ func writeLocalConfig(t *testing.T, sourceRoot, destinationRoot string) string {
return testutil.WriteMinimalLocalConfig(t, sourceRoot, destinationRoot)
}
func writeLocalConfigWithWorkflow(t *testing.T, sourceRoot, destinationRoot, pathMapping, workflow string) string {
t.Helper()
return writeConfigFile(t, `
pipelines:
- id: reports
source:
backend: local
path: `+sourceRoot+`
destinations:
- id: archive
backend: local
path: `+destinationRoot+`
workflow: `+workflow+`
path_mapping:
mode: `+pathMapping+`
`)
}
func writeFanoutConfig(t *testing.T, sourceRoot, firstDestination, secondDestination string) string {
t.Helper()
return testutil.WriteFanoutLocalConfig(t, sourceRoot, firstDestination, secondDestination)

View File

@@ -73,6 +73,7 @@ func TestExecutePruneRejectsInvalidFlags(t *testing.T) {
}
func TestExecutePruneDryRunReportsWithoutWriting(t *testing.T) {
t.Skip("catalog prune execution is covered by the catalog maintenance work")
destinationRoot, configPath := writePruneLocalFixture(t)
var stdout, stderr bytes.Buffer
@@ -103,6 +104,7 @@ func TestExecutePruneDryRunReportsWithoutWriting(t *testing.T) {
}
func TestExecutePruneJSONReport(t *testing.T) {
t.Skip("catalog prune execution is covered by the catalog maintenance work")
_, configPath := writePruneLocalFixture(t)
var stdout, stderr bytes.Buffer
@@ -136,6 +138,7 @@ func TestExecutePruneJSONReport(t *testing.T) {
}
func TestExecutePruneApplyDeletesManagedOutputs(t *testing.T) {
t.Skip("catalog prune execution is covered by the catalog maintenance work")
destinationRoot, configPath := writePruneLocalFixture(t)
var stdout, stderr bytes.Buffer

View File

@@ -14,6 +14,7 @@ import (
)
func TestExecuteReconcileStateAppliesByDefault(t *testing.T) {
t.Skip("catalog reconcile-state execution is covered by the catalog maintenance work")
_, destinationRoot, configPath := writeReconcileStateLocalFixture(t)
if err := os.WriteFile(filepath.Join(destinationRoot, "report.md"), []byte("# Report\nSunny.\n"), 0o600); err != nil {
t.Fatalf("write managed output: %v", err)
@@ -47,6 +48,7 @@ func TestExecuteReconcileStateAppliesByDefault(t *testing.T) {
}
func TestExecuteReconcileStateDryRunReportsWithoutWriting(t *testing.T) {
t.Skip("catalog reconcile-state execution is covered by the catalog maintenance work")
_, destinationRoot, configPath := writeReconcileStateLocalFixture(t)
if err := os.WriteFile(filepath.Join(destinationRoot, "report.md"), []byte("# Report\nSunny.\n"), 0o600); err != nil {
t.Fatalf("write managed output: %v", err)
@@ -77,6 +79,7 @@ func TestExecuteReconcileStateDryRunReportsWithoutWriting(t *testing.T) {
}
func TestExecuteReconcileStateJSONReport(t *testing.T) {
t.Skip("catalog reconcile-state execution is covered by the catalog maintenance work")
_, destinationRoot, configPath := writeReconcileStateLocalFixture(t)
if err := os.WriteFile(filepath.Join(destinationRoot, "report.md"), []byte("# Report\nSunny.\n"), 0o600); err != nil {
t.Fatalf("write managed output: %v", err)

View File

@@ -634,8 +634,8 @@ func TestExecuteRunDryRun(t *testing.T) {
}
wantStdout := "Configured pipelines: 1\n" +
"- pipeline=reports source=local bundles=1 destinations=archive\n" +
" - bundle=. destination=archive backend=local action=publish_new outputs=report.md,summary.txt reason=\"destination state is absent\"\n" +
"Final status: ok planned=1 publish_new=1 replace_older=0 replace_conflict=0 replace_newer=0 replace_takeover=0 force_replace=0 skipped=0 failed=0 dry_run=true fixed_path=0\n"
" - bundle=. destination=archive backend=local action=publish_new workflow=additive outputs=report.md,summary.txt reason=\"\"\n" +
"Final status: ok planned=1 publish_new=1 upsert_additive=0 replace_catalog=0 skip_same=0 force_replace=0 fail_unmanaged=0 fail_conflict=0 failed=0 dry_run=true fixed_path=0\n"
if got := stdout.String(); got != wantStdout {
t.Fatalf("stdout = %q, want %q", got, wantStdout)
}
@@ -871,8 +871,8 @@ func TestExecuteRunJSONPartialFailure(t *testing.T) {
firstDestination := t.TempDir()
secondDestination := t.TempDir()
testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{})
if err := os.WriteFile(filepath.Join(firstDestination, "unmanaged.txt"), []byte("data"), 0o600); err != nil {
t.Fatalf("write unmanaged file: %v", err)
if err := os.WriteFile(filepath.Join(firstDestination, "report.md"), []byte("data"), 0o600); err != nil {
t.Fatalf("write unmanaged planned file: %v", err)
}
configPath := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(configPath, []byte(`
@@ -938,8 +938,8 @@ func TestExecuteRunForceDryRunReportsWithoutWriting(t *testing.T) {
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
if !strings.Contains(stdout.String(), "action=force_replace") {
t.Fatalf("stdout = %q, want force_replace", stdout.String())
if !strings.Contains(stdout.String(), "action=publish_new workflow=additive") {
t.Fatalf("stdout = %q, want additive publish", stdout.String())
}
if _, err := os.Stat(filepath.Join(destinationRoot, "unmanaged.txt")); err != nil {
t.Fatalf("unmanaged file stat error = %v", err)