Skip to content
Open
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
138 changes: 138 additions & 0 deletions rest-api/flow/internal/task/conflict/conflict_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import (
"github.com/NVIDIA/infra-controller/rest-api/flow/pkg/common/devicetypes"
)

func makeOp(opType taskcommon.TaskType, code string) operation.Wrapper {
return operation.Wrapper{Type: opType, Code: code}
}

// makeTask builds a minimal Task for conflict tests.
// Component UUIDs, when provided, are stored under ComponentTypeCompute.
// Pass no UUIDs to get a task with nil ComponentsByType (old-task fallback).
Expand Down Expand Up @@ -459,6 +463,33 @@ func TestBuiltinRule(t *testing.T) {
},
expected: false,
},
{
// decommission uses a wildcard B entry so it blocks every
// operation type, including ones not explicitly listed in the
// rule (such as inject_expectation).
name: "decommission blocks unlisted operation type (wildcard B)",
incoming: makeTask(
rackID, taskcommon.TaskTypeInjectExpectation, "inject",
),
activeTasks: []*taskdef.Task{
makeTask(rackID,
taskcommon.TaskTypeDecommission, "decommission"),
},
expected: true,
},
{
// Symmetric: an incoming decommission blocks an active unlisted
// operation type via the wildcard.
name: "unlisted operation type blocked by incoming decommission (wildcard B symmetric)",
incoming: makeTask(
rackID, taskcommon.TaskTypeDecommission, "decommission",
),
activeTasks: []*taskdef.Task{
makeTask(rackID,
taskcommon.TaskTypeInjectExpectation, "inject"),
},
expected: true,
},
// --- Component-type-specific entries ---
{
// PowerShelf power cuts rack power → blocks any power op
Expand Down Expand Up @@ -682,3 +713,110 @@ func TestResolver_HasConflict(t *testing.T) {
})
}
}

func TestResolver_HasScheduleConflict(t *testing.T) {
// HasScheduleConflict is a coarse-grained check that operates on
// operation.Wrapper values (type + code) without component-level detail.
// It uses the same builtinRule ConflictingPairs as HasConflict, but
// skips component-type and component-UUID checks.

tests := []struct {
name string
incoming operation.Wrapper
existing []operation.Wrapper
expected bool
}{
{
name: "no existing schedules — no conflict",
incoming: makeOp(taskcommon.TaskTypePowerControl, "power_on"),
existing: nil,
expected: false,
},
{
name: "empty existing schedules — no conflict",
incoming: makeOp(taskcommon.TaskTypePowerControl, "power_on"),
existing: []operation.Wrapper{},
expected: false,
},
{
// bring_up has wildcard B so it conflicts with every operation
// type, including power_control.
name: "bring_up conflicts with power",
incoming: makeOp(taskcommon.TaskTypePowerControl, "power_on"),
existing: []operation.Wrapper{
makeOp(taskcommon.TaskTypeBringUp, "bring_up"),
},
expected: true,
},
{
// Symmetric: incoming bring_up conflicts with active firmware.
name: "bring_up conflicts with firmware (symmetric)",
incoming: makeOp(taskcommon.TaskTypeBringUp, "bring_up"),
existing: []operation.Wrapper{
makeOp(taskcommon.TaskTypeFirmwareControl, "upgrade"),
},
expected: true,
},
{
// decommission has wildcard B so it conflicts with every
// operation type.
name: "decommission conflicts with firmware",
incoming: makeOp(taskcommon.TaskTypeDecommission, "decommission"),
existing: []operation.Wrapper{
makeOp(taskcommon.TaskTypeFirmwareControl, "upgrade"),
},
expected: true,
},
{
// decommission wildcard covers operation types not explicitly
// listed in the rule (such as inject_expectation).
name: "decommission conflicts with unlisted operation type (wildcard B)",
incoming: makeOp(taskcommon.TaskTypeInjectExpectation, "inject"),
existing: []operation.Wrapper{
makeOp(taskcommon.TaskTypeDecommission, "decommission"),
},
expected: true,
},
{
// inject_expectation appears in no ConflictingPairs entry, so it
// does not conflict with power or firmware when those are not
// covered by a wildcard-B rule it sits on the other side of.
name: "inject_expectation does not conflict with power",
incoming: makeOp(taskcommon.TaskTypeInjectExpectation, "inject"),
existing: []operation.Wrapper{
makeOp(taskcommon.TaskTypePowerControl, "power_on"),
},
expected: false,
},
{
// Two inject_expectation schedules are always compatible.
name: "inject_expectation does not conflict with itself",
incoming: makeOp(taskcommon.TaskTypeInjectExpectation, "inject"),
existing: []operation.Wrapper{
makeOp(taskcommon.TaskTypeInjectExpectation, "inject"),
},
expected: false,
},
{
// Only one of the existing schedules needs to conflict.
name: "conflict detected when one of multiple existing schedules matches",
incoming: makeOp(taskcommon.TaskTypePowerControl, "power_on"),
existing: []operation.Wrapper{
makeOp(taskcommon.TaskTypeInjectExpectation, "inject"),
makeOp(taskcommon.TaskTypeBringUp, "bring_up"),
},
expected: true,
},
}

resolver := NewResolver(newMockStore())

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(
t, tc.expected,
resolver.HasScheduleConflict(tc.incoming, tc.existing),
)
})
}
}
Loading