8779c88d79
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
178 lines
6.0 KiB
Go
178 lines
6.0 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestStartOrchestratorAndAutomationConsumersOrder(t *testing.T) {
|
|
var order []string
|
|
|
|
err := startOrchestratorAndAutomationConsumers(
|
|
func() error { order = append(order, "bind"); return nil },
|
|
func() error {
|
|
order = append(order, "orchestrator")
|
|
return nil
|
|
},
|
|
func() { order = append(order, "automation") },
|
|
func() { order = append(order, "github") },
|
|
)
|
|
|
|
if err != nil {
|
|
t.Fatalf("start sequence returned error: %v", err)
|
|
}
|
|
want := []string{"bind", "orchestrator", "automation", "github"}
|
|
if !reflect.DeepEqual(order, want) {
|
|
t.Fatalf("start order = %v, want %v", order, want)
|
|
}
|
|
}
|
|
|
|
func TestStartOrchestratorAndAutomationConsumersStopsAfterOrchestratorFailure(t *testing.T) {
|
|
startErr := errors.New("orchestrator unavailable")
|
|
var automationStarted, githubStarted bool
|
|
|
|
err := startOrchestratorAndAutomationConsumers(
|
|
func() error { return nil },
|
|
func() error { return startErr },
|
|
func() { automationStarted = true },
|
|
func() { githubStarted = true },
|
|
)
|
|
|
|
if !errors.Is(err, startErr) {
|
|
t.Fatalf("start sequence error = %v, want %v", err, startErr)
|
|
}
|
|
if automationStarted || githubStarted {
|
|
t.Fatalf("downstream consumers started after orchestrator failure: automation=%v github=%v", automationStarted, githubStarted)
|
|
}
|
|
}
|
|
|
|
// TestStartOrchestratorAndAutomationConsumersStopsAfterBindFailure pins the
|
|
// bind-before-startup ordering invariant (docs/plans/startup-listener-before-
|
|
// recovery/task-03-ordering-guard.md): a bind failure must short-circuit
|
|
// before the orchestrator, automation, or GitHub poller ever start, exactly
|
|
// like an orchestrator-start failure does today.
|
|
func TestStartOrchestratorAndAutomationConsumersStopsAfterBindFailure(t *testing.T) {
|
|
bindErr := errors.New("bind unavailable")
|
|
var orchestratorStarted, automationStarted, githubStarted bool
|
|
|
|
err := startOrchestratorAndAutomationConsumers(
|
|
func() error { return bindErr },
|
|
func() error { orchestratorStarted = true; return nil },
|
|
func() { automationStarted = true },
|
|
func() { githubStarted = true },
|
|
)
|
|
|
|
if !errors.Is(err, bindErr) {
|
|
t.Fatalf("start sequence error = %v, want %v", err, bindErr)
|
|
}
|
|
if orchestratorStarted || automationStarted || githubStarted {
|
|
t.Fatalf("downstream steps started after bind failure: orchestrator=%v automation=%v github=%v",
|
|
orchestratorStarted, automationStarted, githubStarted)
|
|
}
|
|
}
|
|
|
|
// TestPublishReadinessFlipsReadyBeforeSwappingHandler is the regression test
|
|
// for R1-2 (docs/specs/health-endpoint-version/spec.md's 2026-08-22
|
|
// amendment): startGatewayAndServe must flip the readiness flag before
|
|
// swapping in the fully wired router, never the reverse, or a request racing
|
|
// the swap could observe the real router with ready still false and get a
|
|
// spurious 503 — recreating the exact flap docs/specs/startup-listener-
|
|
// before-recovery/spec.md exists to prevent.
|
|
func TestPublishReadinessFlipsReadyBeforeSwappingHandler(t *testing.T) {
|
|
var order []string
|
|
|
|
publishReadiness(
|
|
func() { order = append(order, "ready") },
|
|
func() { order = append(order, "handler") },
|
|
)
|
|
|
|
want := []string{"ready", "handler"}
|
|
if !reflect.DeepEqual(order, want) {
|
|
t.Fatalf("publishReadiness order = %v, want %v", order, want)
|
|
}
|
|
}
|
|
|
|
// TestMarkStartupReadySetsPhaseBeforePublishingReadiness is the regression
|
|
// test for AC-PLATFORM-STARTUP-PROGRESS-002.3: a GET /ready request racing
|
|
// startGatewayAndServe's completion must never observe a successful status
|
|
// against a snapshot that still reports an earlier phase, so the phase must
|
|
// reach Ready before the readiness flag flips, never the reverse.
|
|
func TestMarkStartupReadySetsPhaseBeforePublishingReadiness(t *testing.T) {
|
|
var order []string
|
|
|
|
markStartupReady(
|
|
func() { order = append(order, "phase") },
|
|
func() { order = append(order, "publish") },
|
|
)
|
|
|
|
want := []string{"phase", "publish"}
|
|
if !reflect.DeepEqual(order, want) {
|
|
t.Fatalf("markStartupReady order = %v, want %v", order, want)
|
|
}
|
|
}
|
|
|
|
func TestShouldLogStartupOrchestratorErrorSkipsExpectedShutdownErrors(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{name: "bind failure", err: errServerBindFailed, want: false},
|
|
{name: "canceled", err: context.Canceled, want: false},
|
|
{name: "wrapped canceled", err: fmt.Errorf("start: %w", context.Canceled), want: false},
|
|
{name: "deadline", err: context.DeadlineExceeded, want: false},
|
|
{name: "unexpected", err: errors.New("orchestrator unavailable"), want: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if got := shouldLogStartupOrchestratorError(test.err); got != test.want {
|
|
t.Fatalf("shouldLogStartupOrchestratorError(%v) = %v, want %v", test.err, got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBootstrapRuntimeReadinessHandoffSerializesStartupClose(t *testing.T) {
|
|
runtime := &bootstrapRuntime{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
if !runtime.beginReadinessPublication(ctx) {
|
|
t.Fatal("beginReadinessPublication() = false, want true")
|
|
}
|
|
cancel()
|
|
if runtime.claimStartupClose() {
|
|
t.Fatal("claimStartupClose() = true after readiness publication began")
|
|
}
|
|
}
|
|
|
|
func TestBootstrapRuntimeStartupCloseWinsBeforeReadiness(t *testing.T) {
|
|
runtime := &bootstrapRuntime{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
if runtime.beginReadinessPublication(ctx) {
|
|
t.Fatal("beginReadinessPublication() = true after cancellation")
|
|
}
|
|
if !runtime.claimStartupClose() {
|
|
t.Fatal("claimStartupClose() = false before readiness publication")
|
|
}
|
|
}
|
|
|
|
func TestProcessRuntimeContextSurvivesWorkerCancellation(t *testing.T) {
|
|
processCtx, cancelProcess := context.WithCancel(context.Background())
|
|
defer cancelProcess()
|
|
workerCtx, cancelWorker := context.WithCancel(context.Background())
|
|
workerCtx = withProcessContext(workerCtx, processCtx)
|
|
cancelWorker()
|
|
|
|
if got := processRuntimeContext(workerCtx); got != processCtx {
|
|
t.Fatal("processRuntimeContext() did not return the process context")
|
|
}
|
|
if err := processRuntimeContext(workerCtx).Err(); err != nil {
|
|
t.Fatalf("process runtime context error = %v, want nil", err)
|
|
}
|
|
}
|