feat: add contextual Target actions
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package app_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -53,6 +54,24 @@ func TestApplicationSnapshotStructuresContextualTargetActions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationSnapshotMakesUnknownTargetStateStructurallyUnavailable(t *testing.T) {
|
||||
adapter := &fakeTarget{available: true}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
createAndActivate(t, application)
|
||||
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
got := application.Snapshot().Targets[0].PrimaryAction
|
||||
const reason = "Target actions are unavailable because this Target state is unknown"
|
||||
if got.Kind != 0 || got.Available || got.DisabledReason != reason {
|
||||
t.Fatalf("unknown-state action = %#v, want structurally unavailable with reason %q", got, reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationDispatchesExplicitContextualTargetIntents(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -132,6 +151,57 @@ func TestApplicationReapplyPublishesTruthfulDriftWhenAdapterCannotConverge(t *te
|
||||
}
|
||||
}
|
||||
|
||||
type changingAvailabilityTarget struct {
|
||||
*fakeTarget
|
||||
detections int
|
||||
}
|
||||
|
||||
func (f *changingAvailabilityTarget) Detect(context.Context, *profile.Profile) target.Detection {
|
||||
f.detections++
|
||||
available := f.detections == 1
|
||||
return target.Detection{
|
||||
Availability: target.Availability{Available: available, Evidence: map[bool]string{true: "available", false: "no longer available"}[available]},
|
||||
Inspection: target.Inspection{State: target.External, Evidence: "actual state"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationRechecksAvailabilityImmediatelyBeforeTargetMutation(t *testing.T) {
|
||||
base := &fakeTarget{available: true, state: target.External}
|
||||
adapter := &changingAvailabilityTarget{fakeTarget: base}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
createAndActivate(t, application)
|
||||
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
if before := application.Snapshot().Targets[0]; !before.Available || !before.PrimaryAction.Available {
|
||||
t.Fatalf("initial detection = %#v, want available action", before)
|
||||
}
|
||||
|
||||
result := application.Dispatch(app.Intent{Kind: app.ApplyProfileToTarget, TargetID: adapter.ID()})
|
||||
const reason = "Target actions are unavailable because this Target is unavailable"
|
||||
if result.Err == nil || result.Err.Error() != reason || len(base.calls) != 0 {
|
||||
t.Fatalf("available-to-unavailable result = %#v, SetDesired calls = %#v", result, base.calls)
|
||||
}
|
||||
if got := result.Snapshot.Targets[0]; got.Available || got.AvailabilityEvidence != "no longer available" || got.PrimaryAction.Available || got.PrimaryAction.DisabledReason != reason {
|
||||
t.Fatalf("published targeted detection = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
type failedDetectionTarget struct {
|
||||
*fakeTarget
|
||||
}
|
||||
|
||||
func (f failedDetectionTarget) Detect(context.Context, *profile.Profile) target.Detection {
|
||||
return target.Detection{
|
||||
Availability: target.Availability{Available: true, Evidence: "detected"},
|
||||
Inspection: target.Inspection{State: f.state, Evidence: "actual state"},
|
||||
Err: errors.New("detection failed"),
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationRejectsStructuredUnavailableActionWithoutAdapterMutation(t *testing.T) {
|
||||
adapter := &fakeTarget{available: false, state: target.External}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
@@ -153,18 +223,88 @@ func TestApplicationRejectsStructuredUnavailableActionWithoutAdapterMutation(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationRejectsProfileDependentIntentWithoutActiveProfile(t *testing.T) {
|
||||
adapter := &fakeTarget{available: true, state: target.External}
|
||||
func TestApplicationValidatesEveryStructuredDisabledReasonBeforeAdapterMutation(t *testing.T) {
|
||||
assertRejected := func(t *testing.T, application *app.Application, adapter *fakeTarget, intent app.Intent, reason string) {
|
||||
t.Helper()
|
||||
adapter.calls = nil
|
||||
result := application.Dispatch(intent)
|
||||
if result.Err == nil || !strings.Contains(result.Err.Error(), reason) || len(adapter.calls) != 0 {
|
||||
t.Fatalf("disabled action result = %#v, calls = %#v, want reason containing %q", result, adapter.calls, reason)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("failed check", func(t *testing.T) {
|
||||
adapter := &fakeTarget{available: true, state: target.Disabled}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{failedDetectionTarget{fakeTarget: adapter}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
createAndActivate(t, application)
|
||||
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err == nil {
|
||||
t.Fatal("Target check unexpectedly succeeded")
|
||||
}
|
||||
assertRejected(t, application, adapter, app.Intent{Kind: app.EnableTarget, TargetID: adapter.ID()}, "check failed")
|
||||
})
|
||||
|
||||
t.Run("unavailable Drift Resolution", func(t *testing.T) {
|
||||
adapter := &fakeTarget{available: false, state: target.Drifted}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
createAndActivate(t, application)
|
||||
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
assertRejected(t, application, adapter, app.Intent{Kind: app.RestoreOriginal, TargetID: adapter.ID()}, "unavailable")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicationRejectsProfileDependentIntentsWithoutActiveProfile(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
state target.State
|
||||
intent app.IntentKind
|
||||
}{
|
||||
{name: "Apply Profile", state: target.External, intent: app.ApplyProfileToTarget},
|
||||
{name: "Reapply Active Profile", state: target.Drifted, intent: app.ReapplyActiveProfile},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
adapter := &fakeTarget{available: true, state: test.state}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
result := application.Dispatch(app.Intent{Kind: test.intent, TargetID: adapter.ID()})
|
||||
if result.Err == nil || !strings.Contains(result.Err.Error(), "no Active Profile") || len(adapter.calls) != 0 {
|
||||
t.Fatalf("missing Active Profile result = %#v, calls=%#v", result, adapter.calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationRevalidatesRestoreAfterOwnershipSensitiveStateChange(t *testing.T) {
|
||||
adapter := &fakeTarget{available: true, state: target.Drifted}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
createAndActivate(t, application)
|
||||
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
result := application.Dispatch(app.Intent{Kind: app.ApplyProfileToTarget, TargetID: adapter.ID()})
|
||||
if result.Err == nil || !strings.Contains(result.Err.Error(), "no Active Profile") || len(adapter.calls) != 0 {
|
||||
t.Fatalf("missing Active Profile result = %#v, calls=%#v", result, adapter.calls)
|
||||
|
||||
// Model ownership loss as a fresh inspection becoming External between the
|
||||
// displayed Drift Resolution and dispatch. The stale Restore intent must not
|
||||
// reach SetDesired, where it could otherwise overwrite external state.
|
||||
adapter.state = target.External
|
||||
adapter.calls = nil
|
||||
result := application.Dispatch(app.Intent{Kind: app.RestoreOriginal, TargetID: adapter.ID()})
|
||||
if result.Err == nil || !strings.Contains(result.Err.Error(), "External") || len(adapter.calls) != 0 || result.Snapshot.Targets[0].State != target.External {
|
||||
t.Fatalf("stale Restore result = %#v, calls = %#v", result, adapter.calls)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user