From 6c08749849b9c3e3455a28d5e8746aa5eceacbc5 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Sat, 22 Aug 2026 10:40:22 -0700 Subject: [PATCH] test(blackbox): skip a CloudCreate that reuses a tracked native id The generator draws out-of-band native ids from a small pool, so an id can repeat within a sequence. Re-creating a cloud entry the model already tracks as present is an out-of-band modification of that resource, not a creation: discovery only ingests native ids inventory does not know, so the fresh CloudProperties expectation armed by the repeated create can never converge through the discovery path and the trigger exhausts its retries. Out-of-band modification of unmanaged resources is OpCloudModify's surface; the repeated create is skipped. Also dump the unconverged expectation violations when a discovery trigger gives up, so the next such failure names the offending rows. --- tests/blackbox/executor.go | 15 +++++++ tests/blackbox/unmanaged_recreate_test.go | 49 +++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/blackbox/unmanaged_recreate_test.go diff --git a/tests/blackbox/executor.go b/tests/blackbox/executor.go index 68e315482..e5a299d98 100644 --- a/tests/blackbox/executor.go +++ b/tests/blackbox/executor.go @@ -1768,6 +1768,11 @@ func (h *TestHarness) executeTriggerDiscovery(t *testing.T, model *StateModel) { } t.Logf("TriggerDiscovery: inventory not converged (attempt %d)", attempt+1) } + if _, unmanagedInventory, err := h.extractManagedAndUnmanagedInventory(); err == nil { + for _, v := range CheckUnmanagedModelVsInventory(model, unmanagedInventory) { + t.Logf("TriggerDiscovery: unconverged: %s", v.Message) + } + } require.Failf(t, "discovery did not ingest", "expected unmanaged resources were not ingested after %d attempts", maxDiscoverAttempts) } @@ -1954,6 +1959,16 @@ func (h *TestHarness) waitForAbsorbedInventory(t *testing.T, query, nativeID, ex func (h *TestHarness) executeCloudCreate(t *testing.T, op *Operation, model *StateModel) { t.Helper() + // The generator draws native ids from a small pool, so an id can repeat + // within a sequence. Re-creating an entry the model already tracks as + // present is an out-of-band MODIFY of that resource, not a create: + // discovery only ingests unknown native ids, so it can never converge + // the model's fresh CloudProperties with the already-ingested row (that + // is sync absorption, which OpCloudModify exercises). Skip the op. + if res := model.UnmanagedResources[op.NativeID]; res != nil && (res.PresentInCloud || res.PresentInInventory) { + t.Logf("[op %d] CloudCreate: %s → skipped (already present out-of-band)", op.SequenceNum, op.NativeID) + return + } h.putCloudStateWithRetry(t, op.NativeID, op.ResourceType, op.Properties) model.ApplyUnmanagedCloudCreate(op.NativeID, op.ResourceType, op.Properties) t.Logf("[op %d] CloudCreate: %s (%s)", op.SequenceNum, op.NativeID, op.ResourceType) diff --git a/tests/blackbox/unmanaged_recreate_test.go b/tests/blackbox/unmanaged_recreate_test.go new file mode 100644 index 000000000..e47fed6b4 --- /dev/null +++ b/tests/blackbox/unmanaged_recreate_test.go @@ -0,0 +1,49 @@ +// © 2026 Platform Engineering Labs Inc. +// +// SPDX-License-Identifier: FSL-1.1-ALv2 + +//go:build integration || property + +package blackbox + +import ( + "testing" + "time" + + "github.com/platform-engineering-labs/formae/internal/metastructure/testutil" + "github.com/stretchr/testify/require" +) + +// A CloudCreate that reuses the native id of an already-ingested unmanaged +// resource is skipped: discovery only ingests unknown native ids, so fresh +// CloudProperties expectations for a known id could never converge through +// the discovery path. After the skip, a discovery trigger must still +// converge cleanly. +func TestCloudCreate_RepeatedNativeIDIsSkipped(t *testing.T) { + testutil.RunTestFromProjectRoot(t, func(t *testing.T) { + h := NewTestHarness(t, 30*time.Second) + defer h.Cleanup() + h.ResetAgentState(t) + config := PropertyTestConfig{ResourceCount: 10, StackCount: 1} + model := NewStateModel(config.StackCount, config.ResourceCount) + h.SetupStacks(t, model, config) + + create := Operation{Kind: OpCloudCreate, NativeID: "cloud-1", + ResourceType: "Test::Generic::Resource", + Properties: `{"Name":"cp-1","Value":"v1","SetTags":[],"EntityTags":[],"OrderedItems":[]}`} + h.ExecuteOperation(t, &create, model) + h.executeTriggerDiscovery(t, model) + require.True(t, model.UnmanagedResources["cloud-1"].PresentInInventory) + ingested := model.UnmanagedResources["cloud-1"].CloudProperties + + recreate := Operation{Kind: OpCloudCreate, NativeID: "cloud-1", + ResourceType: "Test::Generic::Resource", + Properties: `{"Name":"cp-other","Value":"v2","SetTags":[],"EntityTags":[],"OrderedItems":[]}`} + h.ExecuteOperation(t, &recreate, model) + + require.Equal(t, ingested, model.UnmanagedResources["cloud-1"].CloudProperties, + "a repeated CloudCreate must not rewrite the tracked cloud properties") + require.True(t, h.waitForUnmanagedInventoryExpectations(t, model, 10*time.Second), + "the unmanaged inventory must still converge after the skipped re-create") + }) +}