feat: unify interactive operation coordination

This commit is contained in:
proxyctl coordinator
2026-07-17 22:23:31 +08:00
parent 177bf34354
commit 7c0b4481d7
17 changed files with 985 additions and 1013 deletions
+40 -37
View File
@@ -60,16 +60,22 @@ func TestApplicationStreamsIndependentTargetDetectionInProductOrder(t *testing.T
}
}
checking, updates := application.BeginTargetDetection()
stream := application.StartOperation(app.Intent{Kind: app.RefreshTargets})
checking := stream.Snapshot
for _, item := range checking.Targets {
if item.DetectionPhase != app.TargetChecking {
t.Fatalf("Target did not begin in Checking: %#v", item)
}
}
updates := stream.Updates
initial := <-updates
if !initial.IsRunning() || len(initial.Progress()) != 3 {
t.Fatalf("initial detection progress = %#v", initial)
}
close(secondRelease)
second := <-updates
if second.TargetID != "second" || second.Snapshot.Targets[1].DetectionPhase != app.TargetReady || second.Snapshot.Targets[1].State != target.External {
if second.Status().TargetID != "second" || second.Snapshot.Targets[1].DetectionPhase != app.TargetReady || second.Snapshot.Targets[1].State != target.External {
t.Fatalf("second completion = %#v", second)
}
assertOnlyDetectionChanged(t, checking, second.Snapshot, "second")
@@ -77,20 +83,20 @@ func TestApplicationStreamsIndependentTargetDetectionInProductOrder(t *testing.T
close(failedRelease)
failed := <-updates
if failed.TargetID != "failed" || failed.Snapshot.Targets[2].DetectionPhase != app.TargetDetectionFailed || failed.Snapshot.Targets[2].DetectionErr == nil {
if failed.Status().TargetID != "failed" || failed.Snapshot.Targets[2].DetectionPhase != app.TargetDetectionFailed || failed.Snapshot.Targets[2].DetectionErr == nil {
t.Fatalf("failed completion = %#v", failed)
}
assertOnlyDetectionChanged(t, second.Snapshot, failed.Snapshot, "failed")
close(firstRelease)
first := <-updates
if first.TargetID != "first" || first.Snapshot.Targets[0].DetectionPhase != app.TargetReady || first.Snapshot.Targets[0].State != target.Enabled {
if first.Status().TargetID != "first" || first.Snapshot.Targets[0].DetectionPhase != app.TargetReady || first.Snapshot.Targets[0].State != target.Enabled {
t.Fatalf("first completion = %#v", first)
}
assertOnlyDetectionChanged(t, failed.Snapshot, first.Snapshot, "first")
final := <-updates
if !final.Complete {
if !final.IsFailed() {
t.Fatalf("final update = %#v", final)
}
assertTargetOrder(t, final.Snapshot, []string{"first", "second", "failed"})
@@ -127,45 +133,36 @@ func (t *overlappingDetectionTarget) Detect(context.Context, *profile.Profile) t
return <-request.result
}
func TestOverlappingTargetDetectionDiscardsLateOlderGeneration(t *testing.T) {
adapter := &overlappingDetectionTarget{requests: make(chan detectionRequest, 2)}
func TestOverlappingTargetDetectionIsRejectedAtTheSerializedOperationEntry(t *testing.T) {
adapter := &overlappingDetectionTarget{requests: make(chan detectionRequest, 1)}
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
if err != nil {
t.Fatal(err)
}
_, olderUpdates := application.BeginTargetDetection()
older := <-adapter.requests
_, newerUpdates := application.BeginTargetDetection()
newer := <-adapter.requests
newer.result <- target.Detection{
Availability: target.Availability{Available: true, Evidence: "new availability"},
Inspection: target.Inspection{State: target.Enabled, Evidence: "new inspection"},
older := application.StartOperation(app.Intent{Kind: app.RefreshTargets})
request := <-adapter.requests
newer := application.StartOperation(app.Intent{Kind: app.RefreshTargets})
closed := <-newer.Updates
if closed.Status().Kind != app.StatusBlocked {
t.Fatalf("overlapping detection = %#v", closed)
}
for range newerUpdates {
request.result <- target.Detection{
Availability: target.Availability{Available: true, Evidence: "authoritative availability"},
Inspection: target.Inspection{State: target.Enabled, Evidence: "authoritative inspection"},
}
older.result <- target.Detection{
Availability: target.Availability{Evidence: "old availability"},
Inspection: target.Inspection{State: target.Disabled, Evidence: "old inspection"},
for range older.Updates {
}
staleUpdates := 0
for range olderUpdates {
staleUpdates++
}
if staleUpdates != 0 {
t.Fatalf("older generation published %d stale authoritative update(s)", staleUpdates)
}
got := application.Snapshot().Targets[0]
if !got.Available || got.State != target.Enabled || got.Evidence != "new inspection" {
t.Fatalf("late older generation overwrote newer result: %#v", got)
if !got.Available || got.State != target.Enabled {
t.Fatalf("serialized detection result = %#v", got)
}
}
type mutationInteractionTarget struct {
mu sync.Mutex
enabled bool
started chan struct{}
release chan struct{}
}
@@ -193,6 +190,7 @@ func (t *mutationInteractionTarget) SetDesired(context.Context, bool, *profile.P
return target.OperationResult{Inspection: target.Inspection{State: target.Enabled, Evidence: "mutation completed"}}
}
func (t *mutationInteractionTarget) Detect(context.Context, *profile.Profile) target.Detection {
close(t.started)
<-t.release
return target.Detection{
Availability: target.Availability{Available: true, Evidence: "stale detection"},
@@ -200,8 +198,8 @@ func (t *mutationInteractionTarget) Detect(context.Context, *profile.Profile) ta
}
}
func TestDispatchMutationInvalidatesInFlightTargetDetection(t *testing.T) {
adapter := &mutationInteractionTarget{release: make(chan struct{})}
func TestDispatchMutationWaitsForInFlightTargetDetection(t *testing.T) {
adapter := &mutationInteractionTarget{started: make(chan struct{}), release: make(chan struct{})}
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
if err != nil {
t.Fatal(err)
@@ -214,16 +212,21 @@ func TestDispatchMutationInvalidatesInFlightTargetDetection(t *testing.T) {
t.Fatal(activated.Err)
}
_, updates := application.BeginTargetDetection()
result := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: adapter.ID(), Enabled: true})
detection := application.StartOperation(app.Intent{Kind: app.RefreshTargets})
<-adapter.started
mutationDone := make(chan app.Result, 1)
go func() {
mutationDone <- application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: adapter.ID(), Enabled: true})
}()
close(adapter.release)
for range detection.Updates {
}
result := <-mutationDone
if result.Err != nil || result.Snapshot.Targets[0].State != target.Enabled {
t.Fatalf("mutation result = %#v", result)
}
close(adapter.release)
for range updates {
}
if got := application.Snapshot().Targets[0]; got.State != target.Enabled || got.Evidence != "mutation completed" {
t.Fatalf("in-flight detection overwrote mutation: %#v", got)
t.Fatalf("serialized mutation state: %#v", got)
}
}