Implement catalog force replacement
This commit is contained in:
@@ -597,7 +597,6 @@ func TestRunJSONIncludesWorkflowActionAndSummary(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunFixedPathFailsUnmanagedWithoutForce(t *testing.T) {
|
||||
t.Skip("catalog workflow protects planned path collisions rather than unrelated unplanned content")
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "bundle", testBundleOptions{})
|
||||
@@ -612,7 +611,6 @@ func TestRunFixedPathFailsUnmanagedWithoutForce(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunFixedPathForceReplacementStaysWithinDestinationRoot(t *testing.T) {
|
||||
t.Skip("force reporting and execution behavior is covered by the catalog reporting work")
|
||||
sourceRoot := t.TempDir()
|
||||
parent := t.TempDir()
|
||||
destinationRoot := filepath.Join(parent, "latest")
|
||||
@@ -639,6 +637,10 @@ func TestRunFixedPathForceReplacementStaysWithinDestinationRoot(t *testing.T) {
|
||||
if _, err := os.Stat(filepath.Join(destinationRoot, "unmanaged.txt")); !os.IsNotExist(err) {
|
||||
t.Fatalf("unmanaged stat error = %v, want removed", err)
|
||||
}
|
||||
catalog := readLocalCatalogState(t, destinationRoot)
|
||||
if catalog.SchemaVersion != state.CatalogSchemaVersion || catalog.State.Mode != state.StateModeCatalog {
|
||||
t.Fatalf("catalog identity = schema %d mode %s", catalog.SchemaVersion, catalog.State.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFixedPathRemoteBackendsUseBackendRoots(t *testing.T) {
|
||||
@@ -1384,7 +1386,6 @@ func TestRunSkipsWhenDestinationStateMatches(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunFailsOnUnmanagedDestination(t *testing.T) {
|
||||
t.Skip("catalog workflow protects planned path collisions rather than unrelated unplanned content")
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
||||
@@ -1399,7 +1400,6 @@ func TestRunFailsOnUnmanagedDestination(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunForceReplacesUnmanagedDestination(t *testing.T) {
|
||||
t.Skip("force reporting and execution behavior is covered by the catalog reporting work")
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
||||
@@ -1419,10 +1419,50 @@ func TestRunForceReplacesUnmanagedDestination(t *testing.T) {
|
||||
if !strings.Contains(stdout.String(), "action=force_replace") {
|
||||
t.Fatalf("stdout = %q, want force_replace", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Final status: ok planned=1 publish_new=0 upsert_additive=0 replace_catalog=0 skip_same=0 force_replace=1 fail_unmanaged=0 fail_conflict=0 failed=0 dry_run=false") {
|
||||
t.Fatalf("stdout = %q, want force_replace counter only", stdout.String())
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(destinationRoot, "unmanaged.txt")); !os.IsNotExist(err) {
|
||||
t.Fatalf("unmanaged file stat error = %v, want not exist", err)
|
||||
}
|
||||
testutil.AssertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nSunny.\n")
|
||||
catalog := readLocalCatalogState(t, destinationRoot)
|
||||
if catalog.SchemaVersion != state.CatalogSchemaVersion || catalog.State.Mode != state.StateModeCatalog {
|
||||
t.Fatalf("catalog identity = schema %d mode %s", catalog.SchemaVersion, catalog.State.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunForceReplacementJSONOutput(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
||||
if err := os.WriteFile(filepath.Join(destinationRoot, "unmanaged.txt"), []byte("old"), 0o600); err != nil {
|
||||
t.Fatalf("write unmanaged file: %v", err)
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
err := Run(context.Background(), RunOptions{
|
||||
ConfigPath: writeLocalConfig(t, sourceRoot, destinationRoot),
|
||||
Force: true,
|
||||
Stdout: &stdout,
|
||||
OutputFormat: OutputFormatJSON,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
result := decodeAppResult(t, stdout.String())
|
||||
actions, ok := result["actions"].([]any)
|
||||
if !ok || len(actions) != 1 {
|
||||
t.Fatalf("actions = %#v, want one action", result["actions"])
|
||||
}
|
||||
action, ok := actions[0].(map[string]any)
|
||||
if !ok || action["action"] != "force_replace" || action["workflow"] != "additive" {
|
||||
t.Fatalf("action = %#v, want force_replace additive", actions[0])
|
||||
}
|
||||
summary, ok := result["summary"].(map[string]any)
|
||||
if !ok || summary["force_replace"] != float64(1) || summary["publish_new"] != float64(0) {
|
||||
t.Fatalf("summary = %#v, want force_replace only", result["summary"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFansOutToLocalDestinations(t *testing.T) {
|
||||
@@ -1527,7 +1567,6 @@ func TestRunExercisesRemoteBackendShapesThroughCommonPath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunForceReplacementStaysWithinRemoteBundlePaths(t *testing.T) {
|
||||
t.Skip("force reporting and execution behavior is covered by the catalog reporting work")
|
||||
localSourceRoot := t.TempDir()
|
||||
writeSourceBundle(t, localSourceRoot, "bundle", testBundleOptions{})
|
||||
s3Destination := fake.New()
|
||||
@@ -1569,6 +1608,12 @@ func TestRunForceReplacementStaysWithinRemoteBundlePaths(t *testing.T) {
|
||||
testutil.AssertFakeFile(t, sshDestination, "bundle/report.md", "# Report\nSunny.\n")
|
||||
testutil.AssertFakeMissing(t, sshDestination, "bundle/old.txt")
|
||||
testutil.AssertFakeFile(t, sshDestination, "bundle-sibling/keep.txt", "keep")
|
||||
for name, backend := range map[string]*fake.Backend{"s3": s3Destination, "ssh": sshDestination} {
|
||||
catalog := readFakeCatalogStateAt(t, backend, "bundle")
|
||||
if catalog.SchemaVersion != state.CatalogSchemaVersion || catalog.State.Mode != state.StateModeCatalog {
|
||||
t.Fatalf("%s catalog identity = schema %d mode %s", name, catalog.SchemaVersion, catalog.State.Mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDryRunDoesNotWrite(t *testing.T) {
|
||||
@@ -1772,6 +1817,36 @@ func readStateFile(t *testing.T, path string) testDestinationState {
|
||||
return view
|
||||
}
|
||||
|
||||
func readLocalCatalogState(t *testing.T, destinationRoot string) state.CatalogState {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(filepath.Join(destinationRoot, storage.StateFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("read catalog state: %v", err)
|
||||
}
|
||||
catalog, err := state.ParseCatalog(data)
|
||||
if err != nil {
|
||||
t.Fatalf("parse catalog state: %v", err)
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
func readFakeCatalogStateAt(t *testing.T, backend *fake.Backend, bundlePath string) state.CatalogState {
|
||||
t.Helper()
|
||||
statePath, err := storage.StatePath(bundlePath)
|
||||
if err != nil {
|
||||
t.Fatalf("state path: %v", err)
|
||||
}
|
||||
data, err := backend.ReadFile(context.Background(), statePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read catalog state: %v", err)
|
||||
}
|
||||
catalog, err := state.ParseCatalog(data)
|
||||
if err != nil {
|
||||
t.Fatalf("parse catalog state: %v", err)
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
func outputsByPath(outputs []testStateOutput) map[string]testStateOutput {
|
||||
byPath := make(map[string]testStateOutput, len(outputs))
|
||||
for _, output := range outputs {
|
||||
|
||||
Reference in New Issue
Block a user