Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions tests/blackbox/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1008,15 +1008,6 @@ func correctModelFromCommandOutcome(t *testing.T, cmd *apimodel.Command, model *
if model.IsAuthoritativeSlot(key.stackIdx, key.slotIdx) {
continue
}
// Cross-stack slots in failed/canceled commands: the agent's
// behavior for cross-stack resources is non-deterministic from
// the command response alone (creates may or may not persist,
// reconcile deletes may or may not complete before cancel).
// Skip model updates for cross-stack slots and rely on the
// model-vs-inventory check excluding them (see CheckModelVsInventory).
if pool != nil && pool.IsCrossStack(key.slotIdx) {
continue
}
res := model.Resource(key.stackIdx, key.slotIdx)
if res == nil || res.State == snap.State {
continue
Expand Down
11 changes: 0 additions & 11 deletions tests/blackbox/invariants.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,13 +503,6 @@ func CheckModelVsInventory(model *StateModel, inventory []pkgmodel.Resource) []V
for s := range model.Stacks {
stack := &model.Stacks[s]
for idx, res := range stack.Resources {
// Cross-stack slots in failed/canceled commands have
// non-deterministic persistence behavior — the command response
// alone can't tell us whether they were persisted. Skip them
// in model-vs-inventory assertions.
if model.Pool != nil && model.Pool.IsCrossStack(idx) {
continue
}
label := model.LabelForResource(s, idx)
resourceType := model.TypeForResource(idx)

Expand Down Expand Up @@ -572,10 +565,6 @@ func CheckModelVsInventory(model *StateModel, inventory []pkgmodel.Resource) []V
if expectedExistingKeys[key] {
continue
}
_, slotIdx, ok := model.findResourceSlot(res.Stack, res.Label)
if ok && model.Pool != nil && model.Pool.IsCrossStack(slotIdx) {
continue
}
violations = append(violations, Violation{
Kind: ViolationModelInventoryMismatch,
Message: fmt.Sprintf("inventory contains unexpected managed resource: stack %s resource %s type=%s",
Expand Down
83 changes: 83 additions & 0 deletions tests/blackbox/state_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package blackbox

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -502,6 +503,88 @@ func TestStateModel_DriftEligibilitySkipsStacksWithInFlightCommands(t *testing.T
// Cross-stack slots reference a parent on the provider stack (stack 0), so an
// in-flight command on the provider stack can cascade onto them. They are only
// eligible for drift while the provider stack is quiescent too.
// findCrossStackSlot returns a cross-stack slot index of the model's pool.
func findCrossStackSlot(t *testing.T, model *StateModel) int {
t.Helper()
require.NotNil(t, model.Pool)
for i := range model.Pool.Slots {
if model.Pool.IsCrossStack(i) {
return i
}
}
t.Fatal("pool has no cross-stack slot")
return -1
}

// Cross-stack slots are asserted against inventory like any other slot: a
// slot the model expects to exist must have an inventory row.
func TestCheckModelVsInventory_CrossStackSlotIsAsserted(t *testing.T) {
model := NewStateModel(2, 10)
crossIdx := findCrossStackSlot(t, model)
model.ApplyCreated(1, []int{crossIdx}, `{"Name":"x","ParentId":"p","Value":"v1"}`)

violations := CheckModelVsInventory(model, nil)

found := false
label := model.LabelForResource(1, crossIdx)
for _, v := range violations {
if v.Kind == ViolationModelInventoryMismatch && strings.Contains(v.Message, label) {
found = true
}
}
assert.True(t, found, "a missing cross-stack inventory row must be reported, not skipped")
}

// The reverse direction holds too: an inventory row for a cross-stack slot
// the model does not expect is an unexpected managed resource.
func TestCheckModelVsInventory_UnexpectedCrossStackRowIsAsserted(t *testing.T) {
model := NewStateModel(2, 10)
crossIdx := findCrossStackSlot(t, model)
label := model.LabelForResource(1, crossIdx)

inventory := []pkgmodel.Resource{{
Stack: "stack-1",
Label: label,
Type: model.TypeForResource(crossIdx),
NativeID: "test-9",
Managed: true,
Properties: []byte(`{"Name":"x","ParentId":"p","Value":"v1"}`),
}}

violations := CheckModelVsInventory(model, inventory)

found := false
for _, v := range violations {
if v.Kind == ViolationModelInventoryMismatch && strings.Contains(v.Message, "unexpected managed resource") && strings.Contains(v.Message, label) {
found = true
}
}
assert.True(t, found, "an unexpected cross-stack inventory row must be reported, not skipped")
}

// A failed command's unmentioned cross-stack slot reverts to its snapshot
// like any other slot: the optimistic prediction must not survive a command
// that never reported an outcome for it.
func TestCorrectModelFromCommandOutcome_UnmentionedCrossStackSlotReverts(t *testing.T) {
model := NewStateModel(2, 10)
crossIdx := findCrossStackSlot(t, model)

// Snapshot taken while the slot did not exist, then an optimistic create.
snapshots := []ResourceSnapshot{{StackIndex: 1, SlotIndex: crossIdx, State: StateNotExist}}
model.ApplyCreated(1, []int{crossIdx}, `{"Name":"x","ParentId":"p","Value":"v1"}`)

cmd := &apimodel.Command{
CommandID: "cmd-failed",
State: "Failed",
ResourceUpdates: nil, // the command died before reaching this slot
}
corrected := map[struct{ stackIdx, slotIdx int }]bool{}
correctModelFromCommandOutcome(t, cmd, model, model.Pool, snapshots, corrected, false)

require.Equal(t, StateNotExist, model.Resource(1, crossIdx).State,
"an unmentioned cross-stack slot in a failed command reverts to its snapshot")
}

func TestStateModel_DriftEligibilityCrossStackNeedsQuiescentProvider(t *testing.T) {
model := NewStateModel(2, 10)
require.NotNil(t, model.Pool)
Expand Down
Loading