package connector_inbound

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/pingcap-inc/nexuscrm-connectors/features/connector/events"
	"github.com/pingcap-inc/nexuscrm-connectors/features/connector/inboundrouting"
	"github.com/pingcap-inc/nexuscrm-connectors/features/connector/workqueue"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/constants"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/models"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/testutil"
	dto "github.com/prometheus/client_model/go"
	"github.com/rs/zerolog"
)

type observedClaimEvents struct {
	*fakeEventsService
	onClaim func(context.Context) (string, time.Time, bool, error)
}

func (s *observedClaimEvents) ClaimByIDForClassesWithMode(ctx context.Context, _, _, _ string, _ sql.NullString, _ int, _ []string, _ int64, _, _ bool) (string, time.Time, bool, error) {
	return s.onClaim(ctx)
}

func schedulerClaimHistogramCount(t *testing.T, workType, queueClass, outcome string) uint64 {
	t.Helper()
	metric := &dto.Metric{}
	if err := connectorInboundSchedulerClaimDuration.WithLabelValues(workType, queueClass, outcome).(prometheus.Metric).Write(metric); err != nil {
		t.Fatal(err)
	}
	return metric.GetHistogram().GetSampleCount()
}

func TestSchedulerClaimObservationPreservesClaimOutcomes(t *testing.T) {
	for _, tc := range []struct {
		name       string
		claimed    bool
		err        error
		panicValue any
	}{
		{name: "claimed", claimed: true},
		{name: "cas_lost"},
		{name: "error", err: errors.New("database unavailable")},
		{name: "canceled", err: context.Canceled},
		{name: "panic", panicValue: "claim panic sentinel"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			ctx := t.Context()
			gauge := connectorInboundSchedulerClaimsInflight.WithLabelValues("connector_event", "bulk")
			beforeInflight := testutil.ToFloat64(gauge)
			beforeCount := schedulerClaimHistogramCount(t, "connector_event", "bulk", tc.name)
			events := &observedClaimEvents{fakeEventsService: &fakeEventsService{}}
			events.onClaim = func(gotCtx context.Context) (string, time.Time, bool, error) {
				if gotCtx != ctx || testutil.ToFloat64(gauge) != beforeInflight+1 {
					t.Error("observation changed claim context or did not expose active claim")
				}
				if tc.panicValue != nil {
					panic(tc.panicValue)
				}
				return "claim-token", time.Now(), tc.claimed, tc.err
			}
			worker := &Worker{cfg: WorkerConfig{WorkerID: "observation-worker", PackInstallID: "observation-pack"}, events: events}
			candidate := schedulerClaimCandidate{
				event:  &connector_events.ConnectorEvent{ID: "observation-event", QueueClass: "default"},
				workID: "observation-work", workType: "connector_event", queueClass: "bulk",
			}
			var observedPanic any
			func() {
				defer func() { observedPanic = recover() }()
				_, claimed, err := worker.claimByIDForScheduler(ctx, candidate)
				if claimed != tc.claimed || err != tc.err {
					t.Errorf("claim result changed: claimed=%t err=%v", claimed, err)
				}
			}()
			if observedPanic != tc.panicValue {
				t.Fatalf("panic = %v; want unchanged %v", observedPanic, tc.panicValue)
			}
			if testutil.ToFloat64(gauge) != beforeInflight || schedulerClaimHistogramCount(t, "connector_event", "bulk", tc.name) != beforeCount+1 {
				t.Fatal("claim completion did not clean active gauge and observe exactly one outcome")
			}
			// A missing candidate is not a database claim or an empty-poll log.
			if _, _, err := worker.claimByIDForScheduler(ctx, schedulerClaimCandidate{}); err != nil {
				t.Fatal(err)
			}
			if schedulerClaimHistogramCount(t, "connector_event", "bulk", tc.name) != beforeCount+1 {
				t.Fatal("missing candidate was counted as a claim")
			}
		})
	}
}

func TestSchedulerClaimObservationSlowBreadcrumbsAreBounded(t *testing.T) {
	var output bytes.Buffer
	o := startSchedulerClaimObservation(WorkerConfig{WorkerID: "worker", PackInstallID: "pack"}, schedulerClaimCandidate{
		event:  &connector_events.ConnectorEvent{ID: "event-id", QueueClass: "bulk", Payload: json.RawMessage(`{"secret":"must-not-log"}`)},
		workID: "work-id", workType: "connector_event", queueClass: "bulk",
	})
	t.Cleanup(func() { o.finish("error") })
	// Advance only this observer's elapsed clock, not claim deadlines or any
	// global timing configuration. Trigger the timer callback deterministically.
	o.mu.Lock()
	o.started = time.Now().Add(-schedulerSlowClaimThreshold - time.Second)
	o.logger = zerolog.New(&output)
	o.mu.Unlock()
	o.warnStillActive()
	o.warnStillActive()
	o.finish("claimed")
	o.warnStillActive()
	o.finish("claimed")
	lines := strings.Split(strings.TrimSpace(output.String()), "\n")
	if len(lines) != 2 {
		t.Fatalf("slow claim wrote %d logs, want one active and one completion", len(lines))
	}
	for i, line := range lines {
		var event map[string]any
		if err := json.Unmarshal([]byte(line), &event); err != nil {
			t.Fatal(err)
		}
		want := []string{"inflight", "claimed"}[i]
		if event["outcome"] != want || event["connector_event_id"] != "event-id" || event["work_item_id"] != "work-id" || event["queue_class"] != "bulk" || event["started_at"] == nil || event["elapsed_ms"].(float64) < 5000 {
			t.Fatalf("incomplete slow-claim breadcrumb: %s", line)
		}
	}
	if strings.Contains(output.String(), "must-not-log") {
		t.Fatal("claim diagnostic leaked payload")
	}
}

func TestSchedulerClaimObservationCompletionRacesTimerWithoutLeak(t *testing.T) {
	gauge := connectorInboundSchedulerClaimsInflight.WithLabelValues("unknown", "unknown")
	before := testutil.ToFloat64(gauge)
	for range 20 {
		o := startSchedulerClaimObservation(WorkerConfig{}, schedulerClaimCandidate{workType: "arbitrary-work-type", queueClass: "arbitrary-queue"})
		o.mu.Lock()
		o.started = time.Now().Add(-schedulerSlowClaimThreshold - time.Second)
		o.logger = zerolog.Nop()
		o.mu.Unlock()
		o.timer.Reset(0)
		var wg sync.WaitGroup
		wg.Add(2)
		go func() { defer wg.Done(); o.warnStillActive() }()
		go func() { defer wg.Done(); o.finish("cas_lost") }()
		wg.Wait()
		o.finish("cas_lost")
	}
	if testutil.ToFloat64(gauge) != before {
		t.Fatal("timer/completion race leaked active claim metric")
	}
}

// ── fakes ─────────────────────────────────────────────────────────────────────

// fakeEventsService is an in-memory EventsService for worker unit tests.
type fakeEventsService struct {
	mu               sync.Mutex
	queue            []*connector_events.ConnectorEvent
	next             int
	claimed          map[string]bool
	processed        []string
	skipped          []string
	failed           []failedCall
	released         []string
	releasedForRetry []failedCall
	blocked          []blockedDependencyCall
	claimErr         error

	// reclaimStaleCalls / peekStaleCalls count limp-path (stale-reclaim-only)
	// invocations so a routing test can assert a StaleReclaimOnly permit takes the
	// stale path instead of the general ClaimNext/PeekBatch.
	reclaimStaleCalls int
	peekStaleCalls    int

	failOnCanceledFinalize bool
	processedCtxErrs       []error
	failedCtxErrs          []error

	// claimByIDResults, when non-nil, maps eventID → whether ClaimByID wins.
	// eventIDs not present in the map default to winning (true).
	claimByIDResults map[string]bool

	// claimByIDErrs, when non-nil, maps eventID → error returned by ClaimByID.
	claimByIDErrs map[string]error

	// coalescedSiblings maps current event ID to the next same-source sibling
	// returned by ClaimNextCoalescedSibling.
	coalescedSiblings map[string]*connector_events.ConnectorEvent

	// settledProjectionPayloads restores the immutable payload sequence for a
	// source-projection WorkItem selected from a dispatcher delivery.
	settledProjectionPayloads map[string][]*connector_events.ConnectorEvent

	claimNextClasses []string
	peekBatchClasses []string
	claimByIDModes   []bool
}

type blockedDependencyCall struct {
	eventID    string
	claimToken string
	probe      IdentityConvergenceProbe
}

type recordingClaimLimiter struct {
	mu       sync.Mutex
	decision ClaimLimiterDecision
	requests []ClaimLimiterRequest
	releases []int
}

type recordingScheduler struct {
	called   bool
	ctx      context.Context
	cfg      schedulerConfig
	executor executor
	err      error
	wakes    int
}

func (r *recordingScheduler) run(ctx context.Context, _ <-chan struct{}, executor executor) error {
	r.called = true
	r.ctx = ctx
	r.executor = executor
	r.cfg = executor.schedulerConfig()
	return r.err
}

func (r *recordingScheduler) Wake() {
	r.wakes++
}

type recordingExecutor struct {
	cfg                schedulerConfig
	permit             ClaimLimiterDecision
	allowed            bool
	permitDelay        time.Duration
	permitErr          error
	permitDecisions    []ClaimLimiterDecision
	permitBatchSizes   []int
	claimEvents        []*connector_events.ConnectorEvent
	claimCalls         int
	claimMaxConcurrent int
	claimInFlight      int
	claimBlock         chan struct{}
	claimCompleted     chan struct{}
	claimPanic         any
	executeResult      claimExecutionResult
	executeErr         error
	executeCalls       int
	executePanic       any
	executeStarted     chan struct{}
	executeStartedFunc func()
	executeBlock       chan struct{}
	permitBatchSize    int
	executeRequest     claimExecutionRequest
	releasedDecision   ClaimLimiterDecision
	releasedDecisions  []ClaimLimiterDecision
	releasedClaimRows  int
}

func (r *recordingExecutor) schedulerConfig() schedulerConfig {
	return r.cfg
}

func (r *recordingExecutor) requestClaimPermit(_ context.Context, _ <-chan struct{}, batchSize int) (ClaimLimiterDecision, bool, error) {
	r.permitBatchSize = batchSize
	return r.permit, r.allowed, r.permitErr
}

func (r *recordingExecutor) requestClaimPermitForScheduler(_ context.Context, batchSize int) (ClaimLimiterDecision, bool, time.Duration, error) {
	r.permitBatchSize = batchSize
	r.permitBatchSizes = append(r.permitBatchSizes, batchSize)
	if len(r.permitDecisions) > 0 {
		decision := r.permitDecisions[0]
		r.permitDecisions = r.permitDecisions[1:]
		return decision, decision.Allow, r.permitDelay, r.permitErr
	}
	return r.permit, r.allowed, r.permitDelay, r.permitErr
}

func (r *recordingExecutor) claimNextForScheduler(ctx context.Context, _ bool) (*connector_events.ConnectorEvent, bool, error) {
	r.claimCalls++
	r.claimInFlight++
	defer func() {
		r.claimInFlight--
		if r.claimCompleted != nil {
			close(r.claimCompleted)
			r.claimCompleted = nil
		}
	}()
	if r.claimInFlight > r.claimMaxConcurrent {
		r.claimMaxConcurrent = r.claimInFlight
	}
	if r.claimPanic != nil {
		panic(r.claimPanic)
	}
	if r.claimBlock != nil {
		select {
		case <-ctx.Done():
			return nil, false, ctx.Err()
		case <-r.claimBlock:
		}
	}
	if len(r.claimEvents) == 0 {
		return nil, false, nil
	}
	ev := r.claimEvents[0]
	r.claimEvents = r.claimEvents[1:]
	return ev, true, nil
}

func (r *recordingExecutor) executeClaim(_ context.Context, _ <-chan struct{}, request claimExecutionRequest) (claimExecutionResult, error) {
	r.executeCalls++
	r.executeRequest = request
	if r.executePanic != nil {
		panic(r.executePanic)
	}
	if r.executeStarted != nil {
		close(r.executeStarted)
		r.executeStarted = nil
	}
	if r.executeStartedFunc != nil {
		r.executeStartedFunc()
	}
	if r.executeBlock != nil {
		<-r.executeBlock
	}
	if request.Event != nil {
		r.executeResult.Claimed = true
		if r.executeResult.ClaimedRows == 0 {
			r.executeResult.ClaimedRows = 1
		}
	}
	return r.executeResult, r.executeErr
}

func (r *recordingExecutor) releaseClaimPermitFromScheduler(decision ClaimLimiterDecision, claimedRows int) {
	r.releasedDecision = decision
	r.releasedDecisions = append(r.releasedDecisions, decision)
	r.releasedClaimRows = claimedRows
}

type batchRecordingExecutor struct {
	*recordingExecutor
	peekCandidates           []schedulerClaimCandidate
	peekCalls                int
	peekLimit                int
	claimByIDCalls           int
	claimByIDResult          map[string]bool
	candidatePermitBatchSize int
	candidatePermitPanic     any
}

func (r *batchRecordingExecutor) claimPermitBatchSizeForScheduler(_ context.Context, _ schedulerClaimCandidate) int {
	if r.candidatePermitPanic != nil {
		panic(r.candidatePermitPanic)
	}
	if r.candidatePermitBatchSize > 1 {
		return r.candidatePermitBatchSize
	}
	return 1
}

func (r *batchRecordingExecutor) peekClaimCandidatesForScheduler(_ context.Context, limit int, _ bool) ([]schedulerClaimCandidate, error) {
	r.peekCalls++
	r.peekLimit = limit
	if limit > len(r.peekCandidates) {
		limit = len(r.peekCandidates)
	}
	return append([]schedulerClaimCandidate(nil), r.peekCandidates[:limit]...), nil
}

func (r *batchRecordingExecutor) claimByIDForScheduler(_ context.Context, candidate schedulerClaimCandidate) (*connector_events.ConnectorEvent, bool, error) {
	r.claimByIDCalls++
	if candidate.event == nil {
		return nil, false, nil
	}
	if r.claimByIDResult != nil {
		if ok, exists := r.claimByIDResult[candidate.event.ID]; exists && !ok {
			return nil, false, nil
		}
	}
	claimed := *candidate.event
	claimed.ClaimToken = "claim-" + r.cfg.PollInterval.String() + "-" + candidate.event.ID
	claimedAt := time.Now()
	claimed.ClaimedAt = &claimedAt
	return &claimed, true, nil
}

func (r *batchRecordingExecutor) executeClaim(ctx context.Context, drain <-chan struct{}, request claimExecutionRequest) (claimExecutionResult, error) {
	if !request.HasCandidate {
		return r.recordingExecutor.executeClaim(ctx, drain, request)
	}
	started := time.Now()
	ev, ok, claimErr := r.claimByIDForScheduler(ctx, request.Candidate)
	result := claimExecutionResult{
		Claimed:        ok,
		ClaimAttempted: true,
		ClaimLatency:   time.Since(started),
		ClaimEligible:  request.Candidate.eligibleAt,
		ClaimErr:       claimErr,
	}
	if claimErr != nil || !ok {
		return result, claimErr
	}
	execRequest := request
	execRequest.Event = ev
	execRequest.HasCandidate = false
	execResult, execErr := r.recordingExecutor.executeClaim(ctx, drain, execRequest)
	execResult.ClaimAttempted = true
	execResult.ClaimLatency = result.ClaimLatency
	execResult.ClaimEligible = result.ClaimEligible
	execResult.ClaimErr = claimErr
	return execResult, execErr
}

func (r *recordingClaimLimiter) BeforeClaim(_ context.Context, req ClaimLimiterRequest) (ClaimLimiterDecision, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.requests = append(r.requests, req)
	decision := r.decision
	if !decision.Allow && decision.Reason == "" {
		decision.Allow = true
	}
	if decision.MaxClaims <= 0 {
		decision.MaxClaims = req.BatchSize
	}
	if decision.ReservationID == 0 {
		decision.ReservationID = int64(len(r.requests))
	}
	return decision, nil
}

func (r *recordingClaimLimiter) ReleaseClaimPermit(_ ClaimLimiterDecision, claimed int) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.releases = append(r.releases, claimed)
}

type failedCall struct {
	eventID  string
	errorMsg string
}

func newFakeEvents(events ...*connector_events.ConnectorEvent) *fakeEventsService {
	return &fakeEventsService{
		queue:   events,
		claimed: make(map[string]bool),
	}
}

func (f *fakeEventsService) ClaimNext(_ context.Context, _, _ string) (*connector_events.ConnectorEvent, bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.claimNextLocked()
}

func (f *fakeEventsService) ClaimNextForClasses(_ context.Context, _, _ string, queueClasses []string) (*connector_events.ConnectorEvent, bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.claimNextClasses = append([]string(nil), queueClasses...)
	return f.claimNextLocked()
}

func (f *fakeEventsService) claimNextLocked() (*connector_events.ConnectorEvent, bool, error) {
	if f.claimErr != nil {
		return nil, false, f.claimErr
	}
	for f.next < len(f.queue) {
		ev := f.queue[f.next]
		f.next++
		if !f.claimed[ev.ID] {
			f.claimed[ev.ID] = true
			return ev, true, nil
		}
	}
	return nil, false, nil
}

func (f *fakeEventsService) PeekBatch(_ context.Context, _ string, n int) ([]*connector_events.ConnectorEvent, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.peekBatchLocked(n)
}

func (f *fakeEventsService) PeekBatchForClasses(_ context.Context, _ string, queueClasses []string, n int) ([]*connector_events.ConnectorEvent, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.peekBatchClasses = append([]string(nil), queueClasses...)
	return f.peekBatchLocked(n)
}

func (f *fakeEventsService) peekBatchLocked(n int) ([]*connector_events.ConnectorEvent, error) {
	if f.claimErr != nil {
		return nil, f.claimErr
	}
	// Return up to n unclaimed rows without marking them claimed.
	var peeked []*connector_events.ConnectorEvent
	i := f.next
	for i < len(f.queue) && len(peeked) < n {
		ev := f.queue[i]
		if !f.claimed[ev.ID] {
			peeked = append(peeked, ev)
		}
		i++
	}
	return peeked, nil
}

// staleReclaimingEventsService implementation — records that the limp path was
// taken. The fake reuses the same backing queue, so behavior is unchanged; only
// the call counters distinguish the stale path from the general one.
func (f *fakeEventsService) ReclaimStaleNext(_ context.Context, _, _ string) (*connector_events.ConnectorEvent, bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.reclaimStaleCalls++
	return f.claimNextLocked()
}

func (f *fakeEventsService) ReclaimStaleNextForClasses(_ context.Context, _, _ string, queueClasses []string) (*connector_events.ConnectorEvent, bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.reclaimStaleCalls++
	f.claimNextClasses = append([]string(nil), queueClasses...)
	return f.claimNextLocked()
}

func (f *fakeEventsService) PeekStaleBatch(_ context.Context, _ string, n int) ([]*connector_events.ConnectorEvent, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.peekStaleCalls++
	return f.peekBatchLocked(n)
}

func (f *fakeEventsService) PeekStaleBatchForClasses(_ context.Context, _ string, queueClasses []string, n int) ([]*connector_events.ConnectorEvent, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.peekStaleCalls++
	f.peekBatchClasses = append([]string(nil), queueClasses...)
	return f.peekBatchLocked(n)
}

func TestWorkerRunClaimFirstUsesConfiguredScheduler(t *testing.T) {
	sentinel := errors.New("scheduler stopped")
	scheduler := &recordingScheduler{err: sentinel}
	service := newCompleteWorkItemServiceFake(&fakeWorkItemClaims{}, &fakeWorkItemEventLoader{}, nil)
	cfg := liveWorkItemConfig()
	cfg.WorkerID = "worker-scheduler"
	cfg.PollInterval = 7 * time.Millisecond
	cfg.BatchSize = 11
	cfg.MaxClaims = 3
	w, err := NewWorkItemWorkerForTest(context.Background(),
		cfg,
		service,
		&fakeSyncState{},
		nil,
		&fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}},
		&stubRouteResolver{routes: map[string]string{workerTestNS + "/noop.created": "NoopFlow__c"}},
	)
	if err != nil {
		t.Fatalf("NewWorkItemWorkerForTest: %v", err)
	}
	w.scheduler = scheduler

	err = w.RunClaimFirstUntilDrained(nil, nil)
	if !errors.Is(err, sentinel) {
		t.Fatalf("RunClaimFirstUntilDrained err = %v, want %v", err, sentinel)
	}
	if !scheduler.called {
		t.Fatalf("configured scheduler was not called")
	}
	if scheduler.ctx == nil {
		t.Fatalf("scheduler received nil context")
	}
	if scheduler.executor == nil {
		t.Fatalf("scheduler did not receive an executor")
	}
	if scheduler.cfg.ClaimPath != ClaimPathWorkItem {
		t.Fatalf("scheduler claim path = %q, want %q", scheduler.cfg.ClaimPath, ClaimPathWorkItem)
	}
	if scheduler.cfg.MaxClaims != 3 || scheduler.cfg.BatchSize != 11 || scheduler.cfg.PollInterval != 7*time.Millisecond {
		t.Fatalf("scheduler cfg = %+v, want MaxClaims=3 BatchSize=11 PollInterval=7ms", scheduler.cfg)
	}
}

func TestWorkerRunClaimFirstFallsBackFromTypedNilScheduler(t *testing.T) {
	ev := livePendingEvent("evt-typed-nil-scheduler", "noop.created")
	claims := &fakeWorkItemClaims{work: liveWorkItem(
		"work-typed-nil-scheduler",
		ev.ID,
		connector_workqueue.WorkTypeConnectorEvent,
		0,
		"lease-typed-nil-scheduler",
	)}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{event: ev}, nil)
	syncstate := &fakeSyncState{}
	cfg := liveWorkItemConfig()
	cfg.WorkerID = "worker-scheduler"
	cfg.PollInterval = time.Millisecond
	cfg.BatchSize = 1
	cfg.MaxClaims = 1
	w, err := NewWorkItemWorkerForTest(context.Background(),
		cfg,
		service,
		syncstate,
		nil,
		&fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}},
		&stubRouteResolver{routes: map[string]string{workerTestNS + "/noop.created": "NoopFlow__c"}},
	)
	if err != nil {
		t.Fatalf("NewWorkItemWorkerForTest: %v", err)
	}
	var typedNil *recordingScheduler
	w.scheduler = typedNil

	if err := w.RunClaimFirstUntilDrained(context.Background(), nil); err != nil {
		t.Fatalf("RunClaimFirstUntilDrained: %v", err)
	}

	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-typed-nil-scheduler" {
		t.Fatalf("completed WorkItems = %q, want work-typed-nil-scheduler", got)
	}
	if got := strings.Join(claims.completeCalls, ","); got != connector_workqueue.WorkStateProcessed {
		t.Fatalf("WorkItem terminal states = %q, want processed", got)
	}
	if len(claims.failInputs) != 0 || claims.retryCalls != 0 || len(syncstate.errors) != 0 {
		t.Fatalf("typed-nil fallback must not record failure: failed=%v retried=%d syncErrors=%v", claims.failInputs, claims.retryCalls, syncstate.errors)
	}
}

func TestCompatibilitySchedulerPreservesPermitAndClaimAdapter(t *testing.T) {
	executor := &recordingExecutor{
		cfg: schedulerConfig{
			MaxClaims:    1,
			BatchSize:    5,
			PollInterval: time.Hour,
		},
		permit: ClaimLimiterDecision{
			Allow:            true,
			MaxClaims:        2,
			StaleReclaimOnly: true,
			ReservationID:    42,
		},
		allowed:       true,
		executeResult: claimExecutionResult{Claimed: true, ClaimedRows: 4},
	}

	if err := (compatibilityScheduler{}).run(context.Background(), nil, executor); err != nil {
		t.Fatalf("compatibility scheduler Run: %v", err)
	}
	if executor.permitBatchSize != 5 {
		t.Fatalf("permit batch size = %d, want 5", executor.permitBatchSize)
	}
	wantReq := claimExecutionRequest{MaxClaims: 2, StaleReclaimOnly: true}
	if executor.executeRequest != wantReq {
		t.Fatalf("execute request = %+v, want %+v", executor.executeRequest, wantReq)
	}
	if executor.releasedDecision.ReservationID != 42 {
		t.Fatalf("released decision reservation = %d, want 42", executor.releasedDecision.ReservationID)
	}
	if executor.releasedClaimRows != 4 {
		t.Fatalf("released claimed rows = %d, want 4", executor.releasedClaimRows)
	}
}

func TestCompatibilitySchedulerCountsEveryClaimedRowTowardMaxClaims(t *testing.T) {
	executor := &recordingExecutor{
		cfg: schedulerConfig{
			MaxClaims:    2,
			BatchSize:    2,
			PollInterval: time.Hour,
		},
		permit: ClaimLimiterDecision{
			Allow:     true,
			MaxClaims: 2,
		},
		allowed:       true,
		executeResult: claimExecutionResult{Claimed: true, ClaimedRows: 2},
	}

	if err := (compatibilityScheduler{}).run(context.Background(), nil, executor); err != nil {
		t.Fatalf("compatibility scheduler Run: %v", err)
	}
	if executor.executeCalls != 1 {
		t.Fatalf("execute calls = %d, want 1 after one two-row execution reaches MaxClaims", executor.executeCalls)
	}
}

func TestCentralClaimSchedulerOwnsClaimLoopAcrossExecutors(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	unblockClaims := make(chan struct{})
	executors := []*recordingExecutor{
		{
			cfg:         schedulerConfig{MaxClaims: 1, BatchSize: 5, PollInterval: time.Millisecond},
			permit:      ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 1},
			allowed:     true,
			claimEvents: []*connector_events.ConnectorEvent{noopEvent("evt-central-1", "noop.created")},
			claimBlock:  unblockClaims,
		},
		{
			cfg:         schedulerConfig{MaxClaims: 1, BatchSize: 5, PollInterval: time.Millisecond},
			permit:      ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 2},
			allowed:     true,
			claimEvents: []*connector_events.ConnectorEvent{noopEvent("evt-central-2", "noop.created")},
			claimBlock:  unblockClaims,
		},
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	errs := make(chan error, len(executors))
	for _, executor := range executors {
		go func(executor *recordingExecutor) {
			errs <- scheduler.run(ctx, nil, executor)
		}(executor)
	}
	time.Sleep(10 * time.Millisecond)
	close(unblockClaims)
	for range executors {
		if err := <-errs; err != nil {
			t.Fatalf("central scheduler run returned error: %v", err)
		}
	}
	totalClaims := 0
	for i, executor := range executors {
		totalClaims += executor.claimCalls
		if executor.executeRequest.Event == nil {
			t.Fatalf("executor %d received no already-claimed event", i)
		}
		if executor.releasedClaimRows != 1 {
			t.Fatalf("executor %d released %d claimed rows, want 1", i, executor.releasedClaimRows)
		}
		if executor.claimMaxConcurrent > 1 {
			t.Fatalf("executor %d concurrent claim calls = %d, want <=1", i, executor.claimMaxConcurrent)
		}
	}
	if totalClaims != 2 {
		t.Fatalf("total claim calls = %d, want one scheduler-owned claim per executor", totalClaims)
	}
}

func TestCentralClaimSchedulerWakeIfRunningRequiresLiveRegistration(t *testing.T) {
	t.Run("never started", func(t *testing.T) {
		scheduler := NewCentralClaimScheduler()
		if scheduler.WakeIfRunning() {
			t.Fatalf("WakeIfRunning returned true for scheduler with no running claim loop")
		}
	})

	t.Run("running without registrations", func(t *testing.T) {
		scheduler := NewCentralClaimScheduler()
		scheduler.mu.Lock()
		scheduler.running = true
		scheduler.done = make(chan struct{})
		scheduler.wakeCh = make(chan struct{}, 1)
		scheduler.mu.Unlock()

		if scheduler.WakeIfRunning() {
			t.Fatalf("WakeIfRunning returned true for scheduler with no registered workers")
		}
	})

	t.Run("running with live registration", func(t *testing.T) {
		scheduler := NewCentralClaimScheduler()
		scheduler.mu.Lock()
		scheduler.running = true
		scheduler.done = make(chan struct{})
		scheduler.wakeCh = make(chan struct{}, 1)
		scheduler.regs = []*centralClaimRegistration{{work: make(chan centralClaimWork, 1)}}
		scheduler.mu.Unlock()

		if !scheduler.WakeIfRunning() {
			t.Fatalf("WakeIfRunning returned false for running scheduler with a live registration")
		}
	})
}

func TestCentralClaimSchedulerDispatchesSixteenSlowExecutors(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	const workerCount = 16
	executeBlock := make(chan struct{})
	allStarted := make(chan struct{})
	var (
		mu        sync.Mutex
		active    int
		maxActive int
		started   int
	)
	executors := make([]*recordingExecutor, 0, workerCount)
	for i := 0; i < workerCount; i++ {
		ordinal := i
		executor := &recordingExecutor{
			cfg:          schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Millisecond},
			permit:       ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: int64(100 + i)},
			allowed:      true,
			claimEvents:  []*connector_events.ConnectorEvent{noopEvent(fmt.Sprintf("evt-central-%02d", ordinal), "noop.created")},
			executeBlock: executeBlock,
		}
		executor.executeStartedFunc = func() {
			mu.Lock()
			active++
			if active > maxActive {
				maxActive = active
			}
			started++
			if started == workerCount {
				close(allStarted)
			}
			mu.Unlock()
		}
		executors = append(executors, executor)
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	errs := make(chan error, workerCount)
	for _, executor := range executors {
		go func(executor *recordingExecutor) {
			errs <- scheduler.run(ctx, nil, executor)
		}(executor)
	}
	select {
	case <-allStarted:
	case <-time.After(time.Second):
		mu.Lock()
		gotStarted := started
		gotMaxActive := maxActive
		mu.Unlock()
		t.Fatalf("started/max active = %d/%d, want all %d slow executors active", gotStarted, gotMaxActive, workerCount)
	}
	mu.Lock()
	gotMaxActive := maxActive
	mu.Unlock()
	if gotMaxActive != workerCount {
		t.Fatalf("max active executions = %d, want %d", gotMaxActive, workerCount)
	}
	close(executeBlock)
	for range executors {
		if err := <-errs; err != nil {
			t.Fatalf("central scheduler run returned error: %v", err)
		}
	}
}

func TestCentralClaimSchedulerDoesNotRedispatchAfterExecutorPanic(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	reg := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{MaxClaims: 2}},
		busy:     true,
	}

	scheduler.handleClaimResult(centralClaimResult{
		reg:        reg,
		panicValue: "executor panic",
	})

	if !reg.closed {
		t.Fatal("registration remained open after executor panic")
	}
	if reg.busy {
		t.Fatal("registration remained busy after executor panic")
	}
	scheduler.regs = []*centralClaimRegistration{reg}
	if next := scheduler.nextAvailableRegistration(time.Now()); next != nil {
		t.Fatal("scheduler selected a registration after executor panic")
	}
}

func TestCentralClaimSchedulerRecoversClaimLoopPanicAndReleasesPermit(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	executor := &recordingExecutor{
		cfg:        schedulerConfig{BatchSize: 1, PollInterval: time.Millisecond},
		permit:     ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 99},
		allowed:    true,
		claimPanic: "claim panic",
	}

	err := scheduler.run(context.Background(), nil, executor)
	if err == nil {
		t.Fatal("scheduler.run returned nil after claim loop panic")
	}
	if !errors.Is(err, errCentralClaimSchedulerPanic) {
		t.Fatalf("scheduler.run error = %v, want errCentralClaimSchedulerPanic", err)
	}
	if executor.releasedDecision.ReservationID != 99 {
		t.Fatalf("released reservation = %d, want 99", executor.releasedDecision.ReservationID)
	}
	if executor.releasedClaimRows != 0 {
		t.Fatalf("released claimed rows = %d, want 0 after claim panic", executor.releasedClaimRows)
	}
}

func TestCentralClaimSchedulerResetsRegistrationsForNewGeneration(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	oldResultCh := make(chan centralClaimResult, 1)
	oldReg := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{BatchSize: 1}},
		work:     make(chan centralClaimWork, 1),
		resultCh: oldResultCh,
		busy:     true,
	}
	scheduler.regs = []*centralClaimRegistration{oldReg}
	scheduler.next = 1
	scheduler.running = true
	scheduler.done = make(chan struct{})
	scheduler.resultCh = oldResultCh
	scheduler.finish(errCentralClaimSchedulerPanic)

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	newReg := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{BatchSize: 1}},
		work:     make(chan centralClaimWork, 1),
	}
	_ = scheduler.register(newReg, ctx, nil)
	defer scheduler.unregister(newReg)

	if len(scheduler.regs) != 1 || scheduler.regs[0] != newReg {
		t.Fatalf("new scheduler generation inherited stale registrations: %#v", scheduler.regs)
	}
	if scheduler.next != 0 {
		t.Fatalf("scheduler next index = %d, want reset to 0", scheduler.next)
	}
	if newReg.resultCh == nil || newReg.resultCh == oldResultCh {
		t.Fatal("new registration did not receive an isolated result channel")
	}
}

func TestCentralClaimSchedulerCompletesClaimedWorkAfterDrain(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	drain := make(chan struct{})
	executeStarted := make(chan struct{})
	executeBlock := make(chan struct{})
	executor := &recordingExecutor{
		cfg:            schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Millisecond},
		permit:         ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 123},
		allowed:        true,
		claimEvents:    []*connector_events.ConnectorEvent{noopEvent("evt-drain-claimed", "noop.created")},
		executeStarted: executeStarted,
		executeBlock:   executeBlock,
	}
	errs := make(chan error, 1)
	go func() {
		errs <- scheduler.run(context.Background(), drain, executor)
	}()

	select {
	case <-executeStarted:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for claimed work to start")
	}
	close(drain)
	select {
	case err := <-errs:
		t.Fatalf("scheduler returned before claimed work completed: %v", err)
	default:
	}
	close(executeBlock)
	select {
	case err := <-errs:
		if err != nil {
			t.Fatalf("scheduler.run returned error: %v", err)
		}
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for scheduler to drain after claimed work completed")
	}
	if executor.releasedDecision.ReservationID != 123 {
		t.Fatalf("released reservation = %d, want 123", executor.releasedDecision.ReservationID)
	}
	if executor.releasedClaimRows != 1 {
		t.Fatalf("released claimed rows = %d, want 1", executor.releasedClaimRows)
	}
}

func TestCentralClaimSchedulerDoesNotSleepWholePackAfterEmptyClass(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	emptyClass := &recordingExecutor{
		cfg:     schedulerConfig{BatchSize: 1, PollInterval: time.Hour},
		permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 201},
		allowed: true,
	}
	executeStarted := make(chan struct{})
	executeBlock := make(chan struct{})
	activeClass := &recordingExecutor{
		cfg:            schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Hour},
		permit:         ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 202},
		allowed:        true,
		claimEvents:    []*connector_events.ConnectorEvent{noopEvent("evt-active-class", "noop.created")},
		executeStarted: executeStarted,
		executeBlock:   executeBlock,
	}
	errs := make(chan error, 2)
	go func() { errs <- scheduler.run(ctx, nil, emptyClass) }()
	time.Sleep(10 * time.Millisecond)
	go func() { errs <- scheduler.run(ctx, nil, activeClass) }()

	select {
	case <-executeStarted:
	case <-time.After(250 * time.Millisecond):
		t.Fatal("active queue class was blocked behind empty class backoff")
	}
	close(executeBlock)
	cancel()
	for range 2 {
		err := <-errs
		if err != nil && !errors.Is(err, context.Canceled) {
			t.Fatalf("scheduler.run returned error: %v", err)
		}
	}
	if activeClass.releasedDecision.ReservationID != 202 {
		t.Fatalf("active class released reservation = %d, want 202", activeClass.releasedDecision.ReservationID)
	}
}

func TestCentralClaimSchedulerDispatchesAllBulkCapableExecutorsWhileSlow(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	const workerCount = 16
	executeBlock := make(chan struct{})
	executors := make([]*recordingExecutor, 0, workerCount)
	started := make([]chan struct{}, 0, 12)
	for i := 0; i < workerCount; i++ {
		executor := &recordingExecutor{
			cfg:     schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Hour},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: int64(i + 1)},
			allowed: true,
		}
		if i >= 4 {
			ch := make(chan struct{})
			executor.claimEvents = []*connector_events.ConnectorEvent{
				noopEvent(fmt.Sprintf("evt-bulk-capable-%02d", i), "noop.created"),
			}
			executor.executeStarted = ch
			executor.executeBlock = executeBlock
			started = append(started, ch)
		}
		executors = append(executors, executor)
	}

	errs := make(chan error, len(executors))
	for _, executor := range executors {
		go func(executor *recordingExecutor) {
			errs <- scheduler.run(ctx, nil, executor)
		}(executor)
	}

	for i, ch := range started {
		select {
		case <-ch:
		case <-time.After(time.Second):
			t.Fatalf("timed out waiting for bulk-capable executor %d to start", i)
		}
	}

	close(executeBlock)
	cancel()
	for range executors {
		err := <-errs
		if err != nil && !errors.Is(err, context.Canceled) {
			t.Fatalf("scheduler.run returned error: %v", err)
		}
	}

	claimed := 0
	for _, executor := range executors {
		if executor.executeRequest.Event != nil {
			claimed++
		}
	}
	if claimed != 12 {
		t.Fatalf("bulk-capable dispatched executors = %d, want 12", claimed)
	}
}

func TestCentralClaimSchedulerBatchFillDispatchesMultipleBeforeResult(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx := context.Background()

	const workerCount = 16
	candidates := make([]schedulerClaimCandidate, 0, workerCount)
	now := time.Now().Add(-time.Second)
	for i := 0; i < workerCount; i++ {
		candidates = append(candidates, schedulerClaimCandidate{
			event:      noopEvent(fmt.Sprintf("evt-batch-fill-%02d", i), "noop.created"),
			eligibleAt: now,
		})
	}
	executors := make([]*batchRecordingExecutor, 0, workerCount)
	for i := 0; i < workerCount; i++ {
		executor := &batchRecordingExecutor{
			recordingExecutor: &recordingExecutor{
				cfg:     schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Hour},
				permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: int64(i + 1)},
				allowed: true,
			},
			peekCandidates: candidates,
		}
		executors = append(executors, executor)
		scheduler.regs = append(scheduler.regs, &centralClaimRegistration{
			executor: executor,
			work:     make(chan centralClaimWork, 1),
		})
	}

	if !scheduler.fillIdleClaimCandidates(ctx, nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	dispatched := 0
	for _, reg := range scheduler.regs {
		if len(reg.work) > 0 {
			dispatched++
		}
	}
	if dispatched <= 1 {
		t.Fatalf("dispatched executions before completion = %d, want > 1", dispatched)
	}
	totalPeekCalls := 0
	peekLimit := 0
	for _, executor := range executors {
		totalPeekCalls += executor.peekCalls
		if executor.peekLimit > peekLimit {
			peekLimit = executor.peekLimit
		}
	}
	if totalPeekCalls != 1 {
		t.Fatalf("peek calls = %d, want 1", totalPeekCalls)
	}
	if peekLimit != workerCount {
		t.Fatalf("peek limit = %d, want %d", peekLimit, workerCount)
	}
	for i, executor := range executors {
		if executor.claimByIDCalls != 0 {
			t.Fatalf("executor %d ClaimByID calls during fill = %d, want 0", i, executor.claimByIDCalls)
		}
	}
	stats := scheduler.Stats()
	if stats.ClaimAttempts != 0 || stats.ClaimSuccess != 0 {
		t.Fatalf("scheduler claim stats before worker execution = attempts %d success %d, want 0/0", stats.ClaimAttempts, stats.ClaimSuccess)
	}
	if stats.PeekCandidateCount != workerCount {
		t.Fatalf("peek candidate count = %d, want %d", stats.PeekCandidateCount, workerCount)
	}
}

func TestCentralClaimSchedulerBatchFillReservesOnePermitPerCandidate(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	candidate := schedulerClaimCandidate{
		event:      noopEvent("evt-workitem-batch-width", "noop.created"),
		eligibleAt: time.Now().Add(-time.Second),
	}
	executor := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:    1,
				BatchSize:    32,
				PollInterval: time.Hour,
				ClaimPath:    ClaimPathWorkItem,
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 401},
			allowed: true,
		},
		peekCandidates: []schedulerClaimCandidate{candidate},
	}
	scheduler.regs = append(scheduler.regs, &centralClaimRegistration{
		executor: executor,
		work:     make(chan centralClaimWork, 1),
	})

	if !scheduler.fillIdleClaimCandidates(context.Background(), nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	if executor.permitBatchSize != 1 {
		t.Fatalf("permit batch size = %d, want one permit for one dispatched candidate", executor.permitBatchSize)
	}
	work := <-scheduler.regs[0].work
	if work.request.MaxClaims != 1 {
		t.Fatalf("dispatched MaxClaims = %d, want one claim for one dispatched candidate", work.request.MaxClaims)
	}
}

func TestCentralClaimSchedulerBatchCollectionExpandsCandidatePermit(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	executor := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:    1,
				BatchSize:    32,
				PollInterval: time.Hour,
				ClaimPath:    ClaimPathWorkItem,
			},
			permitDecisions: []ClaimLimiterDecision{
				{Allow: true, MaxClaims: 1, ReservationID: 501},
				{Allow: true, MaxClaims: 7, ReservationID: 502},
			},
		},
		peekCandidates: []schedulerClaimCandidate{{
			event:      noopEvent("evt-batch-expand", "noop.created"),
			eligibleAt: time.Now().Add(-time.Second),
		}},
		candidatePermitBatchSize: 8,
	}
	scheduler.regs = append(scheduler.regs, &centralClaimRegistration{
		executor: executor,
		work:     make(chan centralClaimWork, 1),
	})

	if !scheduler.fillIdleClaimCandidates(context.Background(), nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	if got := fmt.Sprint(executor.permitBatchSizes); got != "[1 7]" {
		t.Fatalf("permit request sizes = %s, want initial candidate 1 plus batch expansion 7", got)
	}
	work := <-scheduler.regs[0].work
	if work.request.MaxClaims != 8 {
		t.Fatalf("dispatched MaxClaims = %d, want expanded batch width 8", work.request.MaxClaims)
	}
}

func TestCentralClaimSchedulerCandidatePermitSizerPanicFallsBackToOne(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	executor := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:    1,
				BatchSize:    32,
				PollInterval: time.Hour,
				ClaimPath:    ClaimPathWorkItem,
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 551},
			allowed: true,
		},
		peekCandidates: []schedulerClaimCandidate{{
			event:      noopEvent("evt-batch-size-panic", "noop.created"),
			eligibleAt: time.Now().Add(-time.Second),
		}},
		candidatePermitPanic: "route resolver panic",
	}
	scheduler.regs = append(scheduler.regs, &centralClaimRegistration{
		executor: executor,
		work:     make(chan centralClaimWork, 1),
	})

	if !scheduler.fillIdleClaimCandidates(context.Background(), nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	work := <-scheduler.regs[0].work
	if work.request.MaxClaims != 1 {
		t.Fatalf("dispatched MaxClaims = %d after permit sizer panic, want safe fallback 1", work.request.MaxClaims)
	}
}

func TestWorkerSchedulerPermitSizerPreservesBatchCollectionWidth(t *testing.T) {
	w := &Worker{
		cfg: WorkerConfig{
			BatchSize: 32,
			ClaimPath: ClaimPathWorkItem,
		},
		resolver: batchCollectionResolver(32),
	}
	event := noopEvent("evt-batch-size", "noop.created")
	event.InstalledPackID = "pack-batch-size"
	event.RouteVersion = sql.NullInt64{Valid: true, Int64: 1}

	got := w.claimPermitBatchSizeForScheduler(context.Background(), schedulerClaimCandidate{event: event})
	if got != centralClaimWorkItemPermitBatchCap {
		t.Fatalf("candidate permit width = %d, want batch_collection cap %d", got, centralClaimWorkItemPermitBatchCap)
	}
}

func TestCentralClaimSchedulerReleasesBaseAndExpandedPermits(t *testing.T) {
	executor := &recordingExecutor{}
	reg := &centralClaimRegistration{resultCh: make(chan centralClaimResult, 1)}
	work := centralClaimWork{
		permit:            ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 601},
		additionalPermits: []ClaimLimiterDecision{{Allow: true, MaxClaims: 7, ReservationID: 602}},
		request: claimExecutionRequest{
			MaxClaims: 8,
			Event:     noopEvent("evt-release-expanded", "noop.created"),
		},
	}

	NewCentralClaimScheduler().executeRegisteredWork(context.Background(), nil, reg, executor, work)
	if len(executor.releasedDecisions) != 2 {
		t.Fatalf("released permits = %d, want base and expansion", len(executor.releasedDecisions))
	}
	if executor.releasedDecisions[0].ReservationID != 601 || executor.releasedDecisions[1].ReservationID != 602 {
		t.Fatalf("released reservation IDs = %d,%d, want 601,602", executor.releasedDecisions[0].ReservationID, executor.releasedDecisions[1].ReservationID)
	}
}

func TestWorkerSchedulerPermitSizerCachesImmutableRouteMode(t *testing.T) {
	resolver := &countingExecutionSpecResolver{stubRouteResolver: batchCollectionResolver(32)}
	w := &Worker{
		cfg: WorkerConfig{
			BatchSize: 32,
			ClaimPath: ClaimPathWorkItem,
		},
		resolver: resolver,
	}
	event := noopEvent("evt-batch-cache", "noop.created")
	event.InstalledPackID = "pack-batch-cache"
	event.RouteVersion = sql.NullInt64{Valid: true, Int64: 1}
	candidate := schedulerClaimCandidate{event: event}

	for i := 0; i < 2; i++ {
		if got := w.claimPermitBatchSizeForScheduler(context.Background(), candidate); got != centralClaimWorkItemPermitBatchCap {
			t.Fatalf("candidate permit width = %d, want %d", got, centralClaimWorkItemPermitBatchCap)
		}
	}
	if resolver.calls != 1 {
		t.Fatalf("route resolution calls = %d, want one immutable-version lookup", resolver.calls)
	}
}

func TestCentralClaimSchedulerBatchFillKeepsLegacyPermitSingle(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	candidate := schedulerClaimCandidate{
		event:      noopEvent("evt-legacy-batch-width", "noop.created"),
		eligibleAt: time.Now().Add(-time.Second),
	}
	executor := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:    1,
				BatchSize:    32,
				PollInterval: time.Hour,
				ClaimPath:    ClaimPathLegacyEvent,
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 402},
			allowed: true,
		},
		peekCandidates: []schedulerClaimCandidate{candidate},
	}
	scheduler.regs = append(scheduler.regs, &centralClaimRegistration{
		executor: executor,
		work:     make(chan centralClaimWork, 1),
	})

	if !scheduler.fillIdleClaimCandidates(context.Background(), nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	if executor.permitBatchSize != 1 {
		t.Fatalf("permit batch size = %d, want legacy single-claim width 1", executor.permitBatchSize)
	}
}

func TestCentralClaimSchedulerBatchFillPreservesQueueClassAffinity(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx := context.Background()

	bulkCandidate := schedulerClaimCandidate{
		event:      noopEvent("evt-bulk-affinity", "noop.created"),
		eligibleAt: time.Now().Add(-time.Second),
	}
	bulk := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg:     schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Hour, ClaimPath: ClaimPathWorkItem, QueueClasses: []string{connector_inbound_routing.QueueClassBulk}},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 301},
			allowed: true,
		},
		peekCandidates: []schedulerClaimCandidate{bulkCandidate},
	}
	interactive := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:    1,
				BatchSize:    1,
				PollInterval: time.Hour,
				ClaimPath:    ClaimPathWorkItem,
				QueueClasses: []string{connector_inbound_routing.QueueClassInteractive, connector_inbound_routing.QueueClassDefault},
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 302},
			allowed: true,
		},
		peekCandidates: []schedulerClaimCandidate{{
			event:      noopEvent("evt-interactive-affinity", "noop.created"),
			eligibleAt: time.Now().Add(-time.Second),
		}},
	}
	scheduler.regs = append(scheduler.regs,
		&centralClaimRegistration{executor: bulk, work: make(chan centralClaimWork, 1)},
		&centralClaimRegistration{executor: interactive, work: make(chan centralClaimWork, 1)},
	)

	if !scheduler.fillIdleClaimCandidates(ctx, nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	if bulk.peekCalls != 1 {
		t.Fatalf("bulk peek calls = %d, want 1", bulk.peekCalls)
	}
	if bulk.peekLimit != 1 {
		t.Fatalf("bulk peek limit = %d, want 1 compatible worker", bulk.peekLimit)
	}
	if interactive.peekCalls != 0 {
		t.Fatalf("interactive peek calls = %d, want excluded from bulk batch", interactive.peekCalls)
	}
	if len(scheduler.regs[0].work) != 1 {
		t.Fatal("bulk worker did not receive candidate")
	}
	if len(scheduler.regs[1].work) != 0 {
		t.Fatal("interactive worker received bulk candidate")
	}
	if len(interactive.permitBatchSizes) != 0 || interactive.releasedDecision.ReservationID != 0 {
		t.Fatalf("interactive permit requests/releases = %v/%d, want none for the unselected queue shape", interactive.permitBatchSizes, interactive.releasedDecision.ReservationID)
	}
}

func TestCentralClaimSchedulerFallsThroughEmptyQueueShape(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx := context.Background()

	latency := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:    1,
				BatchSize:    1,
				PollInterval: time.Hour,
				ClaimPath:    ClaimPathWorkItem,
				QueueClasses: []string{connector_inbound_routing.QueueClassInteractive, connector_inbound_routing.QueueClassDefault},
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 303},
			allowed: true,
		},
	}
	bulk := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:             1,
				BatchSize:             1,
				PollInterval:          time.Hour,
				ClaimPath:             ClaimPathWorkItem,
				QueueClasses:          []string{connector_inbound_routing.QueueClassInteractive, connector_inbound_routing.QueueClassDefault, connector_inbound_routing.QueueClassBulk},
				PreferredQueueClasses: []string{connector_inbound_routing.QueueClassBulk},
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 304},
			allowed: true,
		},
		peekCandidates: []schedulerClaimCandidate{{
			event:      noopEvent("evt-bulk-after-empty-latency", "noop.created"),
			eligibleAt: time.Now().Add(-time.Second),
		}},
	}
	scheduler.regs = append(scheduler.regs,
		&centralClaimRegistration{executor: latency, work: make(chan centralClaimWork, 1)},
		&centralClaimRegistration{executor: bulk, work: make(chan centralClaimWork, 1)},
	)

	if !scheduler.fillIdleClaimCandidates(ctx, nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	if latency.peekCalls != 1 {
		t.Fatalf("latency peek calls = %d, want 1", latency.peekCalls)
	}
	if latency.releasedDecision.ReservationID != 303 {
		t.Fatalf("latency released reservation = %d, want 303", latency.releasedDecision.ReservationID)
	}
	if bulk.peekCalls != 1 {
		t.Fatalf("bulk peek calls = %d, want 1 after empty latency shape", bulk.peekCalls)
	}
	if len(scheduler.regs[0].work) != 0 || len(scheduler.regs[1].work) != 1 {
		t.Fatalf("latency/bulk dispatched work = %d/%d, want 0/1", len(scheduler.regs[0].work), len(scheduler.regs[1].work))
	}
}

func TestCentralClaimSchedulerSeparatesPreferredQueueShapes(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx := context.Background()
	allowed := []string{
		connector_inbound_routing.QueueClassInteractive,
		connector_inbound_routing.QueueClassDefault,
		connector_inbound_routing.QueueClassBulk,
	}
	latencyPreferred := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:             1,
				BatchSize:             1,
				PollInterval:          time.Hour,
				ClaimPath:             ClaimPathWorkItem,
				QueueClasses:          allowed,
				PreferredQueueClasses: []string{connector_inbound_routing.QueueClassInteractive, connector_inbound_routing.QueueClassDefault},
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 401},
			allowed: true,
		},
		peekCandidates: []schedulerClaimCandidate{{
			event:      noopEvent("evt-latency-preferred", "noop.created"),
			eligibleAt: time.Now().Add(-time.Second),
		}},
	}
	bulkPreferred := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:             1,
				BatchSize:             1,
				PollInterval:          time.Hour,
				ClaimPath:             ClaimPathWorkItem,
				QueueClasses:          allowed,
				PreferredQueueClasses: []string{connector_inbound_routing.QueueClassBulk},
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 402},
			allowed: true,
		},
		peekCandidates: []schedulerClaimCandidate{{
			event:      noopEvent("evt-bulk-preferred", "noop.created"),
			eligibleAt: time.Now().Add(-time.Second),
		}},
	}
	scheduler.regs = append(scheduler.regs,
		&centralClaimRegistration{executor: latencyPreferred, work: make(chan centralClaimWork, 1)},
		&centralClaimRegistration{executor: bulkPreferred, work: make(chan centralClaimWork, 1)},
	)

	if !scheduler.fillIdleClaimCandidates(ctx, nil, time.Now()) {
		t.Fatal("first fillIdleClaimCandidates returned false")
	}
	if latencyPreferred.peekCalls != 1 || bulkPreferred.peekCalls != 0 {
		t.Fatalf("first preferred-shape peeks latency/bulk = %d/%d, want 1/0", latencyPreferred.peekCalls, bulkPreferred.peekCalls)
	}
	if len(scheduler.regs[0].work) != 1 || len(scheduler.regs[1].work) != 0 {
		t.Fatalf("first preferred-shape dispatch latency/bulk = %d/%d, want 1/0", len(scheduler.regs[0].work), len(scheduler.regs[1].work))
	}

	if !scheduler.fillIdleClaimCandidates(ctx, nil, time.Now()) {
		t.Fatal("second fillIdleClaimCandidates returned false")
	}
	if bulkPreferred.peekCalls != 1 || len(scheduler.regs[1].work) != 1 {
		t.Fatalf("second bulk-preferred peek/work = %d/%d, want 1/1", bulkPreferred.peekCalls, len(scheduler.regs[1].work))
	}
}

func TestCentralClaimSchedulerCombinesPreferredQueueShapesForStaleReclaim(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	allowed := []string{
		connector_inbound_routing.QueueClassInteractive,
		connector_inbound_routing.QueueClassDefault,
		connector_inbound_routing.QueueClassBulk,
	}
	latencyPreferred := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:             1,
				BatchSize:             1,
				PollInterval:          time.Hour,
				ClaimPath:             ClaimPathWorkItem,
				QueueClasses:          allowed,
				PreferredQueueClasses: []string{connector_inbound_routing.QueueClassInteractive, connector_inbound_routing.QueueClassDefault},
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 501, StaleReclaimOnly: true},
			allowed: true,
		},
		peekCandidates: []schedulerClaimCandidate{
			{event: noopEvent("evt-stale-a", "noop.created"), eligibleAt: time.Now().Add(-time.Minute)},
			{event: noopEvent("evt-stale-b", "noop.created"), eligibleAt: time.Now().Add(-time.Minute)},
		},
	}
	bulkPreferred := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg: schedulerConfig{
				MaxClaims:             1,
				BatchSize:             1,
				PollInterval:          time.Hour,
				ClaimPath:             ClaimPathWorkItem,
				QueueClasses:          allowed,
				PreferredQueueClasses: []string{connector_inbound_routing.QueueClassBulk},
			},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 502, StaleReclaimOnly: true},
			allowed: true,
		},
	}
	scheduler.regs = append(scheduler.regs,
		&centralClaimRegistration{executor: latencyPreferred, work: make(chan centralClaimWork, 1)},
		&centralClaimRegistration{executor: bulkPreferred, work: make(chan centralClaimWork, 1)},
	)

	if !scheduler.fillIdleClaimCandidates(context.Background(), nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false")
	}
	if latencyPreferred.peekCalls != 1 || latencyPreferred.peekLimit != 2 || bulkPreferred.peekCalls != 0 {
		t.Fatalf("stale preferred-shape peeks latency(limit)/bulk = %d(%d)/%d, want 1(2)/0", latencyPreferred.peekCalls, latencyPreferred.peekLimit, bulkPreferred.peekCalls)
	}
	if len(scheduler.regs[0].work) != 1 || len(scheduler.regs[1].work) != 1 {
		t.Fatalf("stale preferred-shape dispatch latency/bulk = %d/%d, want 1/1", len(scheduler.regs[0].work), len(scheduler.regs[1].work))
	}
}

func TestCentralClaimSchedulerSkipsInflightCandidatesAcrossOverlappingQueueShapes(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx := context.Background()
	eligibleAt := time.Now().Add(-time.Second)
	candidate := func(id string) schedulerClaimCandidate {
		return schedulerClaimCandidate{
			event:      noopEvent(id, "noop.created"),
			eligibleAt: eligibleAt,
		}
	}
	bulkCandidates := []schedulerClaimCandidate{
		candidate("evt-overlap-000"),
		candidate("evt-overlap-001"),
	}
	mixedCandidates := []schedulerClaimCandidate{
		candidate("evt-overlap-000"),
		candidate("evt-overlap-001"),
		candidate("evt-overlap-002"),
		candidate("evt-overlap-003"),
	}
	newExecutor := func(reservationID int, queueClasses []string, candidates []schedulerClaimCandidate) *batchRecordingExecutor {
		return &batchRecordingExecutor{
			recordingExecutor: &recordingExecutor{
				cfg: schedulerConfig{
					MaxClaims:    1,
					BatchSize:    1,
					PollInterval: time.Hour,
					ClaimPath:    ClaimPathWorkItem,
					QueueClasses: queueClasses,
				},
				permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: int64(reservationID)},
				allowed: true,
			},
			peekCandidates: candidates,
		}
	}
	bulkOne := newExecutor(401, []string{connector_inbound_routing.QueueClassBulk}, bulkCandidates)
	bulkTwo := newExecutor(402, []string{connector_inbound_routing.QueueClassBulk}, bulkCandidates)
	mixedOne := newExecutor(403, []string{connector_inbound_routing.QueueClassInteractive, connector_inbound_routing.QueueClassDefault, connector_inbound_routing.QueueClassBulk}, mixedCandidates)
	mixedTwo := newExecutor(404, []string{connector_inbound_routing.QueueClassInteractive, connector_inbound_routing.QueueClassDefault, connector_inbound_routing.QueueClassBulk}, mixedCandidates)
	scheduler.regs = append(scheduler.regs,
		&centralClaimRegistration{executor: bulkOne, work: make(chan centralClaimWork, 1)},
		&centralClaimRegistration{executor: bulkTwo, work: make(chan centralClaimWork, 1)},
		&centralClaimRegistration{executor: mixedOne, work: make(chan centralClaimWork, 1)},
		&centralClaimRegistration{executor: mixedTwo, work: make(chan centralClaimWork, 1)},
	)

	if !scheduler.fillIdleClaimCandidates(ctx, nil, time.Now()) {
		t.Fatal("first fillIdleClaimCandidates returned false, want batch fill path")
	}
	if len(scheduler.regs[0].work) != 1 || len(scheduler.regs[1].work) != 1 {
		t.Fatalf("bulk workers received work = %d/%d, want 1/1", len(scheduler.regs[0].work), len(scheduler.regs[1].work))
	}
	if !scheduler.fillIdleClaimCandidates(ctx, nil, time.Now()) {
		t.Fatal("second fillIdleClaimCandidates returned false, want mixed batch fill path")
	}
	if mixedOne.peekCalls != 1 {
		t.Fatalf("mixed peek calls = %d, want 1", mixedOne.peekCalls)
	}
	if mixedOne.peekLimit != 4 {
		t.Fatalf("mixed peek limit = %d, want 4 to look past 2 inflight duplicates", mixedOne.peekLimit)
	}
	if len(scheduler.regs[2].work) != 1 || len(scheduler.regs[3].work) != 1 {
		t.Fatalf("mixed workers received work = %d/%d, want 1/1", len(scheduler.regs[2].work), len(scheduler.regs[3].work))
	}
	got := []string{
		(<-scheduler.regs[2].work).request.Candidate.event.ID,
		(<-scheduler.regs[3].work).request.Candidate.event.ID,
	}
	want := []string{"evt-overlap-002", "evt-overlap-003"}
	if strings.Join(got, ",") != strings.Join(want, ",") {
		t.Fatalf("mixed candidates = %v, want %v", got, want)
	}
}

func TestCentralClaimSchedulerCASLoserReleasesPermitNoBusy(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx := context.Background()

	const workerCount = 16
	const winners = 8
	candidates := make([]schedulerClaimCandidate, 0, workerCount)
	claimResults := make(map[string]bool, workerCount)
	for i := 0; i < workerCount; i++ {
		candidates = append(candidates, schedulerClaimCandidate{
			event:      noopEvent(fmt.Sprintf("evt-cas-mixed-%02d", i), "noop.created"),
			eligibleAt: time.Now().Add(-time.Second),
		})
		claimResults[candidates[i].event.ID] = i < winners
	}
	executors := make([]*batchRecordingExecutor, 0, workerCount)
	for i := 0; i < workerCount; i++ {
		executor := &batchRecordingExecutor{
			recordingExecutor: &recordingExecutor{
				cfg:     schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Hour},
				permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: int64(i + 1)},
				allowed: true,
			},
			peekCandidates:  candidates,
			claimByIDResult: claimResults,
		}
		executors = append(executors, executor)
		scheduler.regs = append(scheduler.regs, &centralClaimRegistration{
			executor: executor,
			work:     make(chan centralClaimWork, 1),
		})
	}

	if !scheduler.fillIdleClaimCandidates(ctx, nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	stats := scheduler.Stats()
	if stats.BusyWorkers != workerCount {
		t.Fatalf("busy workers after dispatch = %d, want %d", stats.BusyWorkers, workerCount)
	}
	if stats.IdleWorkers != 0 {
		t.Fatalf("idle workers after dispatch = %d, want 0", stats.IdleWorkers)
	}
	if stats.ClaimCASLost != 0 {
		t.Fatalf("CAS losers before worker execution = %d, want 0", stats.ClaimCASLost)
	}
	scheduler.resultCh = make(chan centralClaimResult, workerCount)
	for _, reg := range scheduler.regs {
		work := <-reg.work
		scheduler.executeRegisteredWork(ctx, nil, reg, reg.executor, work)
	}
	scheduler.drainClaimResults()
	stats = scheduler.Stats()
	if stats.BusyWorkers != 0 {
		t.Fatalf("busy workers after results = %d, want 0", stats.BusyWorkers)
	}
	if stats.IdleWorkers != workerCount {
		t.Fatalf("idle workers after results = %d, want %d", stats.IdleWorkers, workerCount)
	}
	if stats.ClaimSuccess != winners {
		t.Fatalf("claim success = %d, want %d", stats.ClaimSuccess, winners)
	}
	if stats.ClaimCASLost != workerCount-winners {
		t.Fatalf("CAS losers = %d, want %d", stats.ClaimCASLost, workerCount-winners)
	}
	for i := winners; i < workerCount; i++ {
		if len(scheduler.regs[i].work) != 0 {
			t.Fatalf("loser %d received dispatched work", i)
		}
		if executors[i].releasedDecision.ReservationID == 0 || executors[i].releasedClaimRows != 0 {
			t.Fatalf("loser %d release = reservation %d rows %d, want reservation and 0 rows", i, executors[i].releasedDecision.ReservationID, executors[i].releasedClaimRows)
		}
	}
}

func TestCentralClaimSchedulerPartialBatchRefillsBeforePollInterval(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx := context.Background()

	const workerCount = 16
	const candidateCount = 2
	candidates := make([]schedulerClaimCandidate, 0, candidateCount)
	now := time.Now().Add(-time.Second)
	for i := 0; i < candidateCount; i++ {
		candidates = append(candidates, schedulerClaimCandidate{
			event:      noopEvent(fmt.Sprintf("evt-partial-refill-%02d", i), "noop.created"),
			eligibleAt: now,
		})
	}
	for i := 0; i < workerCount; i++ {
		executor := &batchRecordingExecutor{
			recordingExecutor: &recordingExecutor{
				cfg:     schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Hour},
				permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: int64(i + 1)},
				allowed: true,
			},
			peekCandidates: candidates,
		}
		scheduler.regs = append(scheduler.regs, &centralClaimRegistration{
			executor: executor,
			work:     make(chan centralClaimWork, 1),
		})
	}

	if !scheduler.fillIdleClaimCandidates(ctx, nil, time.Now()) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	dispatched := 0
	deferred := 0
	for _, reg := range scheduler.regs {
		if len(reg.work) > 0 {
			dispatched++
			continue
		}
		if reg.nextPoll.IsZero() {
			t.Fatal("idle registration was not dispatched and was not deferred")
		}
		if delay := time.Until(reg.nextPoll); delay > time.Second {
			t.Fatalf("idle registration deferred for %s, want bounded refill delay below 1s", delay)
		}
		deferred++
	}
	if dispatched != candidateCount {
		t.Fatalf("dispatched = %d, want %d", dispatched, candidateCount)
	}
	if deferred != workerCount-candidateCount {
		t.Fatalf("deferred = %d, want %d", deferred, workerCount-candidateCount)
	}
}

func TestCentralClaimSchedulerEmptyBatchHonorsConfiguredPollInterval(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	pollInterval := time.Hour
	executor := &batchRecordingExecutor{
		recordingExecutor: &recordingExecutor{
			cfg:     schedulerConfig{BatchSize: 1, PollInterval: pollInterval},
			permit:  ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: 1},
			allowed: true,
		},
	}
	reg := &centralClaimRegistration{executor: executor, work: make(chan centralClaimWork, 1)}
	scheduler.regs = []*centralClaimRegistration{reg}

	before := time.Now()
	if !scheduler.fillIdleClaimCandidates(context.Background(), nil, before) {
		t.Fatal("fillIdleClaimCandidates returned false, want batch fill path")
	}
	if reg.nextPoll.Before(before.Add(pollInterval)) {
		t.Fatalf("empty batch next poll = %v, want no earlier than configured interval %v", reg.nextPoll, pollInterval)
	}
	if executor.releasedDecision.ReservationID != 1 || executor.releasedClaimRows != 0 {
		t.Fatalf("empty batch release = reservation %d rows %d, want reservation 1 and 0 rows", executor.releasedDecision.ReservationID, executor.releasedClaimRows)
	}
}

func TestCentralClaimSchedulerNoClaimAttemptClearsDeferredWorkers(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	scheduler.wakeCh = make(chan struct{}, 1)
	deferred := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{BatchSize: 1, PollInterval: time.Hour}},
		nextPoll: time.Now().Add(time.Hour),
	}
	attempted := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{BatchSize: 1, PollInterval: time.Hour}},
		busy:     true,
	}
	scheduler.regs = []*centralClaimRegistration{attempted, deferred}

	scheduler.handleClaimResult(centralClaimResult{
		reg: attempted,
		result: claimExecutionResult{
			ClaimAttempted: true,
			Claimed:        false,
		},
	})

	if attempted.busy {
		t.Fatal("attempted registration stayed busy after no-claim result")
	}
	if !deferred.nextPoll.IsZero() {
		t.Fatalf("deferred registration next poll = %v, want cleared", deferred.nextPoll)
	}
	select {
	case <-scheduler.wakeCh:
	default:
		t.Fatal("no-claim attempt did not signal scheduler wake")
	}
}

func TestCentralClaimSchedulerTransientClaimErrorClearsDeferredWorkers(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	scheduler.wakeCh = make(chan struct{}, 1)
	deferred := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{BatchSize: 1, PollInterval: time.Hour}},
		nextPoll: time.Now().Add(time.Hour),
	}
	attempted := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{BatchSize: 1, PollInterval: time.Hour}},
		busy:     true,
	}
	scheduler.regs = []*centralClaimRegistration{attempted, deferred}

	scheduler.handleClaimResult(centralClaimResult{
		reg: attempted,
		result: claimExecutionResult{
			ClaimAttempted: true,
			ClaimErr:       errors.New("invalid connection"),
		},
	})

	if attempted.busy {
		t.Fatal("attempted registration stayed busy after transient claim error")
	}
	if !deferred.nextPoll.IsZero() {
		t.Fatalf("deferred registration next poll = %v, want cleared", deferred.nextPoll)
	}
	select {
	case <-scheduler.wakeCh:
	default:
		t.Fatal("transient claim error did not signal scheduler wake")
	}
}

func TestCentralClaimSchedulerTransientProcessingErrorDoesNotExhaustMaxClaims(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	scheduler.wakeCh = make(chan struct{}, 1)
	deferred := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{BatchSize: 1, PollInterval: time.Hour}},
		nextPoll: time.Now().Add(time.Hour),
	}
	attempted := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Hour}},
		busy:     true,
	}
	scheduler.regs = []*centralClaimRegistration{attempted, deferred}

	scheduler.handleClaimResult(centralClaimResult{
		reg: attempted,
		result: claimExecutionResult{
			Claimed:       true,
			ClaimedRows:   1,
			ProcessingErr: errors.New("invalid connection"),
		},
	})

	if attempted.busy {
		t.Fatal("attempted registration stayed busy after transient processing error")
	}
	if attempted.processed != 0 {
		t.Fatalf("attempted processed = %d, want 0 so MaxClaims is not exhausted by transient processing error", attempted.processed)
	}
	if !deferred.nextPoll.IsZero() {
		t.Fatalf("deferred registration next poll = %v, want cleared", deferred.nextPoll)
	}
	select {
	case <-scheduler.wakeCh:
	default:
		t.Fatal("transient processing error did not signal scheduler wake")
	}
	if scheduler.allRegistrationsDone() {
		t.Fatal("scheduler considered registrations done after transient processing error")
	}
}

func TestCentralClaimSchedulerCompletionClearsDeferredEmptyWorkers(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	const workerCount = 16
	executeBlock := make(chan struct{})
	executors := make([]*recordingExecutor, 0, workerCount)
	started := make([]chan struct{}, workerCount)
	initialClaims := make([]chan struct{}, workerCount)
	for i := 0; i < workerCount; i++ {
		started[i] = make(chan struct{})
		initialClaims[i] = make(chan struct{})
		executor := &recordingExecutor{
			cfg:            schedulerConfig{MaxClaims: 1, BatchSize: 1, PollInterval: time.Hour},
			permit:         ClaimLimiterDecision{Allow: true, MaxClaims: 1, ReservationID: int64(i + 1)},
			allowed:        true,
			executeStarted: started[i],
			executeBlock:   executeBlock,
			claimCompleted: initialClaims[i],
		}
		if i == 0 {
			executor.claimEvents = []*connector_events.ConnectorEvent{noopEvent("evt-lane-head-00", "noop.created")}
		}
		executors = append(executors, executor)
	}

	errs := make(chan error, len(executors))
	for _, executor := range executors {
		go func(executor *recordingExecutor) {
			errs <- scheduler.run(ctx, nil, executor)
		}(executor)
	}

	select {
	case <-started[0]:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for first worker to start")
	}
	for i, claimed := range initialClaims {
		select {
		case <-claimed:
		case <-time.After(time.Second):
			t.Fatalf("worker %d did not complete its initial claim", i)
		}
	}
	for i := 1; i < workerCount; i++ {
		executors[i].claimEvents = []*connector_events.ConnectorEvent{
			noopEvent(fmt.Sprintf("evt-lane-head-%02d", i), "noop.created"),
		}
	}

	close(executeBlock)
	for i := 1; i < workerCount; i++ {
		select {
		case <-started[i]:
		case <-time.After(time.Second):
			t.Fatalf("worker %d stayed deferred after completion wake", i)
		}
	}

	cancel()
	for range executors {
		err := <-errs
		if err != nil && !errors.Is(err, context.Canceled) {
			t.Fatalf("scheduler.run returned error: %v", err)
		}
	}
}

func TestCentralClaimSchedulerCompletionSignalsWake(t *testing.T) {
	scheduler := NewCentralClaimScheduler()
	wakeCh := make(chan struct{}, 1)
	completed := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{BatchSize: 1, PollInterval: time.Hour}},
		busy:     true,
	}
	deferred := &centralClaimRegistration{
		executor: &recordingExecutor{cfg: schedulerConfig{BatchSize: 1, PollInterval: time.Hour}},
		nextPoll: time.Now().Add(time.Hour),
	}
	scheduler.wakeCh = wakeCh
	scheduler.regs = []*centralClaimRegistration{completed, deferred}

	scheduler.handleClaimResult(centralClaimResult{
		reg:    completed,
		result: claimExecutionResult{Claimed: true, ClaimedRows: 1},
	})

	if completed.busy {
		t.Fatal("completed registration remained busy")
	}
	if !deferred.nextPoll.IsZero() {
		t.Fatalf("deferred registration next poll = %v, want cleared", deferred.nextPoll)
	}
	select {
	case <-wakeCh:
	default:
		t.Fatal("completion did not signal scheduler wake")
	}
}

func TestWorkItemClaimFirstPerEventUsesAllWorkersWithBatchConfig(t *testing.T) {
	const workerCount = 16
	now := time.Now().UTC()
	claims := newConcurrentWorkItemClaims(workerCount, now)
	events := make(map[string]*connector_events.ConnectorEvent, workerCount)
	for i := 0; i < workerCount; i++ {
		eventID := fmt.Sprintf("event-direct-clean-%02d", i)
		sourceID := fmt.Sprintf("source-%02d", i)
		payload, err := EncodeQueueEnvelope("contact", sourceID, json.RawMessage(`{"id":"`+sourceID+`","propertyName":"nexuscrm_pressure_noop"}`))
		if err != nil {
			t.Fatalf("EncodeQueueEnvelope: %v", err)
		}
		events[eventID] = &connector_events.ConnectorEvent{
			ID:              eventID,
			Namespace:       workerTestNS,
			InstalledPackID: "pack-test",
			EventKind:       "contact.propertychange",
			QueueClass:      connector_inbound_routing.QueueClassBulk,
			Status:          "pending",
			ClaimToken:      "lease-" + eventID,
			Payload:         payload,
			SourceLaneKey:   fmt.Sprintf("lane-%02d", i),
			RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
			CreatedDate:     now.Add(time.Duration(i) * time.Microsecond),
		}
	}
	bridge := newBlockingTerminalWorkItemBridge(workerCount)
	claims.events = events
	claims.terminal = bridge
	limiter := NewSQLClaimLimiter(SQLClaimLimiterConfig{
		Pressure: &fakePressureProvider{snapshot: connector_events.ClaimPressureSnapshot{
			InstalledPackID:      "pack-test",
			QueueClasses:         []string{connector_inbound_routing.QueueClassBulk},
			ProcessingCount:      0,
			StaleProcessingCount: 0,
			ClaimableCount:       workerCount,
			OldestClaimableAge:   0,
		}},
		MaxProcessingRows: 32,
	})
	scheduler := NewCentralClaimScheduler()
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/contact.propertychange": {
				FlowDeveloperName: "NoopPressure__c",
				InvocationMode:    connector_inbound_routing.InvocationModePerEvent,
				QueueClass:        connector_inbound_routing.QueueClassBulk,
				FlowExecutionFilter: &connector_inbound_routing.PayloadValueFilter{
					Path:      ".propertyName",
					EqualsAny: []string{"never_matches_direct_clean"},
				},
			},
		},
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	errs := make(chan error, workerCount)
	for i := 0; i < workerCount; i++ {
		w, err := NewWorkItemWorkerForTest(context.Background(), WorkerConfig{
			Namespace:                     workerTestNS,
			PackInstallID:                 "pack-test",
			WorkerID:                      fmt.Sprintf("worker-direct-clean-%02d", i),
			BatchSize:                     32,
			PollInterval:                  time.Millisecond,
			MaxClaims:                     1,
			QueueClasses:                  []string{connector_inbound_routing.QueueClassBulk},
			ClaimLimiter:                  limiter,
			WorkItemSchedulerOwnerID:      "scheduler-owner",
			WorkItemSchedulerFencingToken: "scheduler-fence",
			WorkItemLeaseDuration:         time.Minute,
		}, claims, &fakeSyncState{}, nil, &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}, resolver)
		if err != nil {
			t.Fatalf("NewWorkItemWorkerForTest %d: %v", i, err)
		}
		preference := []string{connector_inbound_routing.QueueClassBulk}
		w.SetWorkItemQueueClassPreference(preference)
		preference[0] = "mutated-input"
		gotPreference := w.WorkItemQueueClassPreference()
		if len(gotPreference) != 1 || gotPreference[0] != connector_inbound_routing.QueueClassBulk {
			t.Fatalf("WorkItem queue preference = %v, want [bulk]", gotPreference)
		}
		gotPreference[0] = "mutated-output"
		if got := w.WorkItemQueueClassPreference(); len(got) != 1 || got[0] != connector_inbound_routing.QueueClassBulk {
			t.Fatalf("WorkItem queue preference after caller mutation = %v, want [bulk]", got)
		}
		w.UseCentralClaimScheduler(scheduler)
		go func() {
			errs <- w.RunClaimFirstUntilDrained(ctx, nil)
		}()
	}

	select {
	case <-bridge.allActive:
	case <-ctx.Done():
		t.Fatalf("max active WorkItem terminalizations = %d, want %d; claimed=%d", bridge.MaxActive(), workerCount, claims.ClaimCalls())
	}
	if got := bridge.MaxActive(); got != workerCount {
		t.Fatalf("max active WorkItem terminalizations = %d, want %d", got, workerCount)
	}
	bridge.Release()
	for i := 0; i < workerCount; i++ {
		if err := <-errs; err != nil && !errors.Is(err, context.Canceled) {
			t.Fatalf("worker %d RunClaimFirstUntilDrained: %v", i, err)
		}
	}
	if got := claims.Completed(); got != workerCount {
		t.Fatalf("completed WorkItems = %d, want %d", got, workerCount)
	}
}

func TestWorkerUsesConfiguredQueueClassesForClaimAndPeek(t *testing.T) {
	fakes := newFakeEvents(noopEvent("evt-bulk", "noop.created"))
	w := &Worker{
		cfg: WorkerConfig{
			PackInstallID: "pack-test",
			WorkerID:      "worker-bulk",
			QueueClasses:  []string{connector_inbound_routing.QueueClassBulk},
		},
		events: fakes,
	}

	if _, _, err := w.claimNext(context.Background(), false); err != nil {
		t.Fatalf("claimNext: %v", err)
	}
	if got := strings.Join(fakes.claimNextClasses, ","); got != connector_inbound_routing.QueueClassBulk {
		t.Fatalf("claimNext queue classes = %q, want %q", got, connector_inbound_routing.QueueClassBulk)
	}

	if _, err := w.peekBatch(context.Background(), 1, false); err != nil {
		t.Fatalf("peekBatch: %v", err)
	}
	if got := strings.Join(fakes.peekBatchClasses, ","); got != connector_inbound_routing.QueueClassBulk {
		t.Fatalf("peekBatch queue classes = %q, want %q", got, connector_inbound_routing.QueueClassBulk)
	}
}

func TestWorkerLimpPermitRoutesToStaleReclaim(t *testing.T) {
	// A limp permit (staleOnly=true) must route claim/peek to the stale-reclaim-only
	// path, never the general ClaimNext/PeekBatch — otherwise a degraded tick could
	// admit a fresh pending row past the cap.
	fakes := newFakeEvents(noopEvent("evt-stale", "noop.created"))
	w := &Worker{
		cfg:    WorkerConfig{PackInstallID: "pack-test", WorkerID: "worker-limp"},
		events: fakes,
	}

	if _, _, err := w.claimNext(context.Background(), true); err != nil {
		t.Fatalf("claimNext(staleOnly): %v", err)
	}
	if fakes.reclaimStaleCalls != 1 {
		t.Fatalf("reclaimStaleCalls = %d, want 1 (limp claim must take the stale path)", fakes.reclaimStaleCalls)
	}
	if _, err := w.peekBatch(context.Background(), 1, true); err != nil {
		t.Fatalf("peekBatch(staleOnly): %v", err)
	}
	if fakes.peekStaleCalls != 1 {
		t.Fatalf("peekStaleCalls = %d, want 1 (limp peek must take the stale path)", fakes.peekStaleCalls)
	}

	// Normal permits must NOT take the stale path.
	if _, _, err := w.claimNext(context.Background(), false); err != nil {
		t.Fatalf("claimNext(normal): %v", err)
	}
	if _, err := w.peekBatch(context.Background(), 1, false); err != nil {
		t.Fatalf("peekBatch(normal): %v", err)
	}
	if fakes.reclaimStaleCalls != 1 || fakes.peekStaleCalls != 1 {
		t.Fatalf("normal path took the stale route: reclaimStale=%d peekStale=%d, want 1/1", fakes.reclaimStaleCalls, fakes.peekStaleCalls)
	}
}

func TestWorkerWorkItemCapacityFullPreservesThrottle(t *testing.T) {
	w := &Worker{cfg: WorkerConfig{ClaimPath: ClaimPathWorkItem, BatchSize: 1}}

	decision := w.capClaimLimiterDecision(ClaimLimiterDecision{
		Allow:         false,
		Reason:        "processing_capacity_full",
		ReservationID: 44,
	})
	if decision.Allow {
		t.Fatalf("decision.Allow = true reason=%q, want limiter throttle preserved", decision.Reason)
	}
	if decision.Reason != "processing_capacity_full" {
		t.Fatalf("decision.Reason = %q, want processing_capacity_full", decision.Reason)
	}
	if decision.MaxClaims != 1 {
		t.Fatalf("decision.MaxClaims = %d, want 1", decision.MaxClaims)
	}
	if decision.ReservationID != 44 {
		t.Fatalf("decision.ReservationID = %d, want original reservation 44", decision.ReservationID)
	}

	eventWorker := &Worker{cfg: WorkerConfig{ClaimPath: ClaimPathLegacyEvent, BatchSize: 1}}
	eventDecision := eventWorker.capClaimLimiterDecision(ClaimLimiterDecision{
		Allow:  false,
		Reason: "processing_capacity_full",
	})
	if eventDecision.Allow {
		t.Fatalf("event path decision = %+v, want generic limiter throttle preserved", eventDecision)
	}
}

func TestWorkerWorkItemDBPoolExhaustedPreservesThrottle(t *testing.T) {
	w := &Worker{cfg: WorkerConfig{ClaimPath: ClaimPathWorkItem, BatchSize: 1}}

	decision := w.capClaimLimiterDecision(ClaimLimiterDecision{
		Allow:  false,
		Reason: "db_pool_exhausted",
		Delay:  300 * time.Millisecond,
	})
	if decision.Allow {
		t.Fatalf("decision.Allow = true reason=%q, want DB pool throttle preserved", decision.Reason)
	}
	if decision.Reason != "db_pool_exhausted" {
		t.Fatalf("decision.Reason = %q, want db_pool_exhausted", decision.Reason)
	}
	if decision.MaxClaims != 1 {
		t.Fatalf("decision.MaxClaims = %d, want 1", decision.MaxClaims)
	}
	if decision.Delay != 300*time.Millisecond {
		t.Fatalf("decision.Delay = %s, want 300ms", decision.Delay)
	}

	eventWorker := &Worker{cfg: WorkerConfig{ClaimPath: ClaimPathLegacyEvent, BatchSize: 1}}
	eventDecision := eventWorker.capClaimLimiterDecision(ClaimLimiterDecision{
		Allow:  false,
		Reason: "db_pool_exhausted",
		Delay:  300 * time.Millisecond,
	})
	if eventDecision.Allow {
		t.Fatalf("event path decision = %+v, want generic limiter throttle preserved", eventDecision)
	}
}

func TestWorkerWorkItemCapacityFullMinProgressCapsSingleClaim(t *testing.T) {
	w := &Worker{cfg: WorkerConfig{ClaimPath: ClaimPathWorkItem, BatchSize: 1}}

	decision := w.capClaimLimiterDecision(ClaimLimiterDecision{
		Allow:     true,
		MaxClaims: 16,
	})
	if decision.MaxClaims != 1 {
		t.Fatalf("decision.MaxClaims = %d, want single WorkItem claim cap", decision.MaxClaims)
	}
}

func TestWorkerTerminalFinalizeWakesScheduler(t *testing.T) {
	ev := noopEvent("evt-finalize-wake", "noop.created")
	ev.ClaimToken = "claim-token"
	scheduler := &recordingScheduler{}
	w := &Worker{
		cfg:       WorkerConfig{WorkerID: "worker-finalize-wake"},
		events:    newFakeEvents(ev),
		scheduler: scheduler,
	}

	if err := w.markProcessed(ev); err != nil {
		t.Fatalf("markProcessed: %v", err)
	}
	if scheduler.wakes != 1 {
		t.Fatalf("scheduler wakes after markProcessed = %d, want 1", scheduler.wakes)
	}
	if err := w.markSkipped(ev); err != nil {
		t.Fatalf("markSkipped: %v", err)
	}
	if scheduler.wakes != 2 {
		t.Fatalf("scheduler wakes after markSkipped = %d, want 2", scheduler.wakes)
	}
}

func TestWorkerWorkItemQueuePressureUnavailablePreservesStaleOnly(t *testing.T) {
	w := &Worker{cfg: WorkerConfig{ClaimPath: ClaimPathWorkItem, BatchSize: 1}}

	decision := w.capClaimLimiterDecision(ClaimLimiterDecision{
		Allow:            true,
		Reason:           "queue_pressure_unavailable",
		MaxClaims:        1,
		ReservationID:    55,
		StaleReclaimOnly: true,
	})
	if !decision.Allow {
		t.Fatalf("decision.Allow = false reason=%q, want stale-only permit preserved", decision.Reason)
	}
	if !decision.StaleReclaimOnly {
		t.Fatal("decision.StaleReclaimOnly = false, want stale-only pressure fallback preserved")
	}
	if decision.Reason != "queue_pressure_unavailable" {
		t.Fatalf("decision.Reason = %q, want queue_pressure_unavailable", decision.Reason)
	}
	if decision.MaxClaims != 1 {
		t.Fatalf("decision.MaxClaims = %d, want 1", decision.MaxClaims)
	}
	if decision.ReservationID != 55 {
		t.Fatalf("decision.ReservationID = %d, want original reservation 55", decision.ReservationID)
	}

	eventWorker := &Worker{cfg: WorkerConfig{ClaimPath: ClaimPathLegacyEvent, BatchSize: 1}}
	eventDecision := eventWorker.capClaimLimiterDecision(ClaimLimiterDecision{
		Allow:            true,
		Reason:           "queue_pressure_unavailable",
		MaxClaims:        1,
		StaleReclaimOnly: true,
	})
	if !eventDecision.StaleReclaimOnly {
		t.Fatalf("event path decision = %+v, want stale-only limp preserved", eventDecision)
	}
}

func TestWorkerLimpPermitWithoutStaleSupportClaimsNothing(t *testing.T) {
	// Safety fallback: if the events service does NOT implement stale-reclaim, a
	// limp permit reclaims NOTHING rather than falling back to a general claim that
	// could over-admit.
	w := &Worker{
		cfg:    WorkerConfig{PackInstallID: "pack-test", WorkerID: "worker-limp"},
		events: generalOnlyEvents{inner: newFakeEvents(noopEvent("evt-x", "noop.created"))},
	}
	ev, ok, err := w.claimNext(context.Background(), true)
	if err != nil {
		t.Fatalf("claimNext(staleOnly) no-support: %v", err)
	}
	if ok || ev != nil {
		t.Fatalf("limp claim without stale support = (%v, %v), want nothing claimed", ev, ok)
	}
	peeked, err := w.peekBatch(context.Background(), 1, true)
	if err != nil {
		t.Fatalf("peekBatch(staleOnly) no-support: %v", err)
	}
	if len(peeked) != 0 {
		t.Fatalf("limp peek without stale support = %v, want empty", peeked)
	}
}

// generalOnlyEvents exposes ONLY EventsService (not staleReclaimingEventsService),
// to exercise the worker's safety fallback when the injected service lacks
// stale-only reclaim.
type generalOnlyEvents struct{ inner *fakeEventsService }

func (g generalOnlyEvents) ClaimNext(ctx context.Context, packID, workerID string) (*connector_events.ConnectorEvent, bool, error) {
	return g.inner.ClaimNext(ctx, packID, workerID)
}
func (g generalOnlyEvents) PeekBatch(ctx context.Context, packID string, n int) ([]*connector_events.ConnectorEvent, error) {
	return g.inner.PeekBatch(ctx, packID, n)
}
func (g generalOnlyEvents) ClaimByID(ctx context.Context, eventID, workerID, packID string, deliveryID sql.NullString, deliverySequence int) (string, time.Time, bool, error) {
	return g.inner.ClaimByID(ctx, eventID, workerID, packID, deliveryID, deliverySequence)
}
func (g generalOnlyEvents) MarkProcessed(ctx context.Context, eventID, claimToken string) error {
	return g.inner.MarkProcessed(ctx, eventID, claimToken)
}
func (g generalOnlyEvents) MarkSkipped(ctx context.Context, eventID, claimToken string) error {
	return g.inner.MarkSkipped(ctx, eventID, claimToken)
}
func (g generalOnlyEvents) MarkFailed(ctx context.Context, eventID, claimToken, errorMsg string) error {
	return g.inner.MarkFailed(ctx, eventID, claimToken, errorMsg)
}

type concurrentWorkItemClaims struct {
	mu            sync.Mutex
	works         []*connector_workqueue.WorkItem
	events        map[string]*connector_events.ConnectorEvent
	terminal      *blockingTerminalWorkItemBridge
	claimCalls    int
	completeCalls int
}

var _ WorkItemService = (*concurrentWorkItemClaims)(nil)

func newConcurrentWorkItemClaims(count int, now time.Time) *concurrentWorkItemClaims {
	works := make([]*connector_workqueue.WorkItem, 0, count)
	for i := 0; i < count; i++ {
		eventID := fmt.Sprintf("event-direct-clean-%02d", i)
		works = append(works, &connector_workqueue.WorkItem{
			WorkID:           fmt.Sprintf("work-direct-clean-%02d", i),
			EventID:          eventID,
			InstalledPackID:  "pack-test",
			WorkType:         connector_workqueue.WorkTypeConnectorEvent,
			QueueClass:       connector_inbound_routing.QueueClassBulk,
			State:            connector_workqueue.WorkStateProcessing,
			LeaseToken:       "lease-" + eventID,
			Attempt:          1,
			SourceLaneKey:    fmt.Sprintf("lane-%02d", i),
			LaneOrdinal:      1,
			Generation:       0,
			CreatedDate:      now.Add(time.Duration(i) * time.Microsecond),
			LastModifiedDate: now,
		})
	}
	return &concurrentWorkItemClaims{works: works}
}

func (f *concurrentWorkItemClaims) AcquireSchedulerLease(context.Context, connector_workqueue.SchedulerLeaseInput) (bool, error) {
	return true, nil
}

func (f *concurrentWorkItemClaims) RenewSchedulerLease(context.Context, connector_workqueue.SchedulerLeaseInput) (bool, error) {
	return true, nil
}

func (f *concurrentWorkItemClaims) ClaimNextWorkItem(ctx context.Context, in connector_workqueue.ClaimWorkItemInput) (*connector_workqueue.WorkItem, bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.claimCalls++
	wantWorkTypes := []string{connector_workqueue.WorkTypeConnectorEvent, connector_workqueue.WorkTypeWorkflowActionApply}
	if got := strings.Join(connector_workqueue.ClaimWorkTypes(ctx), ","); got != strings.Join(wantWorkTypes, ",") {
		return nil, false, fmt.Errorf("WorkItem ClaimWorkTypes = %q, want %q", got, strings.Join(wantWorkTypes, ","))
	}
	if got := strings.Join(in.QueueClasses, ","); got != connector_inbound_routing.QueueClassBulk {
		return nil, false, fmt.Errorf("WorkItem QueueClasses = %q, want bulk", got)
	}
	if len(f.works) == 0 {
		return nil, false, nil
	}
	work := *f.works[0]
	f.works = f.works[1:]
	work.LeaseToken = in.LeaseToken
	work.LeaseOwner = in.OwnerID
	work.LeaseUntil = sql.NullTime{Time: in.LeaseUntil, Valid: true}
	return &work, true, nil
}

func (*concurrentWorkItemClaims) HonorsClaimWorkTypes() bool { return true }

func (*concurrentWorkItemClaims) RequireWorkItemOnlyExecutionCutover(context.Context) error {
	return nil
}

func (f *concurrentWorkItemClaims) PeekClaimableWorkItems(ctx context.Context, _ connector_workqueue.ClaimWorkItemInput, limit int) ([]*connector_workqueue.WorkItem, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	wantWorkTypes := []string{connector_workqueue.WorkTypeConnectorEvent, connector_workqueue.WorkTypeWorkflowActionApply}
	if got := strings.Join(connector_workqueue.ClaimWorkTypes(ctx), ","); got != strings.Join(wantWorkTypes, ",") {
		return nil, fmt.Errorf("WorkItem ClaimWorkTypes = %q, want %q", got, strings.Join(wantWorkTypes, ","))
	}
	if limit > len(f.works) {
		limit = len(f.works)
	}
	out := make([]*connector_workqueue.WorkItem, 0, limit)
	for i := 0; i < limit; i++ {
		work := *f.works[i]
		out = append(out, &work)
	}
	return out, nil
}

func (f *concurrentWorkItemClaims) ClaimWorkItemByEvent(_ context.Context, in connector_workqueue.ClaimWorkItemByEventInput) (*connector_workqueue.WorkItem, bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.claimCalls++
	for i, work := range f.works {
		if work.EventID != in.EventID {
			continue
		}
		claimed := *work
		claimed.LeaseToken = in.LeaseToken
		claimed.LeaseOwner = in.OwnerID
		claimed.LeaseUntil = sql.NullTime{Time: in.LeaseUntil, Valid: true}
		f.works = append(f.works[:i], f.works[i+1:]...)
		return &claimed, true, nil
	}
	return nil, false, nil
}

func (f *concurrentWorkItemClaims) ClaimStaleWorkItemByEvent(ctx context.Context, in connector_workqueue.ClaimWorkItemByEventInput) (*connector_workqueue.WorkItem, bool, error) {
	return f.ClaimWorkItemByEvent(ctx, in)
}

func (*concurrentWorkItemClaims) ResolveClaimableWorkItemGeneration(_ context.Context, in connector_workqueue.ResolveClaimableGenerationInput) (int64, bool, error) {
	return in.Generation, true, nil
}

func (f *concurrentWorkItemClaims) LoadEventForWorkItem(_ context.Context, eventID string) (*connector_events.ConnectorEvent, error) {
	event := f.events[eventID]
	if event == nil {
		return nil, connector_events.ErrEventNotFound
	}
	copy := *event
	return &copy, nil
}

func (f *concurrentWorkItemClaims) LoadEventsForWorkItems(ctx context.Context, eventIDs []string) (map[string]*connector_events.ConnectorEvent, error) {
	result := make(map[string]*connector_events.ConnectorEvent, len(eventIDs))
	for _, eventID := range eventIDs {
		event, err := f.LoadEventForWorkItem(ctx, eventID)
		if err != nil {
			return nil, err
		}
		result[eventID] = event
	}
	return result, nil
}

func (f *concurrentWorkItemClaims) LoadSourceProjectionPayloads(ctx context.Context, eventID string) ([]*connector_events.ConnectorEvent, error) {
	event, err := f.LoadEventForWorkItem(ctx, eventID)
	if err != nil {
		return nil, err
	}
	return []*connector_events.ConnectorEvent{event}, nil
}

func (*concurrentWorkItemClaims) FailWorkItem(_ context.Context, in connector_workqueue.FailWorkItemInput) (*connector_workqueue.FailWorkItemResult, bool, error) {
	return &connector_workqueue.FailWorkItemResult{State: connector_workqueue.WorkStateRetry, FailureCount: 1, RunAfter: in.Now}, true, nil
}

func (f *concurrentWorkItemClaims) CompleteWorkItem(ctx context.Context, _ string, _ string, terminalState string, _ time.Time) (bool, error) {
	if f.terminal != nil {
		if err := f.terminal.wait(ctx, terminalState); err != nil {
			return false, err
		}
	}
	f.mu.Lock()
	f.completeCalls++
	f.mu.Unlock()
	return true, nil
}

func (f *concurrentWorkItemClaims) RetryWorkItem(context.Context, string, string, time.Time, time.Time) (bool, error) {
	return true, nil
}

func (f *concurrentWorkItemClaims) RenewWorkItemLease(context.Context, string, string, time.Time, time.Time) (bool, error) {
	return true, nil
}

func (*concurrentWorkItemClaims) ClaimNextCoalescedExecutorWorkItemSibling(context.Context, string, string, string, string, string, string, string, int64, time.Time, time.Time) (*connector_events.WorkItemCoalescedSiblingClaim, bool, error) {
	return nil, false, nil
}

func (*concurrentWorkItemClaims) ClaimNextCoalescedExecutorWorkItemSiblingSourceScoped(context.Context, string, string, string, string, string, string, string, int64, time.Time, time.Time) (*connector_events.WorkItemCoalescedSiblingClaim, bool, error) {
	return nil, false, nil
}

func (f *concurrentWorkItemClaims) ClaimCalls() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.claimCalls
}

func (f *concurrentWorkItemClaims) Completed() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.completeCalls
}

type blockingTerminalWorkItemBridge struct {
	mu        sync.Mutex
	active    int
	maxActive int
	started   int
	want      int
	allActive chan struct{}
	release   chan struct{}
	once      sync.Once
}

func newBlockingTerminalWorkItemBridge(want int) *blockingTerminalWorkItemBridge {
	return &blockingTerminalWorkItemBridge{
		want:      want,
		allActive: make(chan struct{}),
		release:   make(chan struct{}),
	}
}

func (b *blockingTerminalWorkItemBridge) MarkEventProcessingForWorkItem(context.Context, string, string, string, time.Time, time.Time) error {
	return nil
}

func (b *blockingTerminalWorkItemBridge) ClaimNextCoalescedWorkItemSibling(context.Context, string, string, string, string, string, string, string, int64, time.Time, time.Time) (*connector_events.WorkItemCoalescedSiblingClaim, bool, error) {
	return nil, false, nil
}

func (b *blockingTerminalWorkItemBridge) MarkEventTerminalForWorkItem(ctx context.Context, _ string, _ string, terminalStatus, _ string, _ time.Time) error {
	return b.wait(ctx, terminalStatus)
}

func (b *blockingTerminalWorkItemBridge) wait(ctx context.Context, terminalStatus string) error {
	if terminalStatus != connector_workqueue.WorkStateSkipped {
		return fmt.Errorf("terminalStatus = %q, want skipped", terminalStatus)
	}
	b.mu.Lock()
	b.active++
	if b.active > b.maxActive {
		b.maxActive = b.active
	}
	b.started++
	if b.started == b.want {
		b.once.Do(func() { close(b.allActive) })
	}
	b.mu.Unlock()
	defer func() {
		b.mu.Lock()
		b.active--
		b.mu.Unlock()
	}()
	select {
	case <-b.release:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (b *blockingTerminalWorkItemBridge) ReleaseEventForWorkItemRetry(context.Context, string, string, string, time.Time, time.Time, int) error {
	return nil
}

func (b *blockingTerminalWorkItemBridge) RenewEventLeaseForWorkItem(context.Context, string, string, time.Time) error {
	return nil
}

func (b *blockingTerminalWorkItemBridge) MaxActive() int {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.maxActive
}

func (b *blockingTerminalWorkItemBridge) Release() {
	close(b.release)
}

func TestWorkerClaimLimiterRequestIncludesQueueClasses(t *testing.T) {
	w := &Worker{cfg: WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "worker-interactive",
		QueueClasses:  []string{connector_inbound_routing.QueueClassInteractive, connector_inbound_routing.QueueClassDefault},
		BatchSize:     5,
	}}

	req := w.claimLimiterRequest(0)
	if got := strings.Join(req.QueueClasses, ","); got != "interactive,default" {
		t.Fatalf("QueueClasses = %q, want interactive,default", got)
	}
	if req.BatchSize != 5 {
		t.Fatalf("BatchSize = %d, want 5", req.BatchSize)
	}
}

func (f *fakeEventsService) ClaimByID(_ context.Context, eventID, _ string, _ string, _ sql.NullString, _ int) (string, time.Time, bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	claimedAt := time.Now()
	if f.claimByIDErrs != nil {
		if err, found := f.claimByIDErrs[eventID]; found {
			// Mark as claimed so PeekBatch doesn't retry this row forever.
			f.claimed[eventID] = true
			for f.next < len(f.queue) && f.claimed[f.queue[f.next].ID] {
				f.next++
			}
			return "", time.Time{}, false, err
		}
	}
	if f.claimByIDResults != nil {
		if ok, found := f.claimByIDResults[eventID]; found && !ok {
			// Configured to lose — simulate another worker won; mark as claimed
			// so PeekBatch does not return this row again.
			f.claimed[eventID] = true
			for f.next < len(f.queue) && f.claimed[f.queue[f.next].ID] {
				f.next++
			}
			return "", time.Time{}, false, nil
		}
	}
	// Default: win the claim if the event is in the queue and not yet claimed.
	for _, ev := range f.queue {
		if ev.ID == eventID && !f.claimed[ev.ID] {
			f.claimed[ev.ID] = true
			// Advance f.next past all claimed events.
			for f.next < len(f.queue) && f.claimed[f.queue[f.next].ID] {
				f.next++
			}
			return "token-" + eventID, claimedAt, true, nil
		}
	}
	return "", time.Time{}, false, nil // already claimed or not found
}

func (f *fakeEventsService) ClaimByIDForClassesWithMode(ctx context.Context, eventID, workerID, installedPackID string, deliveryID sql.NullString, deliverySequence int, _ []string, _ int64, _ bool, staleOnly bool) (string, time.Time, bool, error) {
	f.mu.Lock()
	f.claimByIDModes = append(f.claimByIDModes, staleOnly)
	f.mu.Unlock()
	return f.ClaimByID(ctx, eventID, workerID, installedPackID, deliveryID, deliverySequence)
}

func TestWorkerClaimByIDForSchedulerPreservesStaleCandidateMode(t *testing.T) {
	event := noopEvent("evt-stale-candidate", "noop.created")
	fakes := newFakeEvents(event)
	w := &Worker{cfg: WorkerConfig{PackInstallID: "pack-test", WorkerID: "worker-1"}, events: fakes}
	claimed, ok, err := w.claimByIDForScheduler(context.Background(), schedulerClaimCandidate{event: event, staleOnly: true})
	if err != nil || !ok || claimed == nil {
		t.Fatalf("claimByIDForScheduler = (%+v, %v, %v), want stale claim", claimed, ok, err)
	}
	fakes.mu.Lock()
	defer fakes.mu.Unlock()
	if len(fakes.claimByIDModes) != 1 || !fakes.claimByIDModes[0] {
		t.Fatalf("claim modes = %v, want [true]", fakes.claimByIDModes)
	}
}

func (f *fakeEventsService) MarkProcessed(ctx context.Context, eventID, _ string) error {
	f.mu.Lock()
	ctxErr := ctx.Err()
	f.processedCtxErrs = append(f.processedCtxErrs, ctxErr)
	if f.failOnCanceledFinalize && ctxErr != nil {
		f.mu.Unlock()
		return ctxErr
	}
	f.processed = append(f.processed, eventID)
	f.mu.Unlock()
	return nil
}

func (f *fakeEventsService) MarkSkipped(ctx context.Context, eventID, _ string) error {
	f.mu.Lock()
	ctxErr := ctx.Err()
	f.processedCtxErrs = append(f.processedCtxErrs, ctxErr)
	if f.failOnCanceledFinalize && ctxErr != nil {
		f.mu.Unlock()
		return ctxErr
	}
	f.skipped = append(f.skipped, eventID)
	f.mu.Unlock()
	return nil
}

func (f *fakeEventsService) ReleaseClaim(ctx context.Context, eventID, _ string) error {
	f.mu.Lock()
	ctxErr := ctx.Err()
	f.processedCtxErrs = append(f.processedCtxErrs, ctxErr)
	if f.failOnCanceledFinalize && ctxErr != nil {
		f.mu.Unlock()
		return ctxErr
	}
	f.released = append(f.released, eventID)
	delete(f.claimed, eventID)
	for i, ev := range f.queue {
		if ev.ID == eventID && i < f.next {
			f.next = i
			break
		}
	}
	f.mu.Unlock()
	return nil
}

func (f *fakeEventsService) ReleaseClaimForRetry(ctx context.Context, eventID, _, reason string) error {
	f.mu.Lock()
	ctxErr := ctx.Err()
	f.processedCtxErrs = append(f.processedCtxErrs, ctxErr)
	if f.failOnCanceledFinalize && ctxErr != nil {
		f.mu.Unlock()
		return ctxErr
	}
	f.releasedForRetry = append(f.releasedForRetry, failedCall{eventID: eventID, errorMsg: reason})
	delete(f.claimed, eventID)
	for i, ev := range f.queue {
		if ev.ID == eventID && i < f.next {
			f.next = i
			break
		}
	}
	f.mu.Unlock()
	return nil
}

func (f *fakeEventsService) MarkFailed(ctx context.Context, eventID, _, errorMsg string) error {
	f.mu.Lock()
	ctxErr := ctx.Err()
	f.failedCtxErrs = append(f.failedCtxErrs, ctxErr)
	if f.failOnCanceledFinalize && ctxErr != nil {
		f.mu.Unlock()
		return ctxErr
	}
	f.failed = append(f.failed, failedCall{eventID: eventID, errorMsg: errorMsg})
	f.mu.Unlock()
	return nil
}

func (f *fakeEventsService) BlockOnIdentityConvergence(_ context.Context, eventID, claimToken string, probe IdentityConvergenceProbe) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.blocked = append(f.blocked, blockedDependencyCall{eventID: eventID, claimToken: claimToken, probe: probe})
	return nil
}

func (f *fakeEventsService) SupportsIdentityConvergenceBlocking() bool {
	return true
}

type identityConvergenceUnavailableEvents struct {
	*fakeEventsService
}

func (f *identityConvergenceUnavailableEvents) SupportsIdentityConvergenceBlocking() bool {
	return false
}

func (f *fakeEventsService) ClaimNextCoalescedSibling(_ context.Context, currentEventID, _ string, _ string) (*connector_events.ConnectorEvent, bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	next := f.coalescedSiblings[currentEventID]
	if next == nil {
		return nil, false, nil
	}
	f.processed = append(f.processed, currentEventID)
	f.claimed[next.ID] = true
	next.ClaimToken = "token-" + next.ID
	return next, true, nil
}

func (f *fakeEventsService) ClaimNextCoalescedSiblingSourceScoped(ctx context.Context, currentEventID, claimToken, workerID string) (*connector_events.ConnectorEvent, bool, error) {
	return f.ClaimNextCoalescedSibling(ctx, currentEventID, claimToken, workerID)
}

func (f *fakeEventsService) LoadSettledProjectionPayloads(_ context.Context, eventID string) ([]*connector_events.ConnectorEvent, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if payloads, ok := f.settledProjectionPayloads[eventID]; ok {
		return append([]*connector_events.ConnectorEvent(nil), payloads...), nil
	}
	for _, event := range f.queue {
		if event != nil && event.ID == eventID {
			return []*connector_events.ConnectorEvent{event}, nil
		}
	}
	return nil, nil
}

// fakeSyncState records RecordError calls.
type fakeSyncState struct {
	mu     sync.Mutex
	errors []syncStateCall
}

type syncStateCall struct {
	namespace, object, direction, errorMsg string
}

func (f *fakeSyncState) RecordError(_ context.Context, namespace, object, direction, errorMsg string) error {
	f.mu.Lock()
	f.errors = append(f.errors, syncStateCall{namespace, object, direction, errorMsg})
	f.mu.Unlock()
	return nil
}

// fakeBatchRead is a BatchReadClient that always returns empty maps.
type fakeBatchRead struct{}

func (fakeBatchRead) BatchRead(_ context.Context, _, _ string, _ []string) (map[string]json.RawMessage, error) {
	return map[string]json.RawMessage{}, nil
}

// fakeFlowInvoker is a controllable FlowInvoker.
type fakeFlowInvoker struct {
	successResult *FlowResult
	returnErr     error
	calls         int
	origins       []string
	onExecute     func()
	mu            sync.Mutex
}

func (f *fakeFlowInvoker) ExecuteFlow(ctx context.Context, _ string, _ map[string]any, _ *models.UserSession) (*FlowResult, error) {
	f.mu.Lock()
	f.calls++
	if origin, _ := ctx.Value(constants.ContextKeyConnectorSyncOrigin).(string); origin != "" {
		f.origins = append(f.origins, origin)
	}
	f.mu.Unlock()
	if f.onExecute != nil {
		f.onExecute()
	}
	return f.successResult, f.returnErr
}

func (f *fakeFlowInvoker) ExecuteFlowVersion(_ context.Context, _, _ string, _ map[string]any, _ *models.UserSession) (*FlowResult, error) {
	f.mu.Lock()
	f.calls++
	f.mu.Unlock()
	return f.successResult, f.returnErr
}

func (f *fakeFlowInvoker) SystemUser(_ context.Context) *models.UserSession {
	return &models.UserSession{ID: "system-user"}
}

type typedCauseFlowInvoker struct {
	fakeFlowInvoker
	executionID string
	cause       *models.FlowExecutionCause
}

func (f *typedCauseFlowInvoker) ExecuteFlowOutcome(ctx context.Context, flowDeveloperName string, inputs map[string]any, user *models.UserSession) (*FlowOutcome, error) {
	result, err := f.fakeFlowInvoker.ExecuteFlow(ctx, flowDeveloperName, inputs, user)
	return &FlowOutcome{Result: result, ExecutionID: f.executionID, Cause: f.cause}, err
}

func (f *typedCauseFlowInvoker) ExecuteFlowVersionOutcome(ctx context.Context, flowDeveloperName, companionFlowVersionID string, inputs map[string]any, user *models.UserSession) (*FlowOutcome, error) {
	result, err := f.fakeFlowInvoker.ExecuteFlowVersion(ctx, flowDeveloperName, companionFlowVersionID, inputs, user)
	return &FlowOutcome{Result: result, ExecutionID: f.executionID, Cause: f.cause}, err
}

type dependencyFlowInvoker struct {
	fakeFlowInvoker
	dependencyRequest *models.FlowExecutionDependencyRequest
}

func (f *dependencyFlowInvoker) ExecuteFlowOutcome(ctx context.Context, flowDeveloperName string, inputs map[string]any, user *models.UserSession) (*FlowOutcome, error) {
	result, err := f.fakeFlowInvoker.ExecuteFlow(ctx, flowDeveloperName, inputs, user)
	return &FlowOutcome{Result: result}, err
}

func (f *dependencyFlowInvoker) ExecuteFlowVersionOutcome(ctx context.Context, flowDeveloperName, companionFlowVersionID string, inputs map[string]any, user *models.UserSession) (*FlowOutcome, error) {
	result, err := f.fakeFlowInvoker.ExecuteFlowVersion(ctx, flowDeveloperName, companionFlowVersionID, inputs, user)
	return &FlowOutcome{Result: result}, err
}

func (f *dependencyFlowInvoker) ExecuteFlowDependencyOutcome(ctx context.Context, flowDeveloperName string, inputs map[string]any, user *models.UserSession) (*FlowDependencyOutcome, error) {
	result, err := f.fakeFlowInvoker.ExecuteFlow(ctx, flowDeveloperName, inputs, user)
	return &FlowDependencyOutcome{FlowOutcome: &FlowOutcome{Result: result}, Dependency: f.dependencyRequest}, err
}

func (f *dependencyFlowInvoker) ExecuteFlowVersionDependencyOutcome(ctx context.Context, flowDeveloperName, companionFlowVersionID string, inputs map[string]any, user *models.UserSession) (*FlowDependencyOutcome, error) {
	result, err := f.fakeFlowInvoker.ExecuteFlowVersion(ctx, flowDeveloperName, companionFlowVersionID, inputs, user)
	return &FlowDependencyOutcome{FlowOutcome: &FlowOutcome{Result: result}, Dependency: f.dependencyRequest}, err
}

type cancelingFlowInvoker struct {
	cancel context.CancelFunc
	result *FlowResult
	err    error
}

func (f *cancelingFlowInvoker) ExecuteFlow(_ context.Context, _ string, _ map[string]any, _ *models.UserSession) (*FlowResult, error) {
	if f.cancel != nil {
		f.cancel()
	}
	return f.result, f.err
}

func (f *cancelingFlowInvoker) ExecuteFlowVersion(_ context.Context, _, _ string, _ map[string]any, _ *models.UserSession) (*FlowResult, error) {
	if f.cancel != nil {
		f.cancel()
	}
	return f.result, f.err
}

func (f *cancelingFlowInvoker) SystemUser(_ context.Context) *models.UserSession {
	return &models.UserSession{ID: "system-user"}
}

type blockingOnceFlowInvoker struct {
	started chan struct{}
	release chan struct{}
	once    sync.Once
}

func (f *blockingOnceFlowInvoker) ExecuteFlow(ctx context.Context, _ string, _ map[string]any, _ *models.UserSession) (*FlowResult, error) {
	f.once.Do(func() { close(f.started) })
	select {
	case <-f.release:
		return &FlowResult{IsSuccess: true}, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

func (f *blockingOnceFlowInvoker) ExecuteFlowVersion(ctx context.Context, _, _ string, _ map[string]any, _ *models.UserSession) (*FlowResult, error) {
	f.once.Do(func() { close(f.started) })
	select {
	case <-f.release:
		return &FlowResult{IsSuccess: true}, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

func (f *blockingOnceFlowInvoker) SystemUser(_ context.Context) *models.UserSession {
	return &models.UserSession{ID: "system-user"}
}

type blockingClaimEvents struct {
	started chan struct{}
	once    sync.Once
	ctxErr  error
}

func (f *blockingClaimEvents) signalStarted() {
	f.once.Do(func() { close(f.started) })
}

func (f *blockingClaimEvents) ClaimNext(ctx context.Context, _, _ string) (*connector_events.ConnectorEvent, bool, error) {
	f.signalStarted()
	<-ctx.Done()
	f.ctxErr = ctx.Err()
	return nil, false, ctx.Err()
}

func (f *blockingClaimEvents) PeekBatch(ctx context.Context, _ string, _ int) ([]*connector_events.ConnectorEvent, error) {
	f.signalStarted()
	<-ctx.Done()
	f.ctxErr = ctx.Err()
	return nil, ctx.Err()
}

func (f *blockingClaimEvents) ClaimByID(_ context.Context, _ string, _ string, _ string, _ sql.NullString, _ int) (string, time.Time, bool, error) {
	return "", time.Time{}, false, nil
}

func (f *blockingClaimEvents) MarkProcessed(context.Context, string, string) error {
	return nil
}

func (f *blockingClaimEvents) MarkSkipped(context.Context, string, string) error {
	return nil
}

func (f *blockingClaimEvents) MarkFailed(context.Context, string, string, string) error {
	return nil
}

// ── helpers ──────────────────────────────────────────────────────────────────

const workerTestNS = "__worker_test__"

// noopPayload builds a valid QueueEnvelope-encoded payload with objectKind and
// sourceID. The inner user payload contains a single "id" field for testing.
// sourceID is the external object ID (distinct from IdempotencyKey — e.g. "src-abc").
func noopPayload(objectKind, sourceID string) json.RawMessage {
	userPayload := json.RawMessage(`{"id":"` + sourceID + `"}`)
	b, err := EncodeQueueEnvelope(objectKind, sourceID, userPayload)
	if err != nil {
		panic("noopPayload: EncodeQueueEnvelope: " + err.Error())
	}
	return json.RawMessage(b)
}

// noopEvent returns a minimal ConnectorEvent with a valid QueueEnvelope payload
// and a concrete RouteVersion (1) so the worker's strict steady-state path
// (no live-route fallback) succeeds. The IdempotencyKey is the event's dedup
// key (e.g. event ID); the sourceID in the payload is distinct — the external
// object's source ID.
func noopEvent(id, eventKind string) *connector_events.ConnectorEvent {
	return &connector_events.ConnectorEvent{
		ID:              id,
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       eventKind,
		IdempotencyKey:  id,
		Status:          "processing",
		ClaimToken:      "token-" + id,
		// sourceID ("src-" + id) is deliberately different from IdempotencyKey (id)
		// to verify the worker uses sourceID from QueueEnvelope, not IdempotencyKey.
		Payload:      noopPayload("noop_object", "src-"+id),
		RouteVersion: sql.NullInt64{Valid: true, Int64: 1},
	}
}

// payloadWithoutObjectKind returns a queue payload that decodes as a JSON object
// but has no "objectKind" field — simulates a malformed/missing envelope
// (parser-bug class: dispatcher failed to encode objectKind).
func payloadWithoutObjectKind() json.RawMessage {
	// Valid JSON but not a valid QueueEnvelope (missing objectKind).
	return json.RawMessage(`{"sourceID":"src-123","payload":{}}`)
}

// payloadWithoutSourceID returns a queue payload that has "objectKind" but no
// "sourceID" — simulates a malformed envelope (parser-bug class).
func payloadWithoutSourceID() json.RawMessage {
	// Valid JSON but not a valid QueueEnvelope (missing sourceID).
	return json.RawMessage(`{"objectKind":"noop_object","payload":{}}`)
}

// stubRouteResolver lets tests control what ResolveFlow returns per (ns, eventKind).
// ResolveFlowByVersion uses the same route map; version is accepted but not checked
// (tests use RouteVersion.Valid=true with a fixed version and rely on the flow name map).
type stubRouteResolver struct {
	routes map[string]string // key: "namespace/eventKind" → flowDeveloperName
	specs  map[string]connector_inbound_routing.RouteExecutionSpec
	// sweptVersions, if non-nil, causes ResolveFlowByVersion to return
	// ErrRouteVersionSwept for the listed versions.
	sweptVersions map[int64]bool
}

type countingExecutionSpecResolver struct {
	*stubRouteResolver
	calls int
}

func (r *countingExecutionSpecResolver) ResolveExecutionSpecByVersion(ctx context.Context, installedPackID, eventKind string, version int64) (connector_inbound_routing.RouteExecutionSpec, error) {
	r.calls++
	return r.stubRouteResolver.ResolveExecutionSpecByVersion(ctx, installedPackID, eventKind, version)
}

func (s *stubRouteResolver) ResolveFlow(_ context.Context, ns, _ /* packInstallID */, eventKind string) (int64, string, error) {
	if flow, ok := s.routes[ns+"/"+eventKind]; ok {
		return 1, flow, nil
	}
	return 0, "", connector_inbound_routing.ErrRouteNotDeclared
}

func (s *stubRouteResolver) ResolveFlowByVersion(_ context.Context, _ /* installedPackID */, eventKind string, version int64) (string, error) {
	if s.sweptVersions[version] {
		return "", connector_inbound_routing.ErrRouteVersionSwept
	}
	// Derive namespace key from the routes map by scanning for the eventKind.
	for key, flow := range s.routes {
		if len(key) >= len(eventKind) && key[len(key)-len(eventKind):] == eventKind {
			return flow, nil
		}
	}
	return "", connector_inbound_routing.ErrRouteNotDeclared
}

func (s *stubRouteResolver) ResolveExecutionSpecByVersion(_ context.Context, _ /* installedPackID */, eventKind string, version int64) (connector_inbound_routing.RouteExecutionSpec, error) {
	if s.sweptVersions[version] {
		return connector_inbound_routing.RouteExecutionSpec{}, connector_inbound_routing.ErrRouteVersionSwept
	}
	for key, spec := range s.specs {
		if len(key) >= len(eventKind) && key[len(key)-len(eventKind):] == eventKind {
			return connector_inbound_routing.NormalizeRouteExecutionSpec(spec)
		}
	}
	flow, err := s.ResolveFlowByVersion(context.Background(), "", eventKind, version)
	if err != nil {
		return connector_inbound_routing.RouteExecutionSpec{}, err
	}
	return connector_inbound_routing.NormalizeRouteExecutionSpec(connector_inbound_routing.RouteExecutionSpec{FlowDeveloperName: flow})
}

// buildTestWorker constructs a Worker with the given EventsService,
// SyncStateService, and FlowInvoker under workerTestNS.
// Uses a stubRouteResolver backed by inline route map.
func buildTestWorker(
	t *testing.T,
	events EventsService,
	syncstate SyncStateService,
	flow FlowInvoker,
) *Worker {
	t.Helper()

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
		workerTestNS + "/noop.updated": "NoopUpdateFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
			MaxClaims:     0,
		},
		events,
		syncstate,
		fakeBatchRead{},
		flow,
		resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	return w
}

// ── test cases ────────────────────────────────────────────────────────────────

// TestWorker_HappyPath_ClaimBatchRouteFlowMarkProcessed verifies the full
// happy-path: claim → batch-read (noop) → route → flow → MarkProcessed.
func TestWorker_HappyPath_ClaimBatchRouteFlowMarkProcessed(t *testing.T) {
	ev := noopEvent("ev-1", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if len(fakes.processed) != 1 || fakes.processed[0] != "ev-1" {
		t.Errorf("MarkProcessed not called for ev-1; processed=%v", fakes.processed)
	}
	if len(fakes.failed) != 0 {
		t.Errorf("unexpected MarkFailed calls: %v", fakes.failed)
	}
	if len(syncstate.errors) != 0 {
		t.Errorf("unexpected SyncState.RecordError calls: %v", syncstate.errors)
	}
}

// TestWorker_EmptyQueue_ReturnsFalseNil verifies that an empty queue returns
// (false, nil) without calling MarkProcessed or MarkFailed.
func TestWorker_EmptyQueue_ReturnsFalseNil(t *testing.T) {
	fakes := newFakeEvents() // no events
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if claimed {
		t.Fatal("expected claimed=false for empty queue")
	}
}

// TestWorker_FlowNotSuccess_MarkFailedAndSyncState verifies that a flow
// returning IsSuccess=false triggers MarkFailed + SyncState.RecordError.
func TestWorker_FlowNotSuccess_MarkFailedAndSyncState(t *testing.T) {
	ev := noopEvent("ev-2", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{
			IsSuccess:  false,
			FirstError: &FlowError{StatusCode: "FLOW_ERROR", Message: "something went wrong"},
		},
	}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true even on flow failure")
	}
	if err == nil {
		t.Fatal("expected non-nil error on flow failure")
	}
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed call, got %d", len(fakes.failed))
	}
	if len(syncstate.errors) != 1 {
		t.Errorf("want 1 SyncState.RecordError call, got %d", len(syncstate.errors))
	}
	if syncstate.errors[0].direction != "inbound" {
		t.Errorf("SyncState direction = %q, want %q", syncstate.errors[0].direction, "inbound")
	}
	if len(fakes.processed) != 0 {
		t.Error("unexpected MarkProcessed call on flow failure")
	}
}

func TestWorker_IdentityConvergenceDependencyBlocksWithoutFailureBudget(t *testing.T) {
	ev := noopEvent("ev-dependency", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	request, err := models.NewFlowIdentityConvergenceDependencyRequest(
		"Lead",
		"00Q000000000001AAA",
		"pingcap-hubspot-companion",
		"hubspot-contact",
	)
	if err != nil {
		t.Fatal(err)
	}
	flow := &dependencyFlowInvoker{
		fakeFlowInvoker:   fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true, IsRetriable: true}},
		dependencyRequest: &request,
	}

	w := buildTestWorker(t, fakes, syncstate, flow)
	claimed, err := w.processOne(context.Background())
	if err != nil || !claimed {
		t.Fatalf("processOne = (%v, %v), want claimed durable block", claimed, err)
	}
	if len(fakes.blocked) != 1 || fakes.blocked[0].eventID != ev.ID || fakes.blocked[0].probe.SourceID == "" {
		t.Fatalf("blocked calls = %#v, want exact typed dependency", fakes.blocked)
	}
	blocked := fakes.blocked[0].probe
	if blocked.Dependency.RecordID != request.RecordID ||
		blocked.Dependency.ConnectorOwnerKey != request.ConnectorOwnerKey ||
		blocked.Dependency.DestinationSlotID != request.DestinationSlotID ||
		blocked.Dependency.ConnectorNamespace != workerTestNS ||
		blocked.Dependency.ConnectorSourceObject != "noop_object" ||
		blocked.Dependency.ConnectorRemoteID != "src-"+ev.ID ||
		blocked.Dependency.Validate() != nil {
		t.Fatalf("bound dependency = %#v, want request plus immutable event scope", blocked.Dependency)
	}
	if len(fakes.failed) != 0 || len(fakes.releasedForRetry) != 0 || len(fakes.processed) != 0 {
		t.Fatalf("dependency consumed another finalization path: failed=%v retry=%v processed=%v", fakes.failed, fakes.releasedForRetry, fakes.processed)
	}
	if len(syncstate.errors) != 0 {
		t.Fatalf("dependency block polluted SyncState: %#v", syncstate.errors)
	}
}

func TestWorker_IdentityConvergenceDependencyWithoutDurableCapabilityReleasesForRetry(t *testing.T) {
	ev := noopEvent("ev-dependency-capability", "noop.created")
	fakes := newFakeEvents(ev)
	events := &identityConvergenceUnavailableEvents{fakeEventsService: fakes}
	syncstate := &fakeSyncState{}
	request, err := models.NewFlowIdentityConvergenceDependencyRequest(
		"Lead",
		"00Q000000000001AAA",
		"pingcap-hubspot-companion",
		"hubspot-contact",
	)
	if err != nil {
		t.Fatal(err)
	}
	flow := &dependencyFlowInvoker{
		fakeFlowInvoker:   fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}},
		dependencyRequest: &request,
	}

	w := buildTestWorker(t, events, syncstate, flow)
	claimed, err := w.processOne(context.Background())
	if err == nil || !claimed {
		t.Fatalf("processOne = (%v, %v), want claimed compatibility retry", claimed, err)
	}
	if len(fakes.releasedForRetry) != 1 || fakes.releasedForRetry[0].eventID != ev.ID {
		t.Fatalf("releasedForRetry = %#v, want one compatibility retry", fakes.releasedForRetry)
	}
	if len(fakes.blocked) != 0 || len(fakes.failed) != 0 || len(fakes.processed) != 0 {
		t.Fatalf("unsupported dependency used unsafe finalization path: blocked=%v failed=%v processed=%v", fakes.blocked, fakes.failed, fakes.processed)
	}
}

func TestWorker_IdentityConvergenceDependencyRequiresSuccessfulResult(t *testing.T) {
	for _, tc := range []struct {
		name      string
		result    *FlowResult
		companion bool
	}{
		{name: "nil result"},
		{name: "unsuccessful result", result: &FlowResult{IsSuccess: false}},
		{name: "unsuccessful companion result", result: &FlowResult{IsSuccess: false}, companion: true},
	} {
		t.Run(tc.name, func(t *testing.T) {
			ev := noopEvent("ev-dependency-unsuccessful", "noop.created")
			if tc.companion {
				ev.CompanionPackInstallID = "companion-install"
				ev.CompanionFlowVersionID = "companion-flow-version"
			}
			fakes := newFakeEvents(ev)
			syncstate := &fakeSyncState{}
			request, err := models.NewFlowIdentityConvergenceDependencyRequest(
				"Lead",
				"00Q000000000001AAA",
				"pingcap-hubspot-companion",
				"hubspot-contact",
			)
			if err != nil {
				t.Fatal(err)
			}
			flow := &dependencyFlowInvoker{
				fakeFlowInvoker:   fakeFlowInvoker{successResult: tc.result},
				dependencyRequest: &request,
			}
			w := buildTestWorker(t, fakes, syncstate, flow)
			if tc.companion {
				w.SetCompanionInstalledChecker(func(context.Context, string) (bool, error) { return true, nil })
			}

			claimed, err := w.processOne(context.Background())
			if !claimed || err == nil {
				t.Fatalf("processOne = (%v, %v), want bounded failure", claimed, err)
			}
			if len(fakes.failed) != 1 || len(fakes.blocked) != 0 || len(fakes.processed) != 0 {
				t.Fatalf("unsuccessful dependency used wrong finalization path: failed=%v blocked=%v processed=%v", fakes.failed, fakes.blocked, fakes.processed)
			}
		})
	}
}

func TestMandatoryWorkItemServiceWithoutDependencyCapabilityDoesNotAdvertiseIt(t *testing.T) {
	service := newMandatoryWorkItemEventsService(&concurrentWorkItemClaims{}, WorkerConfig{})
	if service.SupportsIdentityConvergenceBlocking() {
		t.Fatal("legacy custom WorkItemService unexpectedly advertised durable dependency blocking")
	}
}

func TestWorker_InvalidIdentityConvergenceDependencyConsumesFailureBudget(t *testing.T) {
	ev := noopEvent("ev-invalid-dependency", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	invalid := models.FlowExecutionDependencyRequest{}
	flow := &dependencyFlowInvoker{
		fakeFlowInvoker:   fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true, IsRetriable: true}},
		dependencyRequest: &invalid,
	}

	w := buildTestWorker(t, fakes, syncstate, flow)
	claimed, err := w.processOne(context.Background())
	if !claimed || err == nil {
		t.Fatalf("processOne = (%v, %v), want claimed deterministic failure", claimed, err)
	}
	if len(fakes.failed) != 1 || fakes.failed[0].eventID != ev.ID {
		t.Fatalf("failed calls = %#v, want claimed WorkItem failure-budget transition", fakes.failed)
	}
	if len(syncstate.errors) != 1 {
		t.Fatalf("SyncState errors = %#v, want one bounded failure record", syncstate.errors)
	}
	if len(fakes.blocked) != 0 || len(fakes.releasedForRetry) != 0 || len(fakes.processed) != 0 {
		t.Fatalf("invalid dependency used wrong finalization path: blocked=%v retry=%v processed=%v", fakes.blocked, fakes.releasedForRetry, fakes.processed)
	}
}

// TestWorker_FlowFailedRetriable_ReleasesForRetry verifies the OUT-1 follow-up:
// a flow returning a FAILED result with IsRetriable=true (the shape a
// FlowRetriableFault produces — IsSuccess=false, IsRetriable=true) is re-driven
// via ReleaseClaimForRetry instead of MarkFailed. MarkFailed increments
// RetryCount and dead-letters at MaxRetryAttempts (~7.5 min cumulative), which
// can dead-letter a concurrent create-vs-adopt loser BEFORE the 15-minute
// winner lease expires. Routing through ReleaseClaimForRetry leaves the
// genuine-failure budget untouched (spin protection / backoff is enforced and
// tested in events.ReleaseClaimForRetry) so the loser self-heals.
func TestWorker_FlowFailedRetriable_ReleasesForRetry(t *testing.T) {
	ev := noopEvent("ev-failed-retriable", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{
			IsSuccess:   false,
			IsRetriable: true,
			FirstError:  &FlowError{StatusCode: "RETRIABLE", Message: "retry after the in-flight create finalizes"},
		},
	}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected non-nil error on retriable defer (event not finalized this attempt)")
	}
	if len(fakes.releasedForRetry) != 1 {
		t.Fatalf("want 1 ReleaseClaimForRetry call, got %d", len(fakes.releasedForRetry))
	}
	if fakes.releasedForRetry[0].eventID != "ev-failed-retriable" {
		t.Errorf("ReleaseClaimForRetry eventID = %q, want %q", fakes.releasedForRetry[0].eventID, "ev-failed-retriable")
	}
	if !strings.Contains(fakes.releasedForRetry[0].errorMsg, "retriable") {
		t.Errorf("ReleaseClaimForRetry reason = %q, want substring %q", fakes.releasedForRetry[0].errorMsg, "retriable")
	}
	// The retry budget must NOT be burned: MarkFailed (the only RetryCount-
	// incrementing path) and MarkProcessed must not be called.
	if len(fakes.failed) != 0 {
		t.Errorf("MarkFailed must NOT be called for a retriable defer, got %d calls", len(fakes.failed))
	}
	if len(fakes.processed) != 0 {
		t.Error("MarkProcessed must NOT be called for a retriable defer")
	}
	// A transient retriable defer should not pollute SyncState with an error
	// record (mirrors the existing batch-defer release path).
	if len(syncstate.errors) != 0 {
		t.Errorf("SyncState.RecordError must NOT be called for a retriable defer, got %d", len(syncstate.errors))
	}
}

func TestWorker_FlowTypedCauseRetriable_ReleasesForRetryDespiteTerminalPresentation(t *testing.T) {
	ev := noopEvent("ev-typed-cause-retriable", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &typedCauseFlowInvoker{
		fakeFlowInvoker: fakeFlowInvoker{successResult: &FlowResult{
			IsSuccess: false,
			FirstError: &FlowError{
				StatusCode: "EMAIL_MALFORMED",
				Message:    "presentation only",
			},
		}},
		executionID: "flow-execution-123",
		cause: &models.FlowExecutionCause{
			Code:      "DB_LOCK_WAIT_TIMEOUT",
			Category:  models.FlowExecutionCauseCategoryConcurrency,
			Retriable: true,
		},
	}

	w := buildTestWorker(t, fakes, syncstate, flow)
	claimed, err := w.processOne(context.Background())
	if !claimed || err == nil {
		t.Fatalf("processOne = (%v, %v), want claimed retriable defer", claimed, err)
	}
	if len(fakes.releasedForRetry) != 1 || len(fakes.failed) != 0 || len(syncstate.errors) != 0 {
		t.Fatalf("typed retry path: released=%v failed=%v syncErrors=%v", fakes.releasedForRetry, fakes.failed, syncstate.errors)
	}
}

// TestWorker_FlowRetriableSuccess_MarkFailed verifies that a flow returning
// IsSuccess=true with IsRetriable=true triggers MarkFailed (re-claimable)
// instead of MarkProcessed (terminal). This is the fix for issue #2128:
// out-of-order events whose dependency is missing must not be silently dropped.
func TestWorker_FlowRetriableSuccess_MarkFailed(t *testing.T) {
	ev := noopEvent("ev-retriable", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{
			IsSuccess:   true,
			IsRetriable: true,
			FirstError:  &FlowError{StatusCode: "DEPENDENCY_MISSING", Message: "prerequisite record not yet created"},
		},
	}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected non-nil error on retriable success")
	}
	if len(fakes.failed) != 1 {
		t.Fatalf("want 1 MarkFailed call, got %d", len(fakes.failed))
	}
	if fakes.failed[0].eventID != "ev-retriable" {
		t.Errorf("MarkFailed eventID = %q, want %q", fakes.failed[0].eventID, "ev-retriable")
	}
	if !strings.Contains(fakes.failed[0].errorMsg, "retriable") {
		t.Errorf("MarkFailed errorMsg = %q, want substring %q", fakes.failed[0].errorMsg, "retriable")
	}
	if len(fakes.processed) != 0 {
		t.Error("MarkProcessed must NOT be called on retriable success")
	}
	if len(syncstate.errors) != 1 {
		t.Errorf("want 1 SyncState.RecordError call, got %d", len(syncstate.errors))
	}
}

// TestWorker_FlowRetriableSuccess_NoFirstError verifies that a retriable
// success without a FirstError still triggers MarkFailed with a generic message.
func TestWorker_FlowRetriableSuccess_NoFirstError(t *testing.T) {
	ev := noopEvent("ev-retriable-noerr", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{
			IsSuccess:   true,
			IsRetriable: true,
		},
	}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected non-nil error on retriable success")
	}
	if len(fakes.failed) != 1 {
		t.Fatalf("want 1 MarkFailed call, got %d", len(fakes.failed))
	}
	if !strings.Contains(fakes.failed[0].errorMsg, "dependency not yet satisfied") {
		t.Errorf("MarkFailed errorMsg = %q, want substring %q", fakes.failed[0].errorMsg, "dependency not yet satisfied")
	}
	if len(fakes.processed) != 0 {
		t.Error("MarkProcessed must NOT be called on retriable success")
	}
}

// TestWorker_FlowNotRetriableSuccess_MarkProcessed verifies that a normal
// success (IsSuccess=true, IsRetriable=false) still calls MarkProcessed.
// This is the backward-compatibility guard for the #2128 fix.
func TestWorker_FlowNotRetriableSuccess_MarkProcessed(t *testing.T) {
	ev := noopEvent("ev-normal", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{IsSuccess: true},
	}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if len(fakes.processed) != 1 || fakes.processed[0] != "ev-normal" {
		t.Errorf("MarkProcessed not called for ev-normal; processed=%v", fakes.processed)
	}
	if len(fakes.failed) != 0 {
		t.Errorf("unexpected MarkFailed calls: %v", fakes.failed)
	}
}

// TestWorker_FlowError_MarkFailedAndSyncState verifies that a flow returning
// an error (not nil result) also triggers MarkFailed + SyncState.RecordError.
func TestWorker_FlowError_MarkFailedAndSyncState(t *testing.T) {
	ev := noopEvent("ev-3", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{
		returnErr: errors.New("flow internal error"),
	}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected non-nil error")
	}
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed, got %d", len(fakes.failed))
	}
	if len(syncstate.errors) != 1 {
		t.Errorf("want 1 SyncState error, got %d", len(syncstate.errors))
	}
}

// TestWorker_RouteNotDeclared_FailTerminal verifies that when
// the RouteResolver returns ErrRouteNotDeclared for an unknown eventKind,
// the worker calls failTerminal (MarkFailed, NO SyncState write — terminal class
// because ErrRouteNotDeclared is unrecoverable, not a transient object error).
func TestWorker_RouteNotDeclared_FailTerminal(t *testing.T) {
	ev := noopEvent("ev-4", "noop.unknown") // resolver returns ErrRouteNotDeclared for this kind
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected error for route not declared")
	}
	if !errors.Is(err, connector_inbound_routing.ErrRouteNotDeclared) {
		t.Errorf("want ErrRouteNotDeclared in chain, got %v", err)
	}
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed, got %d", len(fakes.failed))
	}
	// failTerminal: NO SyncState write (ErrRouteNotDeclared is unrecoverable).
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState errors (failTerminal for undeclared route), got %d", len(syncstate.errors))
	}
}

// TestWorker_PayloadMissingObjectKind_TerminalMarkFailed verifies that a
// payload missing __objectKind triggers MarkFailed WITHOUT a SyncState write
// (parser-bug class).
func TestWorker_PayloadMissingObjectKind_TerminalMarkFailed(t *testing.T) {
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-5",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-5",
		Status:          "processing",
		ClaimToken:      "token-ev-5",
		Payload:         payloadWithoutObjectKind(), // missing __objectKind
	}
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected error for missing objectKind")
	}
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed, got %d", len(fakes.failed))
	}
	// Terminal class: NO SyncState write.
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState errors for terminal class, got %d: %v",
			len(syncstate.errors), syncstate.errors)
	}
	if len(fakes.processed) != 0 {
		t.Error("unexpected MarkProcessed call")
	}
}

// TestWorker_ClaimNextErrLeaseLost_WorkerContinues verifies that
// ErrLeaseLost from ClaimNext causes the worker to return the error but
// the Run loop can continue (no panic, no abort).
func TestWorker_ClaimNextErrLeaseLost_WorkerContinues(t *testing.T) {
	fakes := newFakeEvents()
	fakes.claimErr = connector_events.ErrLeaseLost
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	w := buildTestWorker(t, fakes, syncstate, flow)

	// processOne propagates the ClaimNext error.
	claimed, err := w.processOne(context.Background())
	if claimed {
		t.Fatal("expected claimed=false when ClaimNext returns error")
	}
	if !errors.Is(err, connector_events.ErrLeaseLost) {
		t.Fatalf("want ErrLeaseLost, got %v", err)
	}
}

// TestWorker_Run_MaxClaims verifies that the mandatory WorkItem worker with
// MaxClaims=3 completes exactly three claimed rows and returns nil.
func TestWorker_Run_MaxClaims(t *testing.T) {
	events := []*connector_events.ConnectorEvent{
		noopEvent("mc-1", "noop.created"),
		noopEvent("mc-2", "noop.created"),
		noopEvent("mc-3", "noop.created"),
		noopEvent("mc-4", "noop.created"), // must remain claimable
	}
	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-mc-1", events[0].ID, connector_workqueue.WorkTypeConnectorEvent, 0, "lease-mc-1"),
		liveWorkItem("work-mc-2", events[1].ID, connector_workqueue.WorkTypeConnectorEvent, 0, "lease-mc-2"),
		liveWorkItem("work-mc-3", events[2].ID, connector_workqueue.WorkTypeConnectorEvent, 0, "lease-mc-3"),
		liveWorkItem("work-mc-4", events[3].ID, connector_workqueue.WorkTypeConnectorEvent, 0, "lease-mc-4"),
	}}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{events: map[string]*connector_events.ConnectorEvent{
		events[0].ID: events[0],
		events[1].ID: events[1],
		events[2].ID: events[2],
		events[3].ID: events[3],
	}}, nil)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	cfg := liveWorkItemConfig()
	cfg.MaxClaims = 3
	cfg.BatchSize = 1
	w, err := NewWorkItemWorker(context.Background(),
		cfg,
		service,
		syncstate,
		fakeBatchRead{},
		flow,
		resolver,
	)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}
	// The production supervisor installs one CentralClaimScheduler for every
	// WorkItem worker in a pack. Exercise that claim-first path here rather than
	// the compatibility scheduler selected when no scheduler is installed.
	scheduler := NewCentralClaimScheduler()
	w.UseCentralClaimScheduler(scheduler)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := w.RunClaimFirstUntilDrained(ctx, nil); err != nil {
		t.Fatalf("RunClaimFirstUntilDrained: %v", err)
	}

	if claims.claimByEventCalls != 3 {
		t.Fatalf("candidate WorkItem CAS calls = %d, want 3", claims.claimByEventCalls)
	}
	if stats := scheduler.Stats(); stats.ClaimAttempts != 3 || stats.ClaimSuccess != 3 || stats.ClaimCASLost != 0 || stats.ClaimErrors != 0 {
		t.Fatalf("central scheduler stats = %+v, want three successful candidate CAS claims", stats)
	}
	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-mc-1,work-mc-2,work-mc-3" {
		t.Fatalf("completed WorkItems = %q, want first three claims", got)
	}
	if got := strings.Join(claims.completeCalls, ","); got != strings.Join([]string{
		connector_workqueue.WorkStateProcessed,
		connector_workqueue.WorkStateProcessed,
		connector_workqueue.WorkStateProcessed,
	}, ",") {
		t.Fatalf("completion states = %q, want three processed states", got)
	}
	if len(claims.works) != 1 || claims.works[0].WorkID != "work-mc-4" {
		t.Fatalf("remaining WorkItems = %+v, want only work-mc-4 claimable", claims.works)
	}
	if len(claims.failInputs) != 0 || claims.retryCalls != 0 {
		t.Fatalf("failure/retry calls = %v/%d, want none", claims.failInputs, claims.retryCalls)
	}
	if flow.calls != 3 || len(syncstate.errors) != 0 {
		t.Fatalf("flow calls/sync errors = %d/%v, want 3/none", flow.calls, syncstate.errors)
	}
}

func TestWorker_FinalizeProcessedUsesFreshContextAfterFlowCancelsOperation(t *testing.T) {
	ev := noopEvent("ev-finalize-processed", "noop.created")
	fakes := newFakeEvents(ev)
	fakes.failOnCanceledFinalize = true
	syncstate := &fakeSyncState{}
	ctx, cancel := context.WithCancel(context.Background())
	flow := &cancelingFlowInvoker{
		cancel: cancel,
		result: &FlowResult{IsSuccess: true},
	}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(ctx)
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if len(fakes.processed) != 1 || fakes.processed[0] != ev.ID {
		t.Fatalf("processed = %v, want [%s]", fakes.processed, ev.ID)
	}
	if len(fakes.processedCtxErrs) != 1 || fakes.processedCtxErrs[0] != nil {
		t.Fatalf("MarkProcessed ctx errors = %v, want [nil]", fakes.processedCtxErrs)
	}
}

func TestWorker_FinalizeFailedUsesFreshContextAfterFlowCancelsOperation(t *testing.T) {
	ev := noopEvent("ev-finalize-failed", "noop.created")
	fakes := newFakeEvents(ev)
	fakes.failOnCanceledFinalize = true
	syncstate := &fakeSyncState{}
	ctx, cancel := context.WithCancel(context.Background())
	flow := &cancelingFlowInvoker{
		cancel: cancel,
		result: &FlowResult{
			IsSuccess:  false,
			FirstError: &FlowError{StatusCode: "FLOW_ERROR", Message: "side effect failed"},
		},
	}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(ctx)
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected flow failure error")
	}
	if len(fakes.failed) != 1 || fakes.failed[0].eventID != ev.ID {
		t.Fatalf("failed = %v, want one failure for %s", fakes.failed, ev.ID)
	}
	if len(fakes.failedCtxErrs) != 1 || fakes.failedCtxErrs[0] != nil {
		t.Fatalf("MarkFailed ctx errors = %v, want [nil]", fakes.failedCtxErrs)
	}
}

// TestWorker_RunUntilDrainedStopsClaimsOnDrain verifies that the WorkItem
// executor completes a row already in flight when drain fires, then stops before
// claiming the next WorkItem. The fake removes a row only when its WorkItem claim
// succeeds, so the remaining slice is the still-claimable queue state.
func TestWorker_RunUntilDrainedStopsClaimsOnDrain(t *testing.T) {
	first := livePendingEvent("drain-current", "noop.created")
	second := livePendingEvent("drain-next", "noop.created")
	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-drain-current", first.ID, connector_workqueue.WorkTypeConnectorEvent, 0, "lease-drain-current"),
		liveWorkItem("work-drain-next", second.ID, connector_workqueue.WorkTypeConnectorEvent, 0, "lease-drain-next"),
	}}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{events: map[string]*connector_events.ConnectorEvent{
		first.ID:  first,
		second.ID: second,
	}}, nil)
	syncstate := &fakeSyncState{}
	flow := &blockingOnceFlowInvoker{
		started: make(chan struct{}),
		release: make(chan struct{}),
	}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}
	cfg := liveWorkItemConfig()
	cfg.BatchSize = 1
	w, err := NewWorkItemWorker(context.Background(), cfg, service, syncstate, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}

	drain := make(chan struct{})
	runDone := make(chan error, 1)
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	go func() {
		runDone <- w.RunUntilDrained(ctx, drain)
	}()

	select {
	case <-flow.started:
	case <-ctx.Done():
		t.Fatal("timed out waiting for first WorkItem to enter flow")
	}
	// Close drain while the first WorkItem is executing. The completed first
	// row must be terminal; the second remains pending and claimable.
	close(drain)
	close(flow.release)

	select {
	case err := <-runDone:
		if err != nil {
			t.Fatalf("RunUntilDrained: %v", err)
		}
	case <-ctx.Done():
		t.Fatal("timed out waiting for WorkItem drain")
	}

	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-drain-current" {
		t.Fatalf("completed WorkItems = %q, want only the in-flight row", got)
	}
	if got := strings.Join(claims.completeCalls, ","); got != connector_workqueue.WorkStateProcessed {
		t.Fatalf("terminal states = %q, want processed", got)
	}
	if len(claims.failInputs) != 0 || claims.retryCalls != 0 {
		t.Fatalf("failure/retry calls = %v/%d, want none", claims.failInputs, claims.retryCalls)
	}
	if claims.claimCalls != 1 {
		t.Fatalf("WorkItem claims = %d, want one before drain", claims.claimCalls)
	}
	if len(claims.works) != 1 || claims.works[0].EventID != second.ID {
		t.Fatalf("remaining claimable WorkItems = %v, want only %q", claims.works, second.ID)
	}
	if len(syncstate.errors) != 0 {
		t.Fatalf("SyncState.RecordError calls = %v, want none", syncstate.errors)
	}
}

func TestWorker_RunUntilDrainedCancelsInFlightClaimOnly(t *testing.T) {
	events := &blockingClaimEvents{started: make(chan struct{})}
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}
	w := buildTestWorker(t, events, syncstate, flow)
	runCtx := context.Background()
	drain := make(chan struct{})
	runDone := make(chan error, 1)

	go func() {
		runDone <- w.RunUntilDrained(runCtx, drain)
	}()

	select {
	case <-events.started:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for ClaimNext")
	}
	close(drain)

	select {
	case err := <-runDone:
		if err != nil {
			t.Fatalf("RunUntilDrained: %v", err)
		}
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for claim drain")
	}
	if !errors.Is(events.ctxErr, context.Canceled) {
		t.Fatalf("ClaimNext ctxErr = %v, want context.Canceled", events.ctxErr)
	}
}

// TestWorker_PayloadMissingSourceID_TerminalMarkFailed verifies that a
// payload missing __sourceID triggers MarkFailed WITHOUT a SyncState write
// (parser-bug class).
func TestWorker_PayloadMissingSourceID_TerminalMarkFailed(t *testing.T) {
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-sid-missing",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-sid-missing",
		Status:          "processing",
		ClaimToken:      "token-ev-sid-missing",
		Payload:         payloadWithoutSourceID(), // has __objectKind but no __sourceID
	}
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected error for missing __sourceID")
	}
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed, got %d", len(fakes.failed))
	}
	// Terminal class: NO SyncState write.
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState errors for terminal class, got %d: %v",
			len(syncstate.errors), syncstate.errors)
	}
	if len(fakes.processed) != 0 {
		t.Error("unexpected MarkProcessed call")
	}
}

// TestWorker_HappyPath_SourceIDUsedForBatchRead verifies that the worker
// extracts __sourceID from the payload (not IdempotencyKey) for BatchRead.
// noopEvent sets IdempotencyKey=id but sourceID="src-"+id in the payload,
// so if the worker accidentally used IdempotencyKey the batch-read key would differ.
func TestWorker_HappyPath_SourceIDUsedForBatchRead(t *testing.T) {
	// fakeBatchReadCapture records the sourceIDs passed to BatchRead.
	type batchReadCall struct {
		ns, objectKind string
		sourceIDs      []string
	}
	var capturedCalls []batchReadCall
	capturer := &capturingBatchRead{
		fn: func(ns, objectKind string, ids []string) {
			capturedCalls = append(capturedCalls, batchReadCall{ns, objectKind, ids})
		},
	}

	ev := livePendingEvent("ev-brc-1", "noop.created")
	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-brc-1", ev.ID, connector_workqueue.WorkTypeConnectorEvent, 0, ""),
	}}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{event: ev}, nil)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}
	w, err := NewWorkItemWorker(context.Background(),
		liveWorkItemConfig(),
		service,
		syncstate,
		capturer,
		flow,
		resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-brc-1" {
		t.Fatalf("completed WorkItems = %q, want work-brc-1", got)
	}
	if got := strings.Join(claims.completeCalls, ","); got != connector_workqueue.WorkStateProcessed {
		t.Fatalf("WorkItem terminal states = %q, want processed", got)
	}
	if len(claims.failInputs) != 0 {
		t.Fatalf("FailWorkItem calls = %v, want none", claims.failInputs)
	}
	if claims.retryCalls != 0 || len(claims.retryWorkIDs) != 0 {
		t.Fatalf("RetryWorkItem calls/IDs = %d/%v, want 0/[]", claims.retryCalls, claims.retryWorkIDs)
	}
	if len(syncstate.errors) != 0 {
		t.Fatalf("SyncState errors = %v, want none", syncstate.errors)
	}

	if len(capturedCalls) != 1 {
		t.Fatalf("want 1 BatchRead call, got %d", len(capturedCalls))
	}
	got := capturedCalls[0]
	wantSourceID := "src-ev-brc-1" // noopEvent sets sourceID = "src-" + id
	if len(got.sourceIDs) != 1 || got.sourceIDs[0] != wantSourceID {
		t.Errorf("BatchRead sourceIDs = %v, want [%q]", got.sourceIDs, wantSourceID)
	}
	// Confirm it did NOT use the IdempotencyKey.
	if len(got.sourceIDs) == 1 && got.sourceIDs[0] == "ev-brc-1" {
		t.Error("worker incorrectly used IdempotencyKey instead of __sourceID for BatchRead")
	}
}

// capturingFlowInvoker captures the inputs map passed to ExecuteFlow.
type capturingFlowInvoker struct {
	mu           sync.Mutex
	capturedArgs []map[string]any
	result       *FlowResult
}

func (c *capturingFlowInvoker) ExecuteFlow(_ context.Context, _ string, inputs map[string]any, _ *models.UserSession) (*FlowResult, error) {
	c.mu.Lock()
	c.capturedArgs = append(c.capturedArgs, inputs)
	c.mu.Unlock()
	if c.result != nil {
		return c.result, nil
	}
	return &FlowResult{IsSuccess: true}, nil
}

func (c *capturingFlowInvoker) ExecuteFlowVersion(_ context.Context, _, _ string, inputs map[string]any, _ *models.UserSession) (*FlowResult, error) {
	c.mu.Lock()
	c.capturedArgs = append(c.capturedArgs, inputs)
	c.mu.Unlock()
	if c.result != nil {
		return c.result, nil
	}
	return &FlowResult{IsSuccess: true}, nil
}

func TestWorker_StampsConnectorSyncOriginBeforeFlowExecution(t *testing.T) {
	ev := noopEvent("ev-origin-1", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	w := buildTestWorker(t, fakes, syncstate, flow)
	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if flow.calls != 1 {
		t.Fatalf("ExecuteFlow calls = %d, want 1", flow.calls)
	}
	if len(flow.origins) != 1 || flow.origins[0] != workerTestNS {
		t.Fatalf("connector sync origins = %v, want [%q]", flow.origins, workerTestNS)
	}
}

func TestWorker_StampsRedactedConnectorInboundExecutionTrace(t *testing.T) {
	events := &workItemEventsService{
		claimed: map[string]claimedWorkItemRef{
			"event-1": {workID: "work-1", leaseToken: "lease-1", attempt: 3},
		},
	}
	w := &Worker{
		cfg:    WorkerConfig{Namespace: "crm-pack"},
		events: events,
	}
	inputs := map[string]any{
		"payload": map[string]any{"propertyName": "firstname"},
		"record": map[string]any{
			"id":                  "hubspot-contact-123",
			"salesforceleadid":    "00Q123456789ABC",
			"salesforcecontactid": "",
		},
	}
	ctx := w.withConnectorInboundExecutionTrace(
		context.Background(),
		&connector_events.ConnectorEvent{ID: "event-1"},
		preparedEvent{objectKind: "contacts", sourceID: "hubspot-contact-123"},
		inputs,
	)
	trace, ok := constants.ConnectorInboundTraceFromContext(ctx)
	if !ok {
		t.Fatal("ConnectorInboundTraceFromContext() ok = false, want true")
	}
	if trace.WorkItemID != "work-1" || trace.EventID != "event-1" || trace.Attempt != 3 || trace.Namespace != "crm-pack" || trace.ObjectKind != "contacts" {
		t.Fatalf("trace lineage = %#v", trace)
	}
	if trace.SourceIDHash == "" || strings.Contains(trace.SourceIDHash, "hubspot-contact-123") {
		t.Fatalf("SourceIDHash = %q, want non-reversible digest", trace.SourceIDHash)
	}
	if trace.HydratedRecordHash == "" || trace.HydratedFieldCount != 3 {
		t.Fatalf("hydrated fingerprint = %q/%d, want digest/3", trace.HydratedRecordHash, trace.HydratedFieldCount)
	}
	encoded, err := json.Marshal(trace)
	if err != nil {
		t.Fatalf("marshal trace: %v", err)
	}
	for _, forbidden := range []string{"hubspot-contact-123", "00Q123456789ABC", "salesforceleadid"} {
		if strings.Contains(string(encoded), forbidden) {
			t.Fatalf("trace leaked %q: %s", forbidden, encoded)
		}
	}
}

func TestWorker_StampsEveryBatchMemberInConnectorInboundExecutionTrace(t *testing.T) {
	w := &Worker{cfg: WorkerConfig{Namespace: "crm-pack"}}
	ctx := w.withConnectorInboundBatchExecutionTrace(context.Background(), []constants.ConnectorInboundTraceItem{
		{WorkItemID: "work-1", EventID: "event-1", ObjectKind: "contacts", SourceIDHash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
		{WorkItemID: "work-2", EventID: "event-2", ObjectKind: "contacts", SourceIDHash: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},
	})
	trace, ok := constants.ConnectorInboundTraceFromContext(ctx)
	if !ok {
		t.Fatal("ConnectorInboundTraceFromContext() ok = false, want true")
	}
	if trace.EventID != "event-1" || trace.WorkItemID != "work-1" || len(trace.Related) != 1 {
		t.Fatalf("batch trace = %#v", trace)
	}
	if trace.Related[0].EventID != "event-2" || trace.Related[0].WorkItemID != "work-2" {
		t.Fatalf("related trace = %#v", trace.Related[0])
	}
	if trace.SourceIDHash == "" || trace.Related[0].SourceIDHash == "" {
		t.Fatalf("batch trace hashes were not preserved: %#v", trace)
	}
}

func propertyChangeEvent(id, propertyName string) *connector_events.ConnectorEvent {
	payload, err := EncodeQueueEnvelope("contact", "contact-123", json.RawMessage(fmt.Sprintf(`{"propertyName":%q}`, propertyName)))
	if err != nil {
		panic(err)
	}
	return &connector_events.ConnectorEvent{
		ID:              id,
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "contact.propertychange",
		IdempotencyKey:  id,
		Status:          "processing",
		ClaimToken:      "token-" + id,
		Payload:         json.RawMessage(payload),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
	}
}

type fakeOutboundEchoProbe struct {
	evidence OutboundEchoEvidence
	err      error
	inputs   []OutboundEchoProbeInput
}

func (p *fakeOutboundEchoProbe) ProbeRecentOutboundEcho(_ context.Context, in OutboundEchoProbeInput) (OutboundEchoEvidence, error) {
	p.inputs = append(p.inputs, in)
	return p.evidence, p.err
}

type countingBatchRead struct {
	calls int
}

func (c *countingBatchRead) BatchRead(_ context.Context, _, _ string, _ []string) (map[string]json.RawMessage, error) {
	c.calls++
	return map[string]json.RawMessage{}, nil
}

func TestWorker_FlowExecutionFilterSkipsBeforeBatchReadAndFlow(t *testing.T) {
	ev := propertyChangeEvent("evt-skip", "hs_analytics_source")
	syncs := &fakeSyncState{}
	batch := &countingBatchRead{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/contact.propertychange": {
				FlowDeveloperName: "HubSpot_Contact_Lead_Upsert",
				FlowExecutionFilter: &connector_inbound_routing.PayloadValueFilter{
					Path:      ".propertyName",
					EqualsAny: []string{"email", "hubspot_owner_id"},
				},
			},
		},
	}
	claims := &fakeWorkItemClaims{work: liveWorkItem(
		"work-evt-skip",
		ev.ID,
		connector_workqueue.WorkTypeConnectorEvent,
		0,
		"lease-evt-skip",
	)}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{event: ev}, nil)
	cfg := liveWorkItemConfig()
	cfg.MaxClaims = 1
	w, err := NewWorkItemWorker(context.Background(), cfg, service, syncs, batch, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-evt-skip" {
		t.Fatalf("completed WorkItems = %v, want [work-evt-skip]", claims.completeWorkIDs)
	}
	if got := claims.completeCalls; len(got) != 1 || got[0] != connector_workqueue.WorkStateSkipped {
		t.Fatalf("completion states = %v, want [skipped]", got)
	}
	if batch.calls != 0 || flow.calls != 0 {
		t.Fatalf("batch calls=%d flow calls=%d, want both 0", batch.calls, flow.calls)
	}
	if len(claims.failInputs) != 0 || len(claims.retryWorkIDs) != 0 || len(syncs.errors) != 0 {
		t.Fatalf("skip must not record failure: failed=%v retried=%v syncErrors=%v", claims.failInputs, claims.retryWorkIDs, syncs.errors)
	}
}

func TestRunUntilDrained_BatchCollectionInvokesFlowOnce(t *testing.T) {
	ev1 := noopEvent("batch-one", "noop.created")
	ev2 := noopEvent("batch-two", "noop.created")
	events := newFakeEvents(ev1, ev2)
	syncs := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "batch-one", "success": true},
				map[string]any{"eventID": "batch-two", "success": true},
			},
		},
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     2,
		BatchSize:     2,
	}, events, syncs, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), nil); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if flow.calls != 1 {
		t.Fatalf("flow calls = %d, want 1", flow.calls)
	}
	if got := strings.Join(events.processed, ","); got != "batch-one,batch-two" {
		t.Fatalf("processed = %v, want [batch-one batch-two]", events.processed)
	}
}

func TestProcessOne_BatchCollectionUsesCollectionContract(t *testing.T) {
	ev1 := noopEvent("batch-one", "noop.created")
	ev2 := noopEvent("batch-two", "noop.created")
	events := newFakeEvents(ev1, ev2)
	syncs := &fakeSyncState{}
	flow := &capturingFlowInvoker{result: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "batch-one", "success": true},
				map[string]any{"eventID": "batch-two", "success": true},
			},
		},
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     2,
	}, events, syncs, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := strings.Join(events.processed, ","); got != "batch-one,batch-two" {
		t.Fatalf("processed = %v, want [batch-one batch-two]", events.processed)
	}
	if len(flow.capturedArgs) != 1 {
		t.Fatalf("captured flow calls = %d, want 1", len(flow.capturedArgs))
	}
	items, ok := flow.capturedArgs[0]["events"].([]any)
	if !ok || len(items) != 2 {
		t.Fatalf("events input = %#v, want two-item collection", flow.capturedArgs[0]["events"])
	}
}

func TestRunUntilDrained_BatchCollectionMixedResultsDefersLaterSameDelivery(t *testing.T) {
	ev1 := noopEvent("mixed-drain-anchor", "noop.created")
	ev2 := noopEvent("mixed-drain-later", "noop.created")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.DeliveryId = "delivery-mixed-drain"
		ev.DeliverySequence = i + 1
	}
	events := newFakeEvents(ev1, ev2)
	drain := make(chan struct{})
	var closeDrain sync.Once
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{
			IsSuccess: true,
			OutputValues: map[string]any{
				"eventResults": []any{
					map[string]any{"eventID": "mixed-drain-anchor", "success": false, "retriable": true, "message": "retry anchor"},
					map[string]any{"eventID": "mixed-drain-later", "success": true},
				},
			},
		},
		onExecute: func() {
			closeDrain.Do(func() { close(drain) })
		},
	}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     2,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), drain); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if got := strings.Join(failedEventIDs(events.failed), ","); got != "mixed-drain-anchor" {
		t.Fatalf("failed = %v, want [mixed-drain-anchor]", events.failed)
	}
	if got := strings.Join(failedEventIDs(events.releasedForRetry), ","); got != "mixed-drain-later" {
		t.Fatalf("releasedForRetry = %v, want [mixed-drain-later]", events.releasedForRetry)
	}
	if len(events.released) != 0 {
		t.Fatalf("plain released = %v, want none after the batch Flow executed both members", events.released)
	}
	if len(events.processed) != 0 {
		t.Fatalf("processed = %v, want none", events.processed)
	}
	events.mu.Lock()
	laterClaimed := events.claimed["mixed-drain-later"]
	events.mu.Unlock()
	if laterClaimed {
		t.Fatal("mixed-drain-later remained claimed after earlier same-delivery retriable result")
	}
}

func TestProcessOne_BatchCollectionMixedResultsDefersLaterSameDelivery(t *testing.T) {
	ev1 := noopEvent("mixed-claim-anchor", "noop.created")
	ev2 := noopEvent("mixed-claim-later", "noop.created")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.DeliveryId = "delivery-mixed-claim"
		ev.DeliverySequence = i + 1
	}
	events := newFakeEvents(ev1, ev2)
	flow := &capturingFlowInvoker{result: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "mixed-claim-anchor", "success": false, "retriable": true, "message": "retry anchor"},
				map[string]any{"eventID": "mixed-claim-later", "success": true},
			},
		},
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if err == nil {
		t.Fatal("processOne error = nil, want retriable anchor result")
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := strings.Join(failedEventIDs(events.failed), ","); got != "mixed-claim-anchor" {
		t.Fatalf("failed = %v, want [mixed-claim-anchor]", events.failed)
	}
	if got := strings.Join(failedEventIDs(events.releasedForRetry), ","); got != "mixed-claim-later" {
		t.Fatalf("releasedForRetry = %v, want [mixed-claim-later]", events.releasedForRetry)
	}
	if len(events.released) != 0 {
		t.Fatalf("plain released = %v, want none after the batch Flow executed both members", events.released)
	}
	if len(events.processed) != 0 {
		t.Fatalf("processed = %v, want none", events.processed)
	}
	events.mu.Lock()
	laterClaimed := events.claimed["mixed-claim-later"]
	events.mu.Unlock()
	if laterClaimed {
		t.Fatal("mixed-claim-later remained claimed after earlier same-delivery retriable result")
	}
}

func TestProcessOne_CompanionBatchCollectionUsesCollectionContract(t *testing.T) {
	ev := noopEvent("companion-batch-one", "noop.created")
	ev.CompanionPackInstallID = "companion-install"
	ev.CompanionFlowVersionID = "companion-flow-version"
	events := newFakeEvents(ev)
	syncs := &fakeSyncState{}
	flow := &capturingFlowInvoker{result: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "companion-batch-one", "success": true},
			},
		},
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "CompanionBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     2,
	}, events, syncs, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	w.SetCompanionInstalledChecker(func(context.Context, string) (bool, error) { return true, nil })
	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := strings.Join(events.processed, ","); got != "companion-batch-one" {
		t.Fatalf("processed = %v, want [companion-batch-one]", events.processed)
	}
	if len(flow.capturedArgs) != 1 {
		t.Fatalf("captured flow calls = %d, want 1", len(flow.capturedArgs))
	}
	items, ok := flow.capturedArgs[0]["events"].([]any)
	if !ok || len(items) != 1 {
		t.Fatalf("events input = %#v, want single-item collection", flow.capturedArgs[0]["events"])
	}
}

func TestProcessOne_BatchCollectionClaimErrorFinalizesClaimedGroup(t *testing.T) {
	ev1 := noopEvent("batch-one", "noop.created")
	ev2 := noopEvent("batch-two", "noop.created")
	ev3 := noopEvent("batch-three", "noop.created")
	events := newFakeEvents(ev1, ev2, ev3)
	events.claimByIDErrs = map[string]error{"batch-three": errors.New("claim failed")}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      3,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     3,
	}, events, &fakeSyncState{}, fakeBatchRead{}, &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if err == nil {
		t.Fatal("processOne error = nil, want claim failure")
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := failedEventIDs(events.failed); strings.Join(got, ",") != "batch-one,batch-two" {
		t.Fatalf("failed = %v, want [batch-one batch-two]", got)
	}
}

func TestProcessOne_CompanionBatchCollectionRecheckFailureFinalizesWholeGroup(t *testing.T) {
	ev1 := noopEvent("companion-batch-one", "noop.created")
	ev2 := noopEvent("companion-batch-two", "noop.created")
	for _, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.CompanionPackInstallID = "companion-install"
		ev.CompanionFlowVersionID = "companion-flow-version"
	}
	events := newFakeEvents(ev1, ev2)
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "CompanionBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      2,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	checks := 0
	w.SetCompanionInstalledChecker(func(context.Context, string) (bool, error) {
		checks++
		return checks <= 2, nil
	})
	claimed, err := w.processOne(context.Background())
	if err == nil {
		t.Fatal("processOne error = nil, want companion re-check failure")
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := failedEventIDs(events.failed); strings.Join(got, ",") != "companion-batch-one,companion-batch-two" {
		t.Fatalf("failed = %v, want [companion-batch-one companion-batch-two]", got)
	}
}

func TestProcessOne_BatchCollectionCandidatePrepareFailureDoesNotPoisonAnchor(t *testing.T) {
	ev1 := noopEvent("prep-anchor", "noop.created")
	ev2 := noopEvent("prep-bad-candidate", "noop.created")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.CompanionPackInstallID = "companion-install"
		ev.CompanionFlowVersionID = "companion-flow-version"
		ev.DeliveryId = "delivery-prep-candidate"
		ev.DeliverySequence = i + 1
	}
	events := newFakeEvents(ev1, ev2)
	flow := &capturingFlowInvoker{result: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "prep-anchor", "success": true},
			},
		},
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "CompanionBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      2,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	checks := 0
	w.SetCompanionInstalledChecker(func(context.Context, string) (bool, error) {
		checks++
		return checks != 2, nil
	})
	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := strings.Join(events.processed, ","); got != "prep-anchor" {
		t.Fatalf("processed = %v, want [prep-anchor]", events.processed)
	}
	if len(events.failed) != 0 {
		t.Fatalf("failed = %v, want none before anchor terminal success", events.failed)
	}
	if got := strings.Join(events.released, ","); got != "prep-bad-candidate" {
		t.Fatalf("released = %v, want [prep-bad-candidate]", events.released)
	}
	events.mu.Lock()
	candidateClaimed := events.claimed["prep-bad-candidate"]
	events.mu.Unlock()
	if candidateClaimed {
		t.Fatal("prep-bad-candidate remained claimed after pre-anchor prepare failure")
	}
	if len(flow.capturedArgs) != 1 {
		t.Fatalf("captured flow calls = %d, want 1", len(flow.capturedArgs))
	}
	items, ok := flow.capturedArgs[0]["events"].([]any)
	if !ok || len(items) != 1 {
		t.Fatalf("events input = %#v, want one valid anchor item", flow.capturedArgs[0]["events"])
	}
}

func TestProcessOne_BatchCollectionBatchReadErrorReleasesSameDeliverySibling(t *testing.T) {
	ev1 := noopEvent("batch-read-anchor", "noop.created")
	ev2 := noopEvent("batch-read-later", "noop.created")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.DeliveryId = "delivery-batch-read"
		ev.DeliverySequence = i + 1
	}
	events := newFakeEvents(ev1, ev2)
	syncs := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}
	batchReadErr := errors.New("batch read engine unavailable")
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      2,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     2,
	}, events, syncs, &errorBatchRead{err: batchReadErr}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if err == nil || !errors.Is(err, batchReadErr) {
		t.Fatalf("processOne error = %v, want batchReadErr", err)
	}
	if got := strings.Join(failedEventIDs(events.failed), ","); got != "batch-read-anchor" {
		t.Fatalf("failed = %v, want [batch-read-anchor]", events.failed)
	}
	if len(syncs.errors) != 1 {
		t.Fatalf("sync errors = %d, want 1 for anchor only", len(syncs.errors))
	}
	if got := strings.Join(events.released, ","); got != "batch-read-later" {
		t.Fatalf("released = %v, want [batch-read-later]", events.released)
	}
	events.mu.Lock()
	laterClaimed := events.claimed["batch-read-later"]
	events.mu.Unlock()
	if laterClaimed {
		t.Fatal("batch-read-later remained claimed after pre-anchor batch read failure")
	}
	if flow.calls != 0 {
		t.Fatalf("flow calls = %d, want 0 before successful batch read", flow.calls)
	}
}

func TestRunClaimFirstUntilDrained_BatchCollectionHonorsClaimLimiterMaxClaims(t *testing.T) {
	ev1 := noopEvent("limited-batch-anchor", "noop.created")
	ev2 := noopEvent("limited-batch-later", "noop.created")
	events := newFakeEvents(ev1, ev2)
	limiter := &recordingClaimLimiter{
		decision: ClaimLimiterDecision{
			Allow:     true,
			MaxClaims: 1,
		},
	}
	flow := &capturingFlowInvoker{result: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "limited-batch-anchor", "success": true},
			},
		},
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      2,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     2,
		ClaimLimiter:  limiter,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunClaimFirstUntilDrained(context.Background(), nil); err != nil {
		t.Fatalf("RunClaimFirstUntilDrained: %v", err)
	}
	if len(limiter.requests) != 1 {
		t.Fatalf("claim limiter requests = %d, want 1", len(limiter.requests))
	}
	if got := limiter.requests[0].BatchSize; got != 2 {
		t.Fatalf("claim limiter BatchSize = %d, want worker batch size 2", got)
	}
	if len(limiter.releases) != 1 || limiter.releases[0] != 1 {
		t.Fatalf("claim limiter releases = %v, want [1]", limiter.releases)
	}
	if got := strings.Join(events.processed, ","); got != "limited-batch-anchor" {
		t.Fatalf("processed = %v, want [limited-batch-anchor]", events.processed)
	}
	events.mu.Lock()
	laterClaimed := events.claimed["limited-batch-later"]
	events.mu.Unlock()
	if laterClaimed {
		t.Fatal("limited-batch-later was claimed beyond claim limiter MaxClaims")
	}
	if len(flow.capturedArgs) != 1 {
		t.Fatalf("captured flow calls = %d, want 1", len(flow.capturedArgs))
	}
	items, ok := flow.capturedArgs[0]["events"].([]any)
	if !ok || len(items) != 1 {
		t.Fatalf("events input = %#v, want one item within claim limiter MaxClaims", flow.capturedArgs[0]["events"])
	}
}

func TestRunUntilDrained_BatchCollectionKeepsRouteSnapshotsSeparate(t *testing.T) {
	ev1 := noopEvent("route-one", "noop.created")
	ev2 := noopEvent("route-two", "noop.updated")
	events := newFakeEvents(ev1, ev2)
	flow := &capturingFlowInvoker{result: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "route-one", "success": true},
				map[string]any{"eventID": "route-two", "success": true},
			},
		},
	}}
	spec := connector_inbound_routing.RouteExecutionSpec{
		FlowDeveloperName: "SharedBatchFlow",
		InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
		MaxBatchSize:      10,
	}
	resolver := &stubRouteResolver{specs: map[string]connector_inbound_routing.RouteExecutionSpec{
		workerTestNS + "/noop.created": spec,
		workerTestNS + "/noop.updated": spec,
	}}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     2,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), nil); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if len(flow.capturedArgs) != 2 {
		t.Fatalf("captured flow calls = %d, want 2 route-specific calls", len(flow.capturedArgs))
	}
	for i, args := range flow.capturedArgs {
		items, ok := args["events"].([]any)
		if !ok || len(items) != 1 {
			t.Fatalf("flow call %d events input = %#v, want one route-specific event", i, args["events"])
		}
	}
	if got := strings.Join(events.processed, ","); got != "route-one,route-two" {
		t.Fatalf("processed = %v, want [route-one route-two]", events.processed)
	}
}

func TestRunUntilDrained_BatchCollectionRetriableFlushStopsSameDeliverySibling(t *testing.T) {
	ev1 := noopEvent("batch-anchor", "noop.created")
	ev2 := noopEvent("batch-later", "noop.updated")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.DeliveryId = "delivery-batch-order"
		ev.DeliverySequence = i + 1
	}
	events := newFakeEvents(ev1, ev2)
	drain := make(chan struct{})
	var closeDrain sync.Once
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{
			IsSuccess:   true,
			IsRetriable: true,
		},
		onExecute: func() {
			closeDrain.Do(func() { close(drain) })
		},
	}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
			},
			workerTestNS + "/noop.updated": {
				FlowDeveloperName: "NoopOtherBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     2,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), drain); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if flow.calls != 1 {
		t.Fatalf("flow calls = %d, want 1; later same-delivery sibling must not execute after retriable batch flush", flow.calls)
	}
	if len(events.processed) != 0 {
		t.Fatalf("processed = %v, want none", events.processed)
	}
	if got := strings.Join(failedEventIDs(events.failed), ","); got != "batch-anchor" {
		t.Fatalf("failed = %v, want [batch-anchor]", events.failed)
	}
	events.mu.Lock()
	laterClaimed := events.claimed["batch-later"]
	events.mu.Unlock()
	if laterClaimed {
		t.Fatal("batch-later was claimed before the earlier same-delivery batch reached terminal success")
	}
}

// TestRunUntilDrained_BatchCollectionFailedRetriable_ReleasesForRetry verifies
// that a batch-collection flow returning a FAILED retriable result
// (IsSuccess=false, IsRetriable=true — the shape a FlowRetriableFault produces)
// re-drives every batch member via ReleaseClaimForRetry instead of MarkFailed,
// mirroring the single-event releaseClaimForRetry path (OUT-1 / #3122). This is
// distinct from the IsSuccess=true,IsRetriable=true case (#2128) which stays on
// MarkFailed. Without this, a concurrent create-vs-adopt loser routed through a
// batch-collection inbound flow would still burn the retry budget and
// dead-letter before the 15-minute winner lease expires.
func TestRunUntilDrained_BatchCollectionFailedRetriable_ReleasesForRetry(t *testing.T) {
	ev := noopEvent("batch-failed-retriable", "noop.created")
	events := newFakeEvents(ev)
	drain := make(chan struct{})
	var closeDrain sync.Once
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{
			IsSuccess:   false,
			IsRetriable: true,
			FirstError:  &FlowError{StatusCode: "RETRIABLE", Message: "retry after the in-flight create finalizes"},
		},
		onExecute: func() {
			closeDrain.Do(func() { close(drain) })
		},
	}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     1,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), drain); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	// Failed-retriable batch result -> ReleaseClaimForRetry (backed-off re-drive),
	// NOT MarkFailed (budget burn) and NOT MarkProcessed.
	if got := strings.Join(failedEventIDs(events.releasedForRetry), ","); got != "batch-failed-retriable" {
		t.Fatalf("releasedForRetry = %v, want [batch-failed-retriable]", events.releasedForRetry)
	}
	if len(events.failed) != 0 {
		t.Errorf("MarkFailed must NOT be called for a batch failed-retriable result, got %v", events.failed)
	}
	if len(events.processed) != 0 {
		t.Errorf("processed = %v, want none", events.processed)
	}
}

func TestRunUntilDrained_BatchCollectionFailureDefersExecutedSameDeliverySiblingForRetry(t *testing.T) {
	ev1 := noopEvent("batch-failure-anchor", "noop.created")
	ev2 := noopEvent("batch-failure-later", "noop.created")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.DeliveryId = "delivery-batch-failure"
		ev.DeliverySequence = i + 1
	}
	events := newFakeEvents(ev1, ev2)
	drain := make(chan struct{})
	var closeDrain sync.Once
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{
			IsSuccess: false,
			FirstError: &FlowError{
				StatusCode: "EMAIL_MALFORMED",
				Message:    "presentation only",
			},
		},
		onExecute: func() {
			closeDrain.Do(func() { close(drain) })
		},
	}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     2,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), drain); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if got := strings.Join(failedEventIDs(events.failed), ","); got != "batch-failure-anchor" {
		t.Fatalf("failed = %v, want [batch-failure-anchor]", events.failed)
	}
	if got := strings.Join(failedEventIDs(events.releasedForRetry), ","); got != "batch-failure-later" {
		t.Fatalf("releasedForRetry = %v, want [batch-failure-later]", events.releasedForRetry)
	}
	if len(events.released) != 0 {
		t.Fatalf("plain released = %v, want none after the batch Flow executed both members", events.released)
	}
}

func TestRunUntilDrained_BatchCollectionDependencyConsumesFailureBudget(t *testing.T) {
	ev := noopEvent("batch-dependency", "noop.created")
	events := newFakeEvents(ev)
	request, err := models.NewFlowIdentityConvergenceDependencyRequest(
		"Lead", "00Q000000000002AAA", "pingcap-hubspot-companion", "hubspot-contact",
	)
	if err != nil {
		t.Fatalf("NewFlowIdentityConvergenceDependencyRequest: %v", err)
	}
	flow := &dependencyFlowInvoker{
		fakeFlowInvoker:   fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}},
		dependencyRequest: &request,
	}
	resolver := &stubRouteResolver{specs: map[string]connector_inbound_routing.RouteExecutionSpec{
		workerTestNS + "/noop.created": {
			FlowDeveloperName: "NoopBatchFlow",
			InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
			MaxBatchSize:      10,
		},
	}}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     1,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), nil); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if got := strings.Join(failedEventIDs(events.failed), ","); got != ev.ID {
		t.Fatalf("failed = %v, want [%s]", events.failed, ev.ID)
	}
	if len(events.releasedForRetry) != 0 || len(events.blocked) != 0 || len(events.processed) != 0 {
		t.Fatalf("batch dependency used an unsafe finalization path: retry=%v blocked=%v processed=%v", events.releasedForRetry, events.blocked, events.processed)
	}
}

func TestRunUntilDrained_BatchCollectionTypedCauseRetriable_ReleasesForRetryDespiteTerminalPresentation(t *testing.T) {
	ev1 := noopEvent("batch-typed-cause-retriable-1", "noop.created")
	ev2 := noopEvent("batch-typed-cause-retriable-2", "noop.created")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.DeliveryId = "delivery-batch-typed-cause-retriable"
		ev.DeliverySequence = i + 1
	}
	events := newFakeEvents(ev1, ev2)
	drain := make(chan struct{})
	var closeDrain sync.Once
	flow := &typedCauseFlowInvoker{
		fakeFlowInvoker: fakeFlowInvoker{
			successResult: &FlowResult{
				IsSuccess: false,
				FirstError: &FlowError{
					StatusCode: "EMAIL_MALFORMED",
					Message:    "presentation only",
				},
			},
			onExecute: func() {
				closeDrain.Do(func() { close(drain) })
			},
		},
		executionID: "batch-flow-execution-123",
		cause: &models.FlowExecutionCause{
			Code:      "DB_LOCK_WAIT_TIMEOUT",
			Category:  models.FlowExecutionCauseCategoryConcurrency,
			Retriable: true,
		},
	}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     2,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), drain); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if got := strings.Join(failedEventIDs(events.releasedForRetry), ","); got != "batch-typed-cause-retriable-1,batch-typed-cause-retriable-2" {
		t.Fatalf("releasedForRetry = %v, want both executed same-delivery members", events.releasedForRetry)
	}
	if len(events.released) != 0 {
		t.Fatalf("plain released = %v, want none because both members executed the retriable Flow", events.released)
	}
	if len(events.failed) != 0 {
		t.Fatalf("typed retriable cause must not consume failure budget, got %v", events.failed)
	}
	if len(events.processed) != 0 {
		t.Fatalf("processed = %v, want none", events.processed)
	}
}

func TestRunUntilDrained_BatchCollectionFailedRetriableDependencyReleasesForRetry(t *testing.T) {
	ev := noopEvent("batch-failed-retriable-dependency", "noop.created")
	events := newFakeEvents(ev)
	request, err := models.NewFlowIdentityConvergenceDependencyRequest(
		"Lead", "00Q000000000003AAA", "pingcap-hubspot-companion", "hubspot-contact",
	)
	if err != nil {
		t.Fatalf("NewFlowIdentityConvergenceDependencyRequest: %v", err)
	}
	drain := make(chan struct{})
	var closeDrain sync.Once
	flow := &dependencyFlowInvoker{
		fakeFlowInvoker: fakeFlowInvoker{
			successResult: &FlowResult{
				IsSuccess:   false,
				IsRetriable: true,
				FirstError:  &FlowError{StatusCode: "RETRIABLE", Message: "retry after the in-flight create finalizes"},
			},
			onExecute: func() { closeDrain.Do(func() { close(drain) }) },
		},
		dependencyRequest: &request,
	}
	resolver := &stubRouteResolver{specs: map[string]connector_inbound_routing.RouteExecutionSpec{
		workerTestNS + "/noop.created": {
			FlowDeveloperName: "NoopBatchFlow",
			InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
			MaxBatchSize:      10,
		},
	}}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     1,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), drain); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if got := strings.Join(failedEventIDs(events.releasedForRetry), ","); got != ev.ID {
		t.Fatalf("releasedForRetry = %v, want [%s]", events.releasedForRetry, ev.ID)
	}
	if len(events.failed) != 0 || len(events.blocked) != 0 || len(events.processed) != 0 {
		t.Fatalf("failed-retriable dependency used an unsafe finalization path: failed=%v blocked=%v processed=%v", events.failed, events.blocked, events.processed)
	}
}

func TestRunUntilDrained_BatchCollectionPrepareFailureReleasesSameDeliverySibling(t *testing.T) {
	ev1 := noopEvent("drain-prep-anchor", "noop.created")
	ev2 := noopEvent("drain-prep-bad-candidate", "noop.created")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.CompanionPackInstallID = "companion-install"
		ev.CompanionFlowVersionID = "companion-flow-version"
		ev.DeliveryId = "delivery-drain-prep-candidate"
		ev.DeliverySequence = i + 1
	}
	for _, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.Status = connector_workqueue.WorkStatePending
		ev.ClaimToken = ""
	}
	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-drain-prep-anchor", ev1.ID, connector_workqueue.WorkTypeWorkflowActionApply, 0, "lease-drain-prep-anchor"),
		liveWorkItem("work-drain-prep-candidate", ev2.ID, connector_workqueue.WorkTypeWorkflowActionApply, 0, "lease-drain-prep-candidate"),
	}}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{events: map[string]*connector_events.ConnectorEvent{
		ev1.ID: ev1,
		ev2.ID: ev2,
	}}, nil)
	flow := &capturingFlowInvoker{result: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "drain-prep-anchor", "success": true},
			},
		},
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "CompanionBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      2,
			},
		},
	}
	cfg := liveWorkItemConfig()
	cfg.MaxClaims = 2
	cfg.BatchSize = 2
	w, err := NewWorkItemWorker(context.Background(), cfg, service, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}
	// The anchor passes preparation, the same-delivery candidate fails and is
	// released, then batch execution re-checks the anchor before invoking its
	// companion flow.
	companionChecks := []bool{true, false, true}
	checks := 0
	w.SetCompanionInstalledChecker(func(context.Context, string) (bool, error) {
		if checks >= len(companionChecks) {
			return false, fmt.Errorf("unexpected companion check %d", checks+1)
		}
		present := companionChecks[checks]
		checks++
		return present, nil
	})
	result, err := w.executeClaim(context.Background(), nil, claimExecutionRequest{MaxClaims: 2})
	if err != nil || !result.Claimed || result.ClaimedRows != 1 {
		t.Fatalf("executeClaim = (%+v, %v), want anchor finalized and candidate released", result, err)
	}
	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-drain-prep-anchor" {
		t.Fatalf("completed WorkItems = %v, want anchor", claims.completeWorkIDs)
	}
	if len(claims.failInputs) != 0 {
		t.Fatalf("failed WorkItems = %v, want none before anchor terminal success", claims.failInputs)
	}
	if got := strings.Join(claims.retryWorkIDs, ","); got != "work-drain-prep-candidate" {
		t.Fatalf("retried WorkItems = %v, want candidate", claims.retryWorkIDs)
	}
	adapter := w.events.(*workItemEventsService)
	if _, err := adapter.claimedRef(ev2.ID, claims.retryWorkTokens[0]); !errors.Is(err, connector_events.ErrLeaseLost) {
		t.Fatalf("candidate remained claimed after prepare failure: %v", err)
	}
	if len(flow.capturedArgs) != 1 {
		t.Fatalf("captured flow calls = %d, want 1", len(flow.capturedArgs))
	}
	if checks != len(companionChecks) {
		t.Fatalf("companion checks = %d, want %d", checks, len(companionChecks))
	}
	items, ok := flow.capturedArgs[0]["events"].([]any)
	if !ok || len(items) != 1 {
		t.Fatalf("events input = %#v, want one valid anchor item", flow.capturedArgs[0]["events"])
	}
}

func TestRunUntilDrained_BatchCollectionBatchReadErrorReleasesSameDeliverySibling(t *testing.T) {
	ev1 := noopEvent("drain-read-anchor", "noop.created")
	ev2 := noopEvent("drain-read-later", "noop.created")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.DeliveryId = "delivery-drain-read"
		ev.DeliverySequence = i + 1
	}
	for _, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.Status = connector_workqueue.WorkStatePending
		ev.ClaimToken = ""
	}
	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-drain-read-anchor", ev1.ID, connector_workqueue.WorkTypeWorkflowActionApply, 0, "lease-drain-read-anchor"),
		liveWorkItem("work-drain-read-later", ev2.ID, connector_workqueue.WorkTypeWorkflowActionApply, 0, "lease-drain-read-later"),
	}}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{events: map[string]*connector_events.ConnectorEvent{
		ev1.ID: ev1,
		ev2.ID: ev2,
	}}, nil)
	flow := &capturingFlowInvoker{result: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "drain-read-anchor", "success": true},
			},
		},
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/noop.created": {
				FlowDeveloperName: "NoopBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      2,
			},
		},
	}
	cfg := liveWorkItemConfig()
	cfg.MaxClaims = 2
	cfg.BatchSize = 2
	w, err := NewWorkItemWorker(context.Background(), cfg, service, &fakeSyncState{}, &fallbackBatchRead{
		missingSourceID: "src-drain-read-later",
		detailJSON:      json.RawMessage(`{"Name":"AnchorRecord"}`),
	}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}
	result, err := w.executeClaim(context.Background(), nil, claimExecutionRequest{MaxClaims: 2})
	if err == nil || !strings.Contains(err.Error(), "required enrichment missing") || !result.Claimed || result.ClaimedRows != 2 {
		t.Fatalf("executeClaim = (%+v, %v), want two admitted WorkItems and batch-read error", result, err)
	}
	if len(claims.completeWorkIDs) != 0 {
		t.Fatalf("completed WorkItems = %v, want none after batch-read failure", claims.completeWorkIDs)
	}
	if len(claims.failInputs) != 1 || claims.failInputs[0].WorkID != "work-drain-read-anchor" {
		t.Fatalf("failed WorkItems = %v, want anchor only", claims.failInputs)
	}
	if got := strings.Join(claims.retryWorkIDs, ","); got != "work-drain-read-later" {
		t.Fatalf("retried WorkItems = %v, want later same-delivery member", claims.retryWorkIDs)
	}
	adapter := w.events.(*workItemEventsService)
	if _, err := adapter.claimedRef(ev2.ID, claims.retryWorkTokens[0]); !errors.Is(err, connector_events.ErrLeaseLost) {
		t.Fatalf("later member remained claimed after batch read fallback: %v", err)
	}
	if len(flow.capturedArgs) != 0 {
		t.Fatalf("captured flow calls = %d, want 0 before successful batch read", len(flow.capturedArgs))
	}
}

func TestRunUntilDrained_BatchCollectionFilterDefersSameDeliverySibling(t *testing.T) {
	ev1 := propertyChangeEvent("filter-batch-anchor", "email")
	ev2 := propertyChangeEvent("filter-batch-later", "hs_analytics_source")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.DeliveryId = "delivery-filter-order"
		ev.DeliverySequence = i + 1
	}
	events := newFakeEvents(ev1, ev2)
	drain := make(chan struct{})
	var closeDrain sync.Once
	flow := &fakeFlowInvoker{
		successResult: &FlowResult{
			IsSuccess:   true,
			IsRetriable: true,
		},
		onExecute: func() {
			closeDrain.Do(func() { close(drain) })
		},
	}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/contact.propertychange": {
				FlowDeveloperName: "FilteredBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
				FlowExecutionFilter: &connector_inbound_routing.PayloadValueFilter{
					Path:      ".propertyName",
					EqualsAny: []string{"email"},
				},
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     2,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	if err := w.RunUntilDrained(context.Background(), drain); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if flow.calls != 1 {
		t.Fatalf("flow calls = %d, want 1", flow.calls)
	}
	if got := strings.Join(failedEventIDs(events.failed), ","); got != "filter-batch-anchor" {
		t.Fatalf("failed = %v, want [filter-batch-anchor]", events.failed)
	}
	if len(events.skipped) != 0 {
		t.Fatalf("skipped = %v, want none before anchor terminal success", events.skipped)
	}
}

func TestProcessOne_BatchCollectionFilterDefersSameDeliverySibling(t *testing.T) {
	ev1 := propertyChangeEvent("claim-filter-anchor", "email")
	ev2 := propertyChangeEvent("claim-filter-later", "hs_analytics_source")
	for i, ev := range []*connector_events.ConnectorEvent{ev1, ev2} {
		ev.DeliveryId = "delivery-claim-filter-order"
		ev.DeliverySequence = i + 1
	}
	events := newFakeEvents(ev1, ev2)
	flow := &fakeFlowInvoker{successResult: &FlowResult{
		IsSuccess:   true,
		IsRetriable: true,
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/contact.propertychange": {
				FlowDeveloperName: "FilteredBatchFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
				MaxBatchSize:      10,
				FlowExecutionFilter: &connector_inbound_routing.PayloadValueFilter{
					Path:      ".propertyName",
					EqualsAny: []string{"email"},
				},
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if err == nil {
		t.Fatal("processOne error = nil, want retriable anchor batch failure")
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if flow.calls != 1 {
		t.Fatalf("flow calls = %d, want 1", flow.calls)
	}
	if got := strings.Join(failedEventIDs(events.failed), ","); got != "claim-filter-anchor" {
		t.Fatalf("failed = %v, want [claim-filter-anchor]", events.failed)
	}
	if len(events.skipped) != 0 {
		t.Fatalf("skipped = %v, want none before anchor terminal success", events.skipped)
	}
}

func TestProcessOne_BatchCollectionSkipsDifferentObjectKindBeforeClaim(t *testing.T) {
	ev1 := noopEvent("object-a", "noop.created")
	ev2 := noopEvent("object-b", "noop.created")
	ev2.Payload = noopPayload("other_object", "src-object-b")
	events := newFakeEvents(ev1, ev2)
	flow := &capturingFlowInvoker{result: &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": []any{
				map[string]any{"eventID": "object-a", "success": true},
			},
		},
	}}
	resolver := &stubRouteResolver{specs: map[string]connector_inbound_routing.RouteExecutionSpec{
		workerTestNS + "/noop.created": {
			FlowDeveloperName: "NoopBatchFlow",
			InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
			MaxBatchSize:      2,
		},
	}}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
		BatchSize:     2,
	}, events, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	events.mu.Lock()
	secondClaimed := events.claimed["object-b"]
	processed := append([]string{}, events.processed...)
	failed := append([]failedCall{}, events.failed...)
	events.mu.Unlock()
	if secondClaimed {
		t.Fatal("object-b was claimed before objectKind eligibility was known")
	}
	if got := strings.Join(processed, ","); got != "object-a" {
		t.Fatalf("processed = %v, want [object-a]", processed)
	}
	if len(failed) != 0 {
		t.Fatalf("failed = %v, want none", failed)
	}
}

func failedEventIDs(calls []failedCall) []string {
	out := make([]string, 0, len(calls))
	for _, call := range calls {
		out = append(out, call.eventID)
	}
	return out
}

func TestWorker_LatestBySourceFullRecordCoalescesAndRequiresRecord(t *testing.T) {
	old := propertyChangeEvent("prop-old", "firstname")
	latest := propertyChangeEvent("prop-latest", "lastname")
	events := newFakeEvents(old)
	events.coalescedSiblings = map[string]*connector_events.ConnectorEvent{"prop-old": latest}
	syncs := &fakeSyncState{}
	flow := &capturingFlowInvoker{}
	batch := &returningBatchRead{result: map[string]json.RawMessage{
		"contact-123": json.RawMessage(`{"id":"contact-123","email":"latest@example.test"}`),
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/contact.propertychange": {
				FlowDeveloperName: "HubSpot_Contact_Lead_Upsert",
				InvocationMode:    connector_inbound_routing.InvocationModeLatestBySourceFullRecord,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
	}, events, syncs, batch, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := strings.Join(events.processed, ","); got != "prop-old,prop-latest" {
		t.Fatalf("processed = %v, want [prop-old prop-latest]", events.processed)
	}
	if len(flow.capturedArgs) != 1 {
		t.Fatalf("captured flow calls = %d, want 1", len(flow.capturedArgs))
	}
	if payload, ok := flow.capturedArgs[0]["payload"].(map[string]any); !ok || len(payload) != 0 {
		t.Fatalf("payload = %#v, want empty object for full-record mode", flow.capturedArgs[0]["payload"])
	}
	if _, ok := flow.capturedArgs[0]["record"].(map[string]any); !ok {
		t.Fatalf("record input missing: %#v", flow.capturedArgs[0])
	}
	if got, ok := flow.capturedArgs[0]["coalescedPayloads"]; ok {
		t.Fatalf("coalescedPayloads = %#v, want absent for legacy full-record mode", got)
	}
}

func TestWorker_LatestBySourceFullRecordPreservePayloadCoalescesWithAllDeltasAndRecord(t *testing.T) {
	old := propertyChangeEvent("prop-old", "firstname")
	latest := propertyChangeEvent("prop-latest", "lastname")
	events := newFakeEvents(old)
	events.coalescedSiblings = map[string]*connector_events.ConnectorEvent{"prop-old": latest}
	flow := &capturingFlowInvoker{}
	batch := &returningBatchRead{result: map[string]json.RawMessage{
		"contact-123": json.RawMessage(`{"id":"contact-123","email":"latest@example.test"}`),
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/contact.propertychange": {
				FlowDeveloperName: "DeltaFullRecordFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeLatestBySourceFullRecordPreservePayload,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
	}, events, &fakeSyncState{}, batch, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := strings.Join(events.processed, ","); got != "prop-old,prop-latest" {
		t.Fatalf("processed = %v, want [prop-old prop-latest]", events.processed)
	}
	if len(flow.capturedArgs) != 1 {
		t.Fatalf("captured flow calls = %d, want 1", len(flow.capturedArgs))
	}
	payload, ok := flow.capturedArgs[0]["payload"].(map[string]any)
	if !ok {
		t.Fatalf("payload = %#v, want object", flow.capturedArgs[0]["payload"])
	}
	if got, want := payload["propertyName"], "lastname"; got != want {
		t.Fatalf("payload.propertyName = %#v, want %q", got, want)
	}
	coalescedPayloads, ok := flow.capturedArgs[0]["coalescedPayloads"].([]any)
	if !ok {
		t.Fatalf("coalescedPayloads = %#v, want []any", flow.capturedArgs[0]["coalescedPayloads"])
	}
	if len(coalescedPayloads) != 2 {
		t.Fatalf("coalescedPayloads len = %d, want 2: %#v", len(coalescedPayloads), coalescedPayloads)
	}
	first, ok := coalescedPayloads[0].(map[string]any)
	if !ok {
		t.Fatalf("coalescedPayloads[0] = %#v, want object", coalescedPayloads[0])
	}
	second, ok := coalescedPayloads[1].(map[string]any)
	if !ok {
		t.Fatalf("coalescedPayloads[1] = %#v, want object", coalescedPayloads[1])
	}
	if got, want := first["eventID"], "prop-old"; got != want {
		t.Fatalf("coalescedPayloads[0].eventID = %#v, want %q", got, want)
	}
	if got, want := first["eventKind"], "contact.propertychange"; got != want {
		t.Fatalf("coalescedPayloads[0].eventKind = %#v, want %q", got, want)
	}
	if got, want := first["namespace"], workerTestNS; got != want {
		t.Fatalf("coalescedPayloads[0].namespace = %#v, want %q", got, want)
	}
	if got, want := first["objectKind"], "contact"; got != want {
		t.Fatalf("coalescedPayloads[0].objectKind = %#v, want %q", got, want)
	}
	if got, want := first["sourceID"], "contact-123"; got != want {
		t.Fatalf("coalescedPayloads[0].sourceID = %#v, want %q", got, want)
	}
	if got, want := first["idempotencyKey"], "prop-old"; got != want {
		t.Fatalf("coalescedPayloads[0].idempotencyKey = %#v, want %q", got, want)
	}
	firstPayload, ok := first["payload"].(map[string]any)
	if !ok {
		t.Fatalf("coalescedPayloads[0].payload = %#v, want object", first["payload"])
	}
	if got, want := firstPayload["propertyName"], "firstname"; got != want {
		t.Fatalf("coalescedPayloads[0].payload.propertyName = %#v, want %q", got, want)
	}
	if got, want := second["eventID"], "prop-latest"; got != want {
		t.Fatalf("coalescedPayloads[1].eventID = %#v, want %q", got, want)
	}
	secondPayload, ok := second["payload"].(map[string]any)
	if !ok {
		t.Fatalf("coalescedPayloads[1].payload = %#v, want object", second["payload"])
	}
	if got, want := secondPayload["propertyName"], "lastname"; got != want {
		t.Fatalf("coalescedPayloads[1].payload.propertyName = %#v, want %q", got, want)
	}
	record, ok := flow.capturedArgs[0]["record"].(map[string]any)
	if !ok {
		t.Fatalf("record input missing: %#v", flow.capturedArgs[0])
	}
	if got, want := record["id"], "contact-123"; got != want {
		t.Fatalf("record.id = %#v, want %q", got, want)
	}
}

func TestWorker_SettledLatestBySourceModeKeepsFullRecordPayloadCoalescing(t *testing.T) {
	prep := preparedEvent{spec: connector_inbound_routing.RouteExecutionSpec{
		InvocationMode: connector_inbound_routing.InvocationModeLatestBySourceFullRecordPreservePayloadSettled,
	}}
	w := &Worker{}
	if !w.shouldCoalesceLatestBySource(prep) {
		t.Fatal("settled latest-by-source mode did not opt into source coalescing")
	}
	if !preservesCoalescedPayloads(prep.spec) {
		t.Fatal("settled latest-by-source mode did not preserve coalesced payloads")
	}
}

func TestWorker_LegacySettledProjectionPreservesEverySameDeliveryPayloadExactlyOnce(t *testing.T) {
	first := propertyChangeEvent("delivery-first", "firstname")
	latest := propertyChangeEvent("delivery-latest", "country_picklist__sfdc_")
	for sequence, event := range []*connector_events.ConnectorEvent{first, latest} {
		event.DeliveryId = "delivery-1"
		event.DeliverySequence = sequence
		event.SourceLaneKey = "source-lane-contact-123"
	}
	events := newFakeEvents(first)
	events.settledProjectionPayloads = map[string][]*connector_events.ConnectorEvent{
		first.ID: {first, latest},
	}
	events.coalescedSiblings = map[string]*connector_events.ConnectorEvent{first.ID: latest}
	flow := &capturingFlowInvoker{}
	batch := &returningBatchRead{result: map[string]json.RawMessage{
		"contact-123": json.RawMessage(`{"id":"contact-123","firstname":"Grace","country_picklist__sfdc_":"United States"}`),
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/contact.propertychange": {
				FlowDeveloperName: "DeltaFullRecordFlow",
				InvocationMode:    connector_inbound_routing.InvocationModeLatestBySourceFullRecordPreservePayloadSettled,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
	}, events, &fakeSyncState{}, batch, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if len(flow.capturedArgs) != 1 {
		t.Fatalf("captured flow calls = %d, want 1", len(flow.capturedArgs))
	}
	coalesced, ok := flow.capturedArgs[0]["coalescedPayloads"].([]any)
	if !ok {
		t.Fatalf("coalescedPayloads = %#v, want []any", flow.capturedArgs[0]["coalescedPayloads"])
	}
	if got, want := len(coalesced), 2; got != want {
		t.Fatalf("coalescedPayloads len = %d, want %d: %#v", got, want, coalesced)
	}
	for i, want := range []string{"firstname", "country_picklist__sfdc_"} {
		payload, ok := coalesced[i].(map[string]any)["payload"].(map[string]any)
		if !ok {
			t.Fatalf("coalescedPayloads[%d].payload = %#v, want object", i, coalesced[i])
		}
		if got := payload["propertyName"]; got != want {
			t.Fatalf("coalescedPayloads[%d].payload.propertyName = %#v, want %q", i, got, want)
		}
	}
}

func TestWorker_OutboundEchoProbeAnnotatesSingleEventPayload(t *testing.T) {
	payload, err := EncodeQueueEnvelope("contact", "contact-123", json.RawMessage(`{"propertyName":"firstname","propertyValue":"OrigFN"}`))
	if err != nil {
		t.Fatal(err)
	}
	ev := &connector_events.ConnectorEvent{
		ID:              "prop-stale-firstname",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "contact.propertychange",
		IdempotencyKey:  "prop-stale-firstname",
		Status:          "processing",
		ClaimToken:      "token-prop-stale-firstname",
		Payload:         json.RawMessage(payload),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
		CreatedDate:     time.Date(2026, 6, 27, 2, 51, 57, 0, time.UTC),
	}
	events := newFakeEvents(ev)
	flow := &capturingFlowInvoker{}
	probe := &fakeOutboundEchoProbe{evidence: OutboundEchoEvidence{
		Found:         true,
		Mismatch:      true,
		AttemptID:     "attempt-firstname",
		Property:      "firstname",
		OutboundValue: "LOCALEDIT",
	}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/contact.propertychange": {
				FlowDeveloperName: "HubSpot_Contact_Lead_Upsert",
				InvocationMode:    connector_inbound_routing.InvocationModeLatestBySourceFullRecordPreservePayload,
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
	}, events, &fakeSyncState{}, &returningBatchRead{result: map[string]json.RawMessage{
		"contact-123": json.RawMessage(`{"id":"contact-123"}`),
	}}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	w.SetOutboundEchoProbe(probe)

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if len(probe.inputs) != 1 {
		t.Fatalf("probe calls = %d, want 1", len(probe.inputs))
	}
	if got, want := probe.inputs[0].Property, "firstname"; got != want {
		t.Fatalf("probe property = %q, want %q", got, want)
	}
	if got, want := probe.inputs[0].Value, "OrigFN"; got != want {
		t.Fatalf("probe value = %q, want %q", got, want)
	}
	payloadMap, ok := flow.capturedArgs[0]["payload"].(map[string]any)
	if !ok {
		t.Fatalf("payload = %#v, want object", flow.capturedArgs[0]["payload"])
	}
	if got := payloadMap["nexusRecentOutboundPropertyEcho"]; got != true {
		t.Fatalf("nexusRecentOutboundPropertyEcho = %#v, want true", got)
	}
	if got, want := payloadMap["nexusRecentOutboundPropertyValue"], "LOCALEDIT"; got != want {
		t.Fatalf("nexusRecentOutboundPropertyValue = %#v, want %q", got, want)
	}
	if got, want := payloadMap["nexusRecentOutboundPropertyAttemptId"], "attempt-firstname"; got != want {
		t.Fatalf("nexusRecentOutboundPropertyAttemptId = %#v, want %q", got, want)
	}
}

func TestWorker_LatestBySourceCoalescedSiblingFilterSkipReturnsSuccess(t *testing.T) {
	old := propertyChangeEvent("prop-old", "email")
	latest := propertyChangeEvent("prop-latest", "hs_analytics_source")
	events := newFakeEvents(old)
	events.coalescedSiblings = map[string]*connector_events.ConnectorEvent{"prop-old": latest}
	syncs := &fakeSyncState{}
	batch := &countingBatchRead{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}
	resolver := &stubRouteResolver{
		specs: map[string]connector_inbound_routing.RouteExecutionSpec{
			workerTestNS + "/contact.propertychange": {
				FlowDeveloperName: "ContactPropertyChange",
				InvocationMode:    connector_inbound_routing.InvocationModeLatestBySource,
				FlowExecutionFilter: &connector_inbound_routing.PayloadValueFilter{
					Path:      ".propertyName",
					EqualsAny: []string{"email"},
				},
			},
		},
	}
	w, err := newWorkerForTest(WorkerConfig{
		Namespace:     workerTestNS,
		PackInstallID: "pack-test",
		WorkerID:      "test-worker",
		PollInterval:  time.Millisecond,
		MaxClaims:     1,
	}, events, syncs, batch, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("claimed=false, want true")
	}
	if got := strings.Join(events.processed, ","); got != "prop-old" {
		t.Fatalf("processed = %v, want [prop-old]", events.processed)
	}
	if got := strings.Join(events.skipped, ","); got != "prop-latest" {
		t.Fatalf("skipped = %v, want [prop-latest]", events.skipped)
	}
	if batch.calls != 0 || flow.calls != 0 {
		t.Fatalf("batch calls=%d flow calls=%d, want both 0", batch.calls, flow.calls)
	}
}

func (c *capturingFlowInvoker) SystemUser(_ context.Context) *models.UserSession {
	return &models.UserSession{ID: "system-user"}
}

// capturingBatchRead is a BatchReadClient that records calls and delegates to fn.
type capturingBatchRead struct {
	fn func(ns, objectKind string, sourceIDs []string)
}

func (c *capturingBatchRead) BatchRead(_ context.Context, ns, objectKind string, sourceIDs []string) (map[string]json.RawMessage, error) {
	if c.fn != nil {
		c.fn(ns, objectKind, sourceIDs)
	}
	return map[string]json.RawMessage{}, nil
}

// errorBatchRead is a BatchReadClient that always returns the given error.
type errorBatchRead struct {
	err error
}

func (e *errorBatchRead) BatchRead(_ context.Context, _, _ string, _ []string) (map[string]json.RawMessage, error) {
	return nil, e.err
}

// TestWorker_BatchReadError_FailsEvent verifies that a non-nil error from
// BatchReadClient.BatchRead causes the worker to call MarkFailed +
// SyncState.RecordError, and NOT MarkProcessed.
func TestWorker_BatchReadError_FailsEvent(t *testing.T) {
	ev := noopEvent("ev-batchread-err", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}
	batchReadErr := errors.New("batch read engine unavailable")

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
		},
		fakes,
		syncstate,
		&errorBatchRead{err: batchReadErr},
		flow,
		resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true even on BatchRead error")
	}
	if err == nil {
		t.Fatal("expected non-nil error on BatchRead failure")
	}
	if !errors.Is(err, batchReadErr) {
		t.Errorf("want batchReadErr in chain, got %v", err)
	}
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed call, got %d: %v", len(fakes.failed), fakes.failed)
	}
	if len(syncstate.errors) != 1 {
		t.Errorf("want 1 SyncState.RecordError call, got %d: %v", len(syncstate.errors), syncstate.errors)
	}
	if len(fakes.processed) != 0 {
		t.Error("unexpected MarkProcessed call on BatchRead failure")
	}
	if flow.calls != 0 {
		t.Errorf("FlowInvoker should NOT be called on BatchRead failure, got %d calls", flow.calls)
	}
}

// returningBatchRead is a BatchReadClient that always returns the given map.
type returningBatchRead struct {
	result map[string]json.RawMessage
}

func (r *returningBatchRead) BatchRead(_ context.Context, _, _ string, _ []string) (map[string]json.RawMessage, error) {
	return r.result, nil
}

// TestWorker_BuildFlowInputs_PayloadIsMap verifies that buildFlowInputs returns
// inputs["payload"] as map[string]any (not a JSON string), so that Flow formula
// references like {!payload.objectId} can navigate into nested fields.
func TestWorker_BuildFlowInputs_PayloadIsMap(t *testing.T) {
	ev := noopEvent("ev-map-1", "noop.created")
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	capturer := &capturingFlowInvoker{}

	w := buildTestWorker(t, fakes, syncstate, capturer)

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if len(capturer.capturedArgs) != 1 {
		t.Fatalf("want 1 ExecuteFlow call, got %d", len(capturer.capturedArgs))
	}
	inputs := capturer.capturedArgs[0]
	payloadVal, ok := inputs["payload"]
	if !ok {
		t.Fatal("inputs missing 'payload' key")
	}
	if _, isMap := payloadVal.(map[string]any); !isMap {
		t.Errorf("inputs[\"payload\"] type = %T, want map[string]any", payloadVal)
	}
}

// TestWorker_PayloadUnmarshalFails_TerminalMarkFailed verifies that when
// ev.Payload is malformed JSON, buildFlowInputs returns an error and the
// worker calls MarkFailed without a SyncState write (terminal class).
func TestWorker_PayloadUnmarshalFails_TerminalMarkFailed(t *testing.T) {
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-badpayload",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-badpayload",
		Status:          "processing",
		ClaimToken:      "token-ev-badpayload",
		// Valid __objectKind and __sourceID so we get past extraction,
		// but the whole payload is not a valid JSON object for Unmarshal.
		Payload: json.RawMessage(`not-json`),
	}
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	w := buildTestWorker(t, fakes, syncstate, flow)

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	// DecodeQueueEnvelope will fail first on malformed JSON,
	// before buildFlowInputs is reached — both are terminal class.
	if err == nil {
		t.Fatal("expected error for malformed payload")
	}
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed call, got %d", len(fakes.failed))
	}
	// Terminal class: NO SyncState write.
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState errors for terminal class, got %d: %v",
			len(syncstate.errors), syncstate.errors)
	}
	if len(fakes.processed) != 0 {
		t.Error("unexpected MarkProcessed call")
	}
}

// TestWorker_BuildFlowInputs_RecordIsMap verifies that when BatchRead returns
// a record for the sourceID, inputs["record"] is map[string]any (not a JSON
// string blob), so Flow formulas can navigate into record fields.
func TestWorker_BuildFlowInputs_RecordIsMap(t *testing.T) {
	ev := noopEvent("ev-record-1", "noop.created")
	sourceID := "src-ev-record-1"
	recordJSON := json.RawMessage(`{"Name":"Acme","Amount":1000}`)

	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	capturer := &capturingFlowInvoker{}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
		},
		fakes,
		syncstate,
		&returningBatchRead{result: map[string]json.RawMessage{sourceID: recordJSON}},
		capturer,
		resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if len(capturer.capturedArgs) != 1 {
		t.Fatalf("want 1 ExecuteFlow call, got %d", len(capturer.capturedArgs))
	}
	inputs := capturer.capturedArgs[0]
	recordVal, ok := inputs["record"]
	if !ok {
		t.Fatal("inputs missing 'record' key")
	}
	if _, isMap := recordVal.(map[string]any); !isMap {
		t.Errorf("inputs[\"record\"] type = %T, want map[string]any", recordVal)
	}
}

// TestNoopRouteResolver_AlwaysErrRouteNotDeclared verifies that
// connector_inbound_routing.NoopRouteResolver always returns ErrRouteNotDeclared
// regardless of inputs.
func TestNoopRouteResolver_AlwaysErrRouteNotDeclared(t *testing.T) {
	cases := []struct{ ns, kind string }{
		{"any-ns", "any.kind"},
		{"", ""},
		{"ns", ""},
	}
	var r connector_inbound_routing.NoopRouteResolver
	for _, tc := range cases {
		_, flow, err := r.ResolveFlow(context.Background(), tc.ns, "pack-install-1", tc.kind)
		if !errors.Is(err, connector_inbound_routing.ErrRouteNotDeclared) {
			t.Errorf("ns=%q kind=%q: want ErrRouteNotDeclared, got %v", tc.ns, tc.kind, err)
		}
		if flow != "" {
			t.Errorf("ns=%q kind=%q: want empty flow, got %q", tc.ns, tc.kind, flow)
		}
	}
}

// TestWorker_NilResolver_ErrInvalidWorkerConfig verifies that the mandatory
// WorkItem constructor returns ErrInvalidWorkerConfig when resolver is nil.
func TestWorker_NilResolver_ErrInvalidWorkerConfig(t *testing.T) {
	_, err := NewWorkItemWorker(context.Background(),
		mandatoryWorkerTestConfig(),
		newCompleteWorkItemServiceFake(&fakeWorkItemClaims{}, &fakeWorkItemEventLoader{}, nil),
		&fakeSyncState{},
		fakeBatchRead{},
		&fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}},
		nil, // nil resolver
	)
	if !errors.Is(err, ErrInvalidWorkerConfig) {
		t.Errorf("want ErrInvalidWorkerConfig, got %v", err)
	}
}

// TestNewWorker_NilEvents_ErrInvalidWorkerConfig retains its historical name
// while verifying that the mandatory queue service fails closed when nil.
func TestNewWorker_NilEvents_ErrInvalidWorkerConfig(t *testing.T) {
	_, err := NewWorkItemWorker(context.Background(),
		mandatoryWorkerTestConfig(),
		nil, // nil WorkItem service
		&fakeSyncState{},
		fakeBatchRead{},
		&fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}},
		&stubRouteResolver{},
	)
	if !errors.Is(err, ErrInvalidWorkerConfig) {
		t.Errorf("want ErrInvalidWorkerConfig, got %v", err)
	}
}

// TestNewWorker_NilSyncState_ErrInvalidWorkerConfig verifies that the
// mandatory WorkItem constructor rejects a nil syncstate service.
func TestNewWorker_NilSyncState_ErrInvalidWorkerConfig(t *testing.T) {
	_, err := NewWorkItemWorker(context.Background(),
		mandatoryWorkerTestConfig(),
		newCompleteWorkItemServiceFake(&fakeWorkItemClaims{}, &fakeWorkItemEventLoader{}, nil),
		nil, // nil syncstate
		fakeBatchRead{},
		&fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}},
		&stubRouteResolver{},
	)
	if !errors.Is(err, ErrInvalidWorkerConfig) {
		t.Errorf("want ErrInvalidWorkerConfig, got %v", err)
	}
}

// TestNewWorker_NilFlow_ErrInvalidWorkerConfig verifies that the mandatory
// WorkItem constructor rejects a nil Flow invoker.
func TestNewWorker_NilFlow_ErrInvalidWorkerConfig(t *testing.T) {
	_, err := NewWorkItemWorker(context.Background(),
		mandatoryWorkerTestConfig(),
		newCompleteWorkItemServiceFake(&fakeWorkItemClaims{}, &fakeWorkItemEventLoader{}, nil),
		&fakeSyncState{},
		fakeBatchRead{},
		nil, // nil flow
		&stubRouteResolver{},
	)
	if !errors.Is(err, ErrInvalidWorkerConfig) {
		t.Errorf("want ErrInvalidWorkerConfig, got %v", err)
	}
}

// TestNewWorker_NilBatchRead_ErrInvalidWorkerConfig verifies that the production
// WorkItem constructor fails closed when INT-10 BatchRead wiring is absent.
func TestNewWorker_NilBatchRead_ErrInvalidWorkerConfig(t *testing.T) {
	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}
	_, err := NewWorkItemWorker(context.Background(),
		mandatoryWorkerTestConfig(),
		newCompleteWorkItemServiceFake(&fakeWorkItemClaims{}, &fakeWorkItemEventLoader{}, nil),
		&fakeSyncState{},
		nil, // nil batchRead — must now be rejected
		&fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}},
		resolver,
	)
	if !errors.Is(err, ErrInvalidWorkerConfig) {
		t.Errorf("want ErrInvalidWorkerConfig for nil batchRead, got %v", err)
	}
}

// TestNewWorkerForTest_NilBatchRead_DefaultsToNoop retains its historical name
// while verifying the mandatory test constructor's nil-BatchRead convenience.
func TestNewWorkerForTest_NilBatchRead_DefaultsToNoop(t *testing.T) {
	now := time.Now().UTC()
	claims := &fakeWorkItemClaims{work: &connector_workqueue.WorkItem{
		WorkID: "work-noop-br", EventID: "ev-noop-br",
		InstalledPackID: "pack-test", WorkType: connector_workqueue.WorkTypeConnectorEvent,
		State: connector_workqueue.WorkStateProcessing, LeaseToken: "lease-noop-br", LastModifiedDate: now,
	}}
	loader := &fakeWorkItemEventLoader{event: noopEvent("ev-noop-br", "noop.created")}
	service := newCompleteWorkItemServiceFake(claims, loader, nil)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}
	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	cfg := mandatoryWorkerTestConfig()
	cfg.PollInterval = time.Millisecond
	w, err := NewWorkItemWorkerForTest(context.Background(),
		cfg,
		service,
		syncstate,
		nil, // nil batchRead → defaults to noopBatchReadClient
		flow,
		resolver,
	)
	if err != nil {
		t.Fatalf("NewWorkItemWorkerForTest with nil batchRead: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("processOne: %v", err)
	}
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if got := claims.completeCalls; len(got) != 1 || got[0] != connector_workqueue.WorkStateProcessed {
		t.Errorf("WorkItem completions = %v, want [processed]", got)
	}
}

// incarnationScopedStubResolver is a stubRouteResolver that also checks
// packInstallID: only events whose packInstallID equals activePackInstallID
// are resolved; all others return ErrRouteNotDeclared.
type incarnationScopedStubResolver struct {
	routes              map[string]string // key: "namespace/eventKind"
	activePackInstallID string
}

func (s *incarnationScopedStubResolver) ResolveFlow(_ context.Context, ns, packInstallID, eventKind string) (int64, string, error) {
	if packInstallID != s.activePackInstallID {
		return 0, "", connector_inbound_routing.ErrRouteNotDeclared
	}
	if flow, ok := s.routes[ns+"/"+eventKind]; ok {
		return 1, flow, nil
	}
	return 0, "", connector_inbound_routing.ErrRouteNotDeclared
}

func (s *incarnationScopedStubResolver) ResolveFlowByVersion(_ context.Context, installedPackID, eventKind string, _ int64) (string, error) {
	if installedPackID != s.activePackInstallID {
		return "", connector_inbound_routing.ErrRouteNotDeclared
	}
	for key, flow := range s.routes {
		if len(key) >= len(eventKind) && key[len(key)-len(eventKind):] == eventKind {
			return flow, nil
		}
	}
	return "", connector_inbound_routing.ErrRouteNotDeclared
}

func (s *incarnationScopedStubResolver) ResolveExecutionSpecByVersion(_ context.Context, installedPackID, eventKind string, version int64) (connector_inbound_routing.RouteExecutionSpec, error) {
	flow, err := s.ResolveFlowByVersion(context.Background(), installedPackID, eventKind, version)
	if err != nil {
		return connector_inbound_routing.RouteExecutionSpec{}, err
	}
	return connector_inbound_routing.NormalizeRouteExecutionSpec(connector_inbound_routing.RouteExecutionSpec{FlowDeveloperName: flow})
}

// TestProcessEvent_StaleIncarnationEvent_FailTerminal verifies that when
// a claimed event's InstalledPackID no longer matches the active incarnation,
// the worker calls failTerminal (MarkFailed, NO SyncState write) — because
// ErrRouteNotDeclared from ResolveFlowByVersion is treated as an unrecoverable
// terminal condition, not a transient object error.
func TestProcessEvent_StaleIncarnationEvent_FailTerminal(t *testing.T) {
	// Stale event: queued under old incarnation "pack-old".
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-stale-1",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-old", // old incarnation
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-stale-1",
		Status:          "processing",
		ClaimToken:      "token-stale-1",
		Payload:         noopPayload("noop_object", "src-stale-1"),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
	}
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	// Resolver only accepts the NEW active incarnation "pack-new".
	resolver := &incarnationScopedStubResolver{
		routes: map[string]string{
			workerTestNS + "/noop.created": "NoopFlow__c",
		},
		activePackInstallID: "pack-new",
	}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-new", // worker is for the new incarnation
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
		},
		fakes,
		syncstate,
		fakeBatchRead{},
		flow,
		resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true: event was claimed from queue")
	}
	if err == nil {
		t.Fatal("expected error: stale-incarnation event must fail")
	}
	if !errors.Is(err, connector_inbound_routing.ErrRouteNotDeclared) {
		t.Errorf("want ErrRouteNotDeclared in error chain, got %v", err)
	}
	// Must call MarkFailed (queue row failed, not silently dropped).
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed call (failTerminal), got %d", len(fakes.failed))
	}
	// failTerminal: NO SyncState write (ErrRouteNotDeclared is unrecoverable).
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState.RecordError calls (failTerminal for stale incarnation), got %d", len(syncstate.errors))
	}
	// Flow must NOT be invoked.
	if flow.calls != 0 {
		t.Errorf("FlowInvoker must not be called for stale-incarnation event, got %d calls", flow.calls)
	}
	if len(fakes.processed) != 0 {
		t.Error("unexpected MarkProcessed call for stale-incarnation event")
	}
}

// ── RouteVersion three-state tests ────────────────────────────────────────────

// TestProcessOne_RouteVersionNULL_FailsTerminal verifies that a pre-R22 queue
// row (RouteVersion NULL / Valid=false) is rejected with failTerminal — the
// worker no longer carries a live-route fallback. Pre-R22 rows are handled by
// the migration runbook (docs/runbooks/connector-inbound-pre-r22-migration.md).
func TestProcessOne_RouteVersionNULL_FailsTerminal(t *testing.T) {
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-null-1",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-null-1",
		Status:          "processing",
		ClaimToken:      "token-null-1",
		Payload:         noopPayload("noop_object", "src-null-1"),
		RouteVersion:    sql.NullInt64{Valid: false}, // legacy NULL
	}
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
			MaxClaims:     1,
		},
		fakes, syncstate, fakeBatchRead{}, flow, resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected error: RouteVersion NULL must failTerminal")
	}
	// failTerminal: MarkFailed called, NO SyncState write, NO flow call.
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed, got %d", len(fakes.failed))
	}
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState errors (failTerminal path), got %d", len(syncstate.errors))
	}
	if flow.calls != 0 {
		t.Errorf("flow must not be invoked for NULL RouteVersion, got %d calls", flow.calls)
	}
	if len(fakes.processed) != 0 {
		t.Error("unexpected MarkProcessed call")
	}
}

// TestProcessOne_RouteVersion0_FailTerminal verifies that a RouteVersion=0 event
// (no route was declared at enqueue time) causes failTerminal (MarkFailed, no
// SyncState write, no flow invocation) and returns ErrRouteNotDeclared.
func TestProcessOne_RouteVersion0_FailTerminal(t *testing.T) {
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-zero-1",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-zero-1",
		Status:          "processing",
		ClaimToken:      "token-zero-1",
		Payload:         noopPayload("noop_object", "src-zero-1"),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 0}, // sentinel: no route at enqueue
	}
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
		},
		fakes, syncstate, fakeBatchRead{}, flow, resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected error: RouteVersion=0 must fail")
	}
	// RouteVersion=0 is a "no-route sentinel" handled by the RouteVersion guard
	// before ResolveFlowByVersion is called — error message identifies the cause.
	errMsg := err.Error()
	if !strings.Contains(errMsg, "RouteVersion missing/zero") && !strings.Contains(errMsg, "no immutable RouteVersion snapshot") {
		t.Errorf("want RouteVersion-missing error message, got %v", err)
	}
	// failTerminal: MarkFailed called, NO SyncState write, NO flow call.
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed, got %d", len(fakes.failed))
	}
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState.RecordError (failTerminal path), got %d", len(syncstate.errors))
	}
	if flow.calls != 0 {
		t.Errorf("flow must not be invoked, got %d calls", flow.calls)
	}
}

// TestProcessOne_RouteVersionN_CallsResolveFlowByVersion verifies that an event
// with RouteVersion >= 1 calls ResolveFlowByVersion (not ResolveFlow) and
// successfully routes when the versioned route exists.
func TestProcessOne_RouteVersionN_CallsResolveFlowByVersion(t *testing.T) {
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-versioned-1",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-versioned-1",
		Status:          "processing",
		ClaimToken:      "token-versioned-1",
		Payload:         noopPayload("noop_object", "src-versioned-1"),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 5},
	}
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
			MaxClaims:     1,
		},
		fakes, syncstate, fakeBatchRead{}, flow, resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if len(fakes.processed) != 1 {
		t.Errorf("want 1 MarkProcessed, got %d", len(fakes.processed))
	}
	if flow.calls != 1 {
		t.Errorf("want 1 FlowInvoker call, got %d", flow.calls)
	}
}

// TestProcessOne_RouteVersionSwept_FailTerminal verifies that when
// ResolveFlowByVersion returns ErrRouteVersionSwept (the snapshotted route
// version was garbage-collected), the worker calls failTerminal (not retry).
func TestProcessOne_RouteVersionSwept_FailTerminal(t *testing.T) {
	const sweptVersion = int64(3)
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-swept-1",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-swept-1",
		Status:          "processing",
		ClaimToken:      "token-swept-1",
		Payload:         noopPayload("noop_object", "src-swept-1"),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: sweptVersion},
	}
	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	resolver := &stubRouteResolver{
		routes: map[string]string{
			workerTestNS + "/noop.created": "NoopFlow__c",
		},
		sweptVersions: map[int64]bool{sweptVersion: true},
	}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
		},
		fakes, syncstate, fakeBatchRead{}, flow, resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected error: swept route version must fail")
	}
	if !errors.Is(err, connector_inbound_routing.ErrRouteVersionSwept) {
		t.Errorf("want ErrRouteVersionSwept, got %v", err)
	}
	// failTerminal: MarkFailed called, NO SyncState write, NO flow call.
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed, got %d", len(fakes.failed))
	}
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState.RecordError (failTerminal path), got %d", len(syncstate.errors))
	}
	if flow.calls != 0 {
		t.Errorf("flow must not be invoked for swept version, got %d calls", flow.calls)
	}
}

// ── Legacy-shape terminal tests ───────────────────────────────────────────────

// TestProcessOne_LegacyPayloadShape_FailsTerminal verifies that a pre-R22 queue
// row (raw user payload with __objectKind + __sourceID reserved keys, no
// QueueEnvelope wrapper) fails terminal at decode — the worker requires
// QueueEnvelope as the only runtime read path. Pre-R22 rows are handled by the
// migration runbook (docs/runbooks/connector-inbound-pre-r22-migration.md).
func TestProcessOne_LegacyPayloadShape_FailsTerminal(t *testing.T) {
	legacyRaw := json.RawMessage(`{"__objectKind":"noop_object","__sourceID":"src-legacy-1","data":{"name":"Alice"}}`)
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-legacy-1",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-legacy-1",
		Status:          "processing",
		ClaimToken:      "token-legacy-1",
		Payload:         legacyRaw,
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
	}

	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
		},
		fakes,
		syncstate,
		fakeBatchRead{},
		flow,
		resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	claimed, err := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if err == nil {
		t.Fatal("expected error: legacy payload shape must fail terminal at DecodeQueueEnvelope")
	}
	// failTerminal: MarkFailed called, NO SyncState write, NO flow call.
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed, got %d: %v", len(fakes.failed), fakes.failed)
	}
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState errors (failTerminal), got %d: %v", len(syncstate.errors), syncstate.errors)
	}
	if flow.calls != 0 {
		t.Errorf("flow must not be invoked, got %d calls", flow.calls)
	}
	if len(fakes.processed) != 0 {
		t.Error("unexpected MarkProcessed call")
	}
}

// ── ErrBatchReadRecordShape: null record from BatchRead ───────────────────────

// TestProcessOne_BatchReadReturnsNullRecord_RetriableNotTerminal verifies that
// when BatchRead returns a present-but-null entry for sourceID (i.e. the map
// contains sourceID -> []byte(`null`)), the worker fails retriably via
// failWithSyncState — NOT terminally. An absent entry means "not fetchable";
// a null entry is an INT-10 boundary fault, not a parser payload error.
func TestProcessOne_BatchReadReturnsNullRecord_RetriableNotTerminal(t *testing.T) {
	const sourceID = "src-nullrecord-1"
	userPayload := json.RawMessage(`{"id":"` + sourceID + `"}`)
	envBytes, err := EncodeQueueEnvelope("noop_object", sourceID, userPayload)
	if err != nil {
		t.Fatalf("EncodeQueueEnvelope: %v", err)
	}
	ev := &connector_events.ConnectorEvent{
		ID:              "ev-nullrecord-1",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "ev-nullrecord-1",
		Status:          "processing",
		ClaimToken:      "token-nullrecord-1",
		Payload:         json.RawMessage(envBytes),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
	}

	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	// BatchRead returns JSON null for this sourceID — present key, null value.
	batchRead := &returningBatchRead{
		result: map[string]json.RawMessage{
			sourceID: []byte(`null`),
		},
	}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, newErr := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
		},
		fakes,
		syncstate,
		batchRead,
		flow,
		resolver,
	)
	if newErr != nil {
		t.Fatalf("NewWorker: %v", newErr)
	}

	claimed, procErr := w.processOne(context.Background())
	if !claimed {
		t.Fatal("expected claimed=true")
	}
	if procErr == nil {
		t.Fatal("expected non-nil error: null record is INT-10 boundary fault, must fail retriably")
	}

	// failWithSyncState path: SyncState MUST be written (retriable fault).
	if len(syncstate.errors) == 0 {
		t.Errorf("want >=1 SyncState error (null record is retriable INT-10 fault), got 0")
	}
	// MarkFailed MUST be called.
	if len(fakes.failed) != 1 {
		t.Errorf("want 1 MarkFailed, got %d: %v", len(fakes.failed), fakes.failed)
	}
	// FlowInvoker MUST NOT be called — null record must not reach Flow.
	if flow.calls != 0 {
		t.Errorf("flow must not be invoked on null record, got %d calls", flow.calls)
	}
	// MarkProcessed MUST NOT be called.
	if len(fakes.processed) != 0 {
		t.Error("unexpected MarkProcessed call")
	}
}

// ── ErrBatchReadRecordShape: retriable batch-read record failures ──────────────

// TestProcessOne_BatchReadReturnsNonObjectRecord_RetriableNotTerminal verifies
// that when BatchRead returns bytes that are not a valid JSON object for a
// sourceID, the worker:
//   - calls failWithSyncState (SyncState written + MarkFailed) — NOT failTerminal
//   - does NOT call FlowInvoker.ExecuteFlow
//   - returns claimed=true (queue row was claimed and failed)
//
// This is the INT-10 boundary-fault path. The dispatcher validated the parser
// payload shape before enqueue (validateParserPayload), so the original
// delivery is not to blame — the batch-read boundary returned malformed data.
// The failure must be retriable so that a transient INT-10 issue does not
// permanently lose the delivery.
func TestProcessOne_BatchReadReturnsNonObjectRecord_RetriableNotTerminal(t *testing.T) {
	for _, tc := range []struct {
		name       string
		recordJSON []byte
	}{
		{"json_string", []byte(`"just a string"`)},
		{"json_number", []byte(`123`)},
		{"json_array", []byte(`[{"id":"1"}]`)},
		{"invalid_json", []byte(`not json at all`)},
		{"json_bool", []byte(`true`)},
	} {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			// Build a standard event with a known sourceID so returningBatchRead
			// can return bad record JSON for it.
			const sourceID = "src-batchbad-1"
			userPayload := json.RawMessage(`{"id":"` + sourceID + `"}`)
			envBytes, err := EncodeQueueEnvelope("noop_object", sourceID, userPayload)
			if err != nil {
				t.Fatalf("EncodeQueueEnvelope: %v", err)
			}
			ev := &connector_events.ConnectorEvent{
				ID:              "ev-batchbad-" + tc.name,
				Namespace:       workerTestNS,
				InstalledPackID: "pack-test",
				EventKind:       "noop.created",
				IdempotencyKey:  "ev-batchbad-" + tc.name,
				Status:          "processing",
				ClaimToken:      "token-batchbad-" + tc.name,
				Payload:         json.RawMessage(envBytes),
				RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
			}

			fakes := newFakeEvents(ev)
			syncstate := &fakeSyncState{}
			flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

			// BatchRead returns the malformed record for this sourceID.
			batchRead := &returningBatchRead{
				result: map[string]json.RawMessage{
					sourceID: tc.recordJSON,
				},
			}

			resolver := &stubRouteResolver{routes: map[string]string{
				workerTestNS + "/noop.created": "NoopFlow__c",
			}}

			w, newErr := newWorkerForTest(
				WorkerConfig{
					Namespace:     workerTestNS,
					PackInstallID: "pack-test",
					WorkerID:      "test-worker",
					PollInterval:  1 * time.Millisecond,
				},
				fakes,
				syncstate,
				batchRead,
				flow,
				resolver,
			)
			if newErr != nil {
				t.Fatalf("NewWorker: %v", newErr)
			}

			claimed, procErr := w.processOne(context.Background())
			if !claimed {
				t.Fatal("expected claimed=true")
			}
			if procErr == nil {
				t.Fatal("expected non-nil error for non-object batch-read record")
			}

			// failWithSyncState path: SyncState MUST be written (retriable fault).
			if len(syncstate.errors) == 0 {
				t.Errorf("[%s] want >=1 SyncState error (batch-read boundary fault is retriable), got 0", tc.name)
			}
			// MarkFailed MUST be called (queue row must fail).
			if len(fakes.failed) != 1 {
				t.Errorf("[%s] want 1 MarkFailed, got %d: %v", tc.name, len(fakes.failed), fakes.failed)
			}
			// FlowInvoker MUST NOT be called.
			if flow.calls != 0 {
				t.Errorf("[%s] flow must not be invoked on batch-read shape error, got %d calls", tc.name, flow.calls)
			}
			// MarkProcessed MUST NOT be called.
			if len(fakes.processed) != 0 {
				t.Errorf("[%s] unexpected MarkProcessed call", tc.name)
			}
		})
	}
}

// ── Batch drain tests ─────────────────────────────────────────────────────────

// recordingBatchRead records every BatchRead call for assertion.
type recordingBatchRead struct {
	mu    sync.Mutex
	calls []batchReadCallRecord
}

type batchReadCallRecord struct {
	ns, objectKind string
	sourceIDs      []string
}

func (r *recordingBatchRead) BatchRead(_ context.Context, ns, objectKind string, sourceIDs []string) (map[string]json.RawMessage, error) {
	r.mu.Lock()
	ids := make([]string, len(sourceIDs))
	copy(ids, sourceIDs)
	r.calls = append(r.calls, batchReadCallRecord{ns: ns, objectKind: objectKind, sourceIDs: ids})
	r.mu.Unlock()
	return map[string]json.RawMessage{}, nil
}

// dynamicBatchSuccessFlow returns a success result for every event in each
// batch-collection invocation while retaining the normal fake's call count.
type dynamicBatchSuccessFlow struct {
	fakeFlowInvoker
}

func (f *dynamicBatchSuccessFlow) ExecuteFlow(_ context.Context, _ string, inputs map[string]any, _ *models.UserSession) (*FlowResult, error) {
	f.mu.Lock()
	f.calls++
	f.mu.Unlock()
	items, _ := inputs["events"].([]any)
	results := make([]any, 0, len(items))
	for _, raw := range items {
		item, _ := raw.(map[string]any)
		eventID, _ := item["eventID"].(string)
		results = append(results, map[string]any{"eventID": eventID, "success": true})
	}
	return &FlowResult{
		IsSuccess: true,
		OutputValues: map[string]any{
			"eventResults": results,
		},
	}, nil
}

// TestBatchDrain_ProcessesInClaimOrder verifies that the mandatory WorkItem
// worker scheduler drains successive collection groups through its Run loop in
// claim order, and calls BatchRead exactly once per object kind.
func TestBatchDrain_ProcessesInClaimOrder(t *testing.T) {
	makeEvent := func(id, objectKind, sourceID string) *connector_events.ConnectorEvent {
		return &connector_events.ConnectorEvent{
			ID: id, Namespace: workerTestNS, InstalledPackID: "pack-test",
			EventKind: "noop.created", IdempotencyKey: id, Status: connector_workqueue.WorkStatePending,
			Payload:      noopPayload(objectKind, sourceID),
			RouteVersion: sql.NullInt64{Valid: true, Int64: 1},
		}
	}
	evA1 := makeEvent("ba-1", "kind_a", "src-ba-1")
	evA2 := makeEvent("ba-2", "kind_a", "src-ba-2")
	evB1 := makeEvent("bb-1", "kind_b", "src-bb-1")
	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-ba-1", evA1.ID, connector_workqueue.WorkTypeWorkflowActionApply, 0, "lease-ba-1"),
		liveWorkItem("work-ba-2", evA2.ID, connector_workqueue.WorkTypeWorkflowActionApply, 0, "lease-ba-2"),
		liveWorkItem("work-bb-1", evB1.ID, connector_workqueue.WorkTypeWorkflowActionApply, 0, "lease-bb-1"),
	}}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{events: map[string]*connector_events.ConnectorEvent{
		evA1.ID: evA1,
		evA2.ID: evA2,
		evB1.ID: evB1,
	}}, nil)
	flow := &dynamicBatchSuccessFlow{}
	recorder := &recordingBatchRead{}
	resolver := &stubRouteResolver{specs: map[string]connector_inbound_routing.RouteExecutionSpec{
		workerTestNS + "/noop.created": {
			FlowDeveloperName: "NoopBatchFlow",
			InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
			MaxBatchSize:      10,
		},
	}}
	cfg := liveWorkItemConfig()
	cfg.MaxClaims = 3
	cfg.BatchSize = 10
	w, err := NewWorkItemWorker(context.Background(), cfg, service, &fakeSyncState{}, recorder, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}
	// MaxClaims ends the real scheduler loop after the three terminal WorkItem
	// transitions. The first collection group contains kind_a, then the worker
	// must claim and drain the remaining kind_b group on its next scheduler turn.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := w.Run(ctx); err != nil {
		t.Fatalf("Run: %v", err)
	}
	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-ba-1,work-ba-2,work-bb-1" {
		t.Fatalf("completion order = %q, want claim order", got)
	}
	wantStates := []string{
		connector_workqueue.WorkStateProcessed,
		connector_workqueue.WorkStateProcessed,
		connector_workqueue.WorkStateProcessed,
	}
	if got := strings.Join(claims.completeCalls, ","); got != strings.Join(wantStates, ",") {
		t.Fatalf("completion states = %q, want %q", got, strings.Join(wantStates, ","))
	}
	if len(claims.failInputs) != 0 || claims.retryCalls != 0 {
		t.Fatalf("failure/retry calls = %v/%d, want none", claims.failInputs, claims.retryCalls)
	}

	// Exactly ONE BatchRead call per objectKind.
	recorder.mu.Lock()
	calls := append([]batchReadCallRecord{}, recorder.calls...)
	recorder.mu.Unlock()

	byKind := make(map[string]int)
	for _, c := range calls {
		byKind[c.objectKind]++
	}
	if byKind["kind_a"] != 1 {
		t.Errorf("want 1 BatchRead for kind_a, got %d", byKind["kind_a"])
	}
	if byKind["kind_b"] != 1 {
		t.Errorf("want 1 BatchRead for kind_b, got %d", byKind["kind_b"])
	}
	// Verify kind_a BatchRead includes both source IDs.
	for _, c := range calls {
		if c.objectKind == "kind_a" {
			if len(c.sourceIDs) != 2 {
				t.Errorf("BatchRead kind_a: want 2 sourceIDs, got %d: %v", len(c.sourceIDs), c.sourceIDs)
			}
		}
	}
	flow.mu.Lock()
	flowCalls := flow.calls
	flow.mu.Unlock()
	if flowCalls != 2 {
		t.Errorf("batch flow calls = %d, want 2", flowCalls)
	}
}

// TestBatchDrain_DecodeTerminalEventDoesNotPoisonBatch verifies that a single
// decode-terminal event in the batch is failed terminally (MarkFailed, no
// SyncState) without preventing the remaining events from being processed.
func TestBatchDrain_DecodeTerminalEventDoesNotPoisonBatch(t *testing.T) {
	bad := &connector_events.ConnectorEvent{
		ID: "bad-1", Namespace: workerTestNS, InstalledPackID: "pack-test",
		EventKind: "noop.created", IdempotencyKey: "bad-1", Status: "processing",
		ClaimToken:   "tk-bad-1",
		Payload:      json.RawMessage(`not-valid-json`), // decode fails → terminal
		RouteVersion: sql.NullInt64{Valid: true, Int64: 1},
	}
	good := noopEvent("good-1", "noop.created")

	fakes := newFakeEvents(bad, good)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	w := buildTestWorker(t, fakes, syncstate, flow)
	// Override BatchSize to claim both in one batch.
	w.cfg.BatchSize = 10
	w.cfg.MaxClaims = 2

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := w.Run(ctx); err != nil {
		t.Fatalf("Run: %v", err)
	}

	fakes.mu.Lock()
	processed := append([]string{}, fakes.processed...)
	failed := append([]failedCall{}, fakes.failed...)
	fakes.mu.Unlock()

	// good-1 must be processed.
	if len(processed) != 1 || processed[0] != "good-1" {
		t.Errorf("processed = %v, want [good-1]", processed)
	}
	// bad-1 must be failed terminally (MarkFailed called).
	if len(failed) != 1 || failed[0].eventID != "bad-1" {
		t.Errorf("failed = %v, want [bad-1]", failed)
	}
	// Terminal class: NO SyncState write for bad-1.
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState errors (terminal class), got %d: %v", len(syncstate.errors), syncstate.errors)
	}
}

// TestBatchDrain_RetriableFlowDoesNotBlockSiblings verifies that a retriable
// flow result for one event (MarkFailed called) does not block sibling events
// in the same batch from being processed.
func TestBatchDrain_RetriableFlowDoesNotBlockSiblings(t *testing.T) {
	evRetriable := noopEvent("retriable-1", "noop.created")
	evNormal := noopEvent("normal-1", "noop.created")

	fakes := newFakeEvents(evRetriable, evNormal)
	syncstate := &fakeSyncState{}

	callCount := 0
	var mu sync.Mutex
	// First call returns retriable; subsequent calls succeed.
	flowFn := func() (*FlowResult, error) {
		mu.Lock()
		n := callCount
		callCount++
		mu.Unlock()
		if n == 0 {
			return &FlowResult{
				IsSuccess:   true,
				IsRetriable: true,
				FirstError:  &FlowError{StatusCode: "DEP_MISSING", Message: "prerequisite missing"},
			}, nil
		}
		return &FlowResult{IsSuccess: true}, nil
	}

	invoker := &callbackFlowInvoker{fn: flowFn}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
			MaxClaims:     2,
			BatchSize:     10,
		},
		fakes, syncstate, fakeBatchRead{}, invoker, resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := w.Run(ctx); err != nil {
		t.Fatalf("Run: %v", err)
	}

	fakes.mu.Lock()
	processed := append([]string{}, fakes.processed...)
	failed := append([]failedCall{}, fakes.failed...)
	fakes.mu.Unlock()

	// normal-1 must be processed.
	if len(processed) != 1 || processed[0] != "normal-1" {
		t.Errorf("processed = %v, want [normal-1]", processed)
	}
	// retriable-1 must be failed (re-queued via MarkFailed).
	if len(failed) != 1 || failed[0].eventID != "retriable-1" {
		t.Errorf("failed = %v, want [retriable-1]", failed)
	}
}

// TestBatchDrain_MaxClaimsHonored verifies that BatchSize > MaxClaims still
// only processes MaxClaims events total.
func TestBatchDrain_MaxClaimsHonored(t *testing.T) {
	events := make([]*connector_events.ConnectorEvent, 5)
	for i := range events {
		events[i] = noopEvent(fmt.Sprintf("mc-batch-%d", i), "noop.created")
	}
	fakes := newFakeEvents(events...)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
			MaxClaims:     3, // must stop after 3 even though BatchSize=10
			BatchSize:     10,
		},
		fakes, syncstate, fakeBatchRead{}, flow, resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := w.Run(ctx); err != nil {
		t.Fatalf("Run: %v", err)
	}

	fakes.mu.Lock()
	processed := append([]string{}, fakes.processed...)
	fakes.mu.Unlock()

	if len(processed) != 3 {
		t.Errorf("want 3 processed (MaxClaims), got %d: %v", len(processed), processed)
	}
}

// TestBatchDrain_EmptyClaimBacksOff verifies that an empty batch causes the
// worker to sleep PollInterval before trying again, rather than tight-looping.
func TestBatchDrain_EmptyClaimBacksOff(t *testing.T) {
	// Queue starts empty; after the first back-off we cancel.
	fakes := newFakeEvents() // empty
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  50 * time.Millisecond,
		},
		fakes, syncstate, fakeBatchRead{}, flow, resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()
	start := time.Now()
	w.Run(ctx) // nolint: errcheck — ctx.Err() expected
	elapsed := time.Since(start)

	// The worker must have backed off at least once (PollInterval=50ms).
	// It should NOT return immediately.
	if elapsed < 40*time.Millisecond {
		t.Errorf("worker returned too fast (%v), expected at least one PollInterval back-off", elapsed)
	}
}

// callbackFlowInvoker is a FlowInvoker that delegates to a callback.
type callbackFlowInvoker struct {
	fn func() (*FlowResult, error)
}

func (c *callbackFlowInvoker) ExecuteFlow(_ context.Context, _ string, _ map[string]any, _ *models.UserSession) (*FlowResult, error) {
	return c.fn()
}

func (c *callbackFlowInvoker) ExecuteFlowVersion(_ context.Context, _, _ string, _ map[string]any, _ *models.UserSession) (*FlowResult, error) {
	return c.fn()
}

func (c *callbackFlowInvoker) SystemUser(_ context.Context) *models.UserSession {
	return &models.UserSession{ID: "system-user"}
}

// perKindErrBatchRead is a BatchReadClient that returns a configured error for
// specific objectKinds and an empty success map for all others.
type perKindErrBatchRead struct {
	errs map[string]error // objectKind → error to return
}

func (p *perKindErrBatchRead) BatchRead(_ context.Context, _, objectKind string, _ []string) (map[string]json.RawMessage, error) {
	if err, ok := p.errs[objectKind]; ok {
		return nil, err
	}
	return map[string]json.RawMessage{}, nil
}

// TestBatchDrain_BatchReadErrorIsolatedPerObjectKind verifies that a BatchRead
// failure for one objectKind fails only that kind's events (retriably via
// failWithSyncState → MarkFailed) while events of other objectKinds that
// succeed their BatchRead are still processed normally — i.e. one kind's
// batch-read failure does not poison the whole batch.
func TestBatchDrain_BatchReadErrorIsolatedPerObjectKind(t *testing.T) {
	// Three events: two of kind_a (will fail BatchRead) and one of kind_b
	// (will succeed).  Claim order: evA1, evB1, evA2.
	evA1 := &connector_events.ConnectorEvent{
		ID: "iso-a1", Namespace: workerTestNS, InstalledPackID: "pack-test",
		EventKind: "noop.created", IdempotencyKey: "iso-a1", Status: "processing",
		ClaimToken:   "tk-iso-a1",
		Payload:      noopPayload("kind_a", "src-iso-a1"),
		RouteVersion: sql.NullInt64{Valid: true, Int64: 1},
	}
	evB1 := &connector_events.ConnectorEvent{
		ID: "iso-b1", Namespace: workerTestNS, InstalledPackID: "pack-test",
		EventKind: "noop.created", IdempotencyKey: "iso-b1", Status: "processing",
		ClaimToken:   "tk-iso-b1",
		Payload:      noopPayload("kind_b", "src-iso-b1"),
		RouteVersion: sql.NullInt64{Valid: true, Int64: 1},
	}
	evA2 := &connector_events.ConnectorEvent{
		ID: "iso-a2", Namespace: workerTestNS, InstalledPackID: "pack-test",
		EventKind: "noop.created", IdempotencyKey: "iso-a2", Status: "processing",
		ClaimToken:   "tk-iso-a2",
		Payload:      noopPayload("kind_a", "src-iso-a2"),
		RouteVersion: sql.NullInt64{Valid: true, Int64: 1},
	}

	fakes := newFakeEvents(evA1, evB1, evA2)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	batchRead := &perKindErrBatchRead{
		errs: map[string]error{
			"kind_a": errors.New("upstream batch-read timeout"),
		},
	}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
			MaxClaims:     3,
			BatchSize:     10,
		},
		fakes, syncstate, batchRead, flow, resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := w.Run(ctx); err != nil {
		t.Fatalf("Run: %v", err)
	}

	fakes.mu.Lock()
	processed := append([]string{}, fakes.processed...)
	failed := append([]failedCall{}, fakes.failed...)
	fakes.mu.Unlock()

	// kind_b event must be processed successfully.
	if len(processed) != 1 || processed[0] != "iso-b1" {
		t.Errorf("processed = %v, want [iso-b1]", processed)
	}

	// Both kind_a events must be failed (retriably via failWithSyncState).
	if len(failed) != 2 {
		t.Errorf("failed = %v (len %d), want 2 entries (iso-a1 and iso-a2)", failed, len(failed))
	} else {
		failedIDs := map[string]bool{failed[0].eventID: true, failed[1].eventID: true}
		if !failedIDs["iso-a1"] || !failedIDs["iso-a2"] {
			t.Errorf("failed event IDs = %v, want {iso-a1, iso-a2}", failedIDs)
		}
	}

	// Retriable class: SyncState MUST be written for each kind_a failure.
	if len(syncstate.errors) != 2 {
		t.Errorf("want 2 SyncState errors (one per kind_a event), got %d: %v", len(syncstate.errors), syncstate.errors)
	}
}

// ── Candidate WorkItem CAS tests ──────────────────────────────────────────────

// TestBatchDrain_ClaimByIDLostRaceSkipsRow verifies that the central scheduler
// moves past a candidate whose WorkItem CAS is already lost. The lost candidate
// receives no terminal/retry mutation, while later candidates complete in
// order through the mandatory WorkItem path.
func TestBatchDrain_ClaimByIDLostRaceSkipsRow(t *testing.T) {
	evLost := livePendingEvent("b3-lost", "noop.created")
	evGood := livePendingEvent("b3-good", "noop.created")
	evGood2 := livePendingEvent("b3-good2", "noop.created")
	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-b3-lost", evLost.ID, connector_workqueue.WorkTypeConnectorEvent, 0, ""),
		liveWorkItem("work-b3-good", evGood.ID, connector_workqueue.WorkTypeConnectorEvent, 0, ""),
		liveWorkItem("work-b3-good2", evGood2.ID, connector_workqueue.WorkTypeConnectorEvent, 0, ""),
	}}
	service := &candidateClaimOutcomeWorkItemService{
		completeWorkItemServiceFake: newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{events: map[string]*connector_events.ConnectorEvent{
			evLost.ID:  evLost,
			evGood.ID:  evGood,
			evGood2.ID: evGood2,
		}}, nil),
		lost: map[string]bool{evLost.ID: true},
	}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}
	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}
	cfg := liveWorkItemConfig()
	cfg.MaxClaims = 2
	cfg.BatchSize = 1
	w, err := NewWorkItemWorker(context.Background(), cfg, service, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}
	scheduler := NewCentralClaimScheduler()
	w.UseCentralClaimScheduler(scheduler)
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	if err := w.RunUntilDrained(ctx, nil); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if got := strings.Join(service.ClaimAttempts(), ","); got != "b3-lost,b3-good,b3-good2" {
		t.Fatalf("central scheduler claim attempts = %q, want lost then later candidates", got)
	}
	if stats := scheduler.Stats(); stats.ClaimAttempts != 3 || stats.ClaimCASLost != 1 || stats.ClaimErrors != 0 || stats.ClaimSuccess != 2 {
		t.Fatalf("central scheduler stats = %+v, want 3 attempts with one CAS loss and two successes", stats)
	}
	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-b3-good,work-b3-good2" {
		t.Fatalf("completed WorkItems = %q, want good candidates in order", got)
	}
	if got := strings.Join(claims.completeCalls, ","); got != strings.Join([]string{
		connector_workqueue.WorkStateProcessed,
		connector_workqueue.WorkStateProcessed,
	}, ",") {
		t.Fatalf("terminal states = %q, want processed sequence for successful candidates", got)
	}
	if len(claims.failInputs) != 0 {
		t.Fatalf("FailWorkItem calls = %v, want none after CAS loss or success", claims.failInputs)
	}
	if claims.retryCalls != 0 || len(claims.retryWorkIDs) != 0 {
		t.Fatalf("RetryWorkItem calls/IDs = %d/%v, want 0/[]", claims.retryCalls, claims.retryWorkIDs)
	}
	flow.mu.Lock()
	flowCalls := flow.calls
	flow.mu.Unlock()
	if flowCalls != 2 {
		t.Errorf("FlowInvoker called %d times, want 2 (lost candidate must not execute)", flowCalls)
	}
}

// ── B1/B2 regression tests ────────────────────────────────────────────────────

// TestBatchDrain_DrainAfterClaimStillProcessesClaimedEvent verifies that when
// drain fires after a WorkItem claim has acquired its immutable input and begun
// flow execution, RunUntilDrained returns after that in-flight claim completes
// and does not admit a later candidate.
func TestBatchDrain_DrainAfterClaimStillProcessesClaimedEvent(t *testing.T) {
	ev1 := livePendingEvent("b1-ev-1", "noop.created")
	ev2 := livePendingEvent("b1-ev-2", "noop.created")
	drain := make(chan struct{})
	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-b1-ev-1", ev1.ID, connector_workqueue.WorkTypeConnectorEvent, 0, ""),
		liveWorkItem("work-b1-ev-2", ev2.ID, connector_workqueue.WorkTypeConnectorEvent, 0, ""),
	}}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{events: map[string]*connector_events.ConnectorEvent{
		ev1.ID: ev1,
		ev2.ID: ev2,
	}}, nil)
	flow := &blockingOnceFlowInvoker{started: make(chan struct{}), release: make(chan struct{})}
	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}
	w, err := NewWorkItemWorker(context.Background(), liveWorkItemConfig(), service, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}
	scheduler := NewCentralClaimScheduler()
	w.UseCentralClaimScheduler(scheduler)
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	runDone := make(chan error, 1)
	go func() {
		runDone <- w.RunUntilDrained(ctx, drain)
	}()
	select {
	case <-flow.started:
	case <-ctx.Done():
		t.Fatal("flow did not start after the WorkItem claim")
	}
	close(drain)
	close(flow.release)
	select {
	case err := <-runDone:
		if err != nil {
			t.Fatalf("RunUntilDrained after drain = %v, want nil", err)
		}
	case <-ctx.Done():
		t.Fatal("RunUntilDrained did not return after drain closed")
	}
	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-b1-ev-1" {
		t.Fatalf("completed WorkItems = %q, want in-flight row", got)
	}
	if got := strings.Join(claims.completeCalls, ","); got != connector_workqueue.WorkStateProcessed {
		t.Fatalf("terminal states = %q, want processed", got)
	}
	if len(claims.failInputs) != 0 {
		t.Fatalf("unexpected failed WorkItems: %v", claims.failInputs)
	}
	if claims.retryCalls != 0 || len(claims.retryWorkIDs) != 0 {
		t.Fatalf("RetryWorkItem calls/IDs = %d/%v, want 0/[]", claims.retryCalls, claims.retryWorkIDs)
	}
	if claims.claimByEventCalls != 1 {
		t.Fatalf("candidate CAS calls = %d, want 1 because drain stops future claims", claims.claimByEventCalls)
	}
	if len(claims.works) != 1 || claims.works[0].EventID != ev2.ID {
		t.Fatalf("remaining claimable WorkItems = %v, want only %q", claims.works, ev2.ID)
	}
}

// countingCompanionChecker is a checkCompanionInstalled function that returns
// configurable results per call index.  Calls beyond the results slice return
// the last entry.
type countingCompanionChecker struct {
	mu      sync.Mutex
	results []struct {
		present bool
		err     error
	}
	calls int
}

func (c *countingCompanionChecker) check(_ context.Context, _ string) (bool, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	idx := c.calls
	c.calls++
	if idx >= len(c.results) {
		idx = len(c.results) - 1
	}
	return c.results[idx].present, c.results[idx].err
}

// recordingFlowInvoker records calls to ExecuteFlowVersion and delegates to
// the inner invoker.
type recordingFlowInvoker struct {
	inner               FlowInvoker
	mu                  sync.Mutex
	executeVersionCalls int
}

func (r *recordingFlowInvoker) ExecuteFlow(ctx context.Context, name string, inputs map[string]any, user *models.UserSession) (*FlowResult, error) {
	return r.inner.ExecuteFlow(ctx, name, inputs, user)
}

func (r *recordingFlowInvoker) ExecuteFlowVersion(ctx context.Context, name, versionID string, inputs map[string]any, user *models.UserSession) (*FlowResult, error) {
	r.mu.Lock()
	r.executeVersionCalls++
	r.mu.Unlock()
	return r.inner.ExecuteFlowVersion(ctx, name, versionID, inputs, user)
}

func (r *recordingFlowInvoker) SystemUser(ctx context.Context) *models.UserSession {
	return r.inner.SystemUser(ctx)
}

// ── Batch-read fallback per-event test ───────────────────────────────────────

// fallbackBatchRead is a BatchReadClient for TestBatchDrain_BatchReadErrorFallsBackPerEvent.
// When len(sourceIDs) > 1 (the batch call), it returns an error.
// When len(sourceIDs) == 1, it behaves per-sourceID:
//   - missingSourceID → (nil, error)   "required enrichment missing"
//   - all others      → ({sourceID: detailJSON}, nil)
type fallbackBatchRead struct {
	missingSourceID string
	detailJSON      json.RawMessage
}

func (f *fallbackBatchRead) BatchRead(_ context.Context, _, _ string, sourceIDs []string) (map[string]json.RawMessage, error) {
	if len(sourceIDs) > 1 {
		return nil, errors.New("required enrichment missing")
	}
	id := sourceIDs[0]
	if id == f.missingSourceID {
		return nil, errors.New("required enrichment missing for single id")
	}
	return map[string]json.RawMessage{id: f.detailJSON}, nil
}

// TestBatchDrain_BatchReadErrorFallsBackPerEvent verifies the Phase 2 fallback:
// when the whole-batch BatchRead fails (requiredEnrichmentBatchReadClient path),
// the worker retries each event individually. Events whose single read ALSO fails
// are failed via failWithSyncState (MarkFailed + SyncState.RecordError) and
// dropped; siblings whose single read succeeds execute normally and are
// MarkProcessed.
func TestBatchDrain_BatchReadErrorFallsBackPerEvent(t *testing.T) {
	// Event A: sourceID whose enrichment is missing.
	const sourceIDA = "src-fallback-a"
	envBytesA, err := EncodeQueueEnvelope("fallback_kind", sourceIDA, json.RawMessage(`{"id":"`+sourceIDA+`"}`))
	if err != nil {
		t.Fatalf("EncodeQueueEnvelope A: %v", err)
	}
	evA := &connector_events.ConnectorEvent{
		ID:              "fb-ev-a",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "fb-ev-a",
		Status:          "processing",
		ClaimToken:      "token-fb-ev-a",
		Payload:         json.RawMessage(envBytesA),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
	}

	// Event B: sourceID whose enrichment is present.
	const sourceIDB = "src-fallback-b"
	envBytesB, err := EncodeQueueEnvelope("fallback_kind", sourceIDB, json.RawMessage(`{"id":"`+sourceIDB+`"}`))
	if err != nil {
		t.Fatalf("EncodeQueueEnvelope B: %v", err)
	}
	evB := &connector_events.ConnectorEvent{
		ID:              "fb-ev-b",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "fb-ev-b",
		Status:          "processing",
		ClaimToken:      "token-fb-ev-b",
		Payload:         json.RawMessage(envBytesB),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
	}

	fakes := newFakeEvents(evA, evB)
	syncstate := &fakeSyncState{}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}

	batchRead := &fallbackBatchRead{
		missingSourceID: sourceIDA,
		detailJSON:      json.RawMessage(`{"Name":"BRecord"}`),
	}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, newErr := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
			MaxClaims:     2,
			BatchSize:     10,
		},
		fakes,
		syncstate,
		batchRead,
		flow,
		resolver,
	)
	if newErr != nil {
		t.Fatalf("NewWorker: %v", newErr)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := w.Run(ctx); err != nil {
		t.Fatalf("Run: %v", err)
	}

	fakes.mu.Lock()
	processed := append([]string{}, fakes.processed...)
	failed := append([]failedCall{}, fakes.failed...)
	fakes.mu.Unlock()

	// Event B must be executed and MarkProcessed.
	if len(processed) != 1 || processed[0] != "fb-ev-b" {
		t.Errorf("processed = %v, want [fb-ev-b]", processed)
	}

	// Event A must be failed (retriably via failWithSyncState → MarkFailed).
	foundA := false
	for _, fc := range failed {
		if fc.eventID == "fb-ev-a" {
			foundA = true
		}
	}
	if !foundA {
		t.Errorf("MarkFailed not called for fb-ev-a (missing enrichment); failed = %v", failed)
	}

	// Event B must NOT be in the failed list.
	for _, fc := range failed {
		if fc.eventID == "fb-ev-b" {
			t.Errorf("MarkFailed must NOT be called for fb-ev-b (enrichment present); failed = %v", failed)
		}
	}

	// failWithSyncState path: SyncState.RecordError must be called for A.
	if len(syncstate.errors) == 0 {
		t.Errorf("want >=1 SyncState.RecordError (for fb-ev-a fallback failure), got 0")
	}
	foundSyncA := false
	for _, sc := range syncstate.errors {
		if sc.object == "fallback_kind" {
			foundSyncA = true
			break
		}
	}
	if !foundSyncA {
		t.Errorf("SyncState.RecordError not recorded for fallback_kind; errors = %v", syncstate.errors)
	}
}

// TestBatchDrain_CompanionRotatesBetweenPrepareAndExec verifies that when the
// companion incarnation check passes in Phase 1 (prepareEvent) but fails in
// Phase 3 (executeEvent, immediately before ExecuteFlowVersion), the event is
// failed terminally (B2 regression).  Before the B2 fix, executeEvent called
// ExecuteFlowVersion without a re-check, executing against a stale companion.
func TestBatchDrain_CompanionRotatesBetweenPrepareAndExec(t *testing.T) {
	// Build a cross-pack event (CompanionFlowVersionID set).
	companionPackInstallID := "companion-pack-install-1"
	ev := &connector_events.ConnectorEvent{
		ID:                     "b2-ev-1",
		Namespace:              workerTestNS,
		InstalledPackID:        "pack-test",
		EventKind:              "noop.created",
		IdempotencyKey:         "b2-ev-1",
		Status:                 "processing",
		ClaimToken:             "token-b2-ev-1",
		Payload:                noopPayload("noop_object", "src-b2-ev-1"),
		RouteVersion:           sql.NullInt64{Valid: true, Int64: 1},
		CompanionFlowVersionID: "companion-flow-v1",
		CompanionPackInstallID: companionPackInstallID,
	}

	fakes := newFakeEvents(ev)
	syncstate := &fakeSyncState{}

	// companion checker: first call (prepareEvent) returns present=true,
	// second call (executeEvent) returns present=false.
	checker := &countingCompanionChecker{
		results: []struct {
			present bool
			err     error
		}{
			{present: true, err: nil},  // Phase 1: prepareEvent — passes
			{present: false, err: nil}, // Phase 3: executeEvent — fails
		},
	}

	// inner flow invoker succeeds if called — we assert it is NEVER called.
	innerFlow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}
	recorder := &recordingFlowInvoker{inner: innerFlow}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
			BatchSize:     10,
			MaxClaims:     1,
		},
		fakes,
		syncstate,
		fakeBatchRead{},
		recorder,
		resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	w.SetCompanionInstalledChecker(checker.check)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := w.Run(ctx); err != nil {
		t.Fatalf("Run: %v", err)
	}

	fakes.mu.Lock()
	processed := append([]string{}, fakes.processed...)
	failed := append([]failedCall{}, fakes.failed...)
	fakes.mu.Unlock()

	// checker must have been called exactly twice (once in prepareEvent, once in executeEvent).
	checker.mu.Lock()
	checkerCalls := checker.calls
	checker.mu.Unlock()
	if checkerCalls != 2 {
		t.Errorf("companion checker called %d times, want 2 (once in prepareEvent, once in executeEvent)", checkerCalls)
	}

	// ExecuteFlowVersion must NOT be called — the re-check in executeEvent must
	// catch the rotation before reaching the flow invocation.
	recorder.mu.Lock()
	execVersionCalls := recorder.executeVersionCalls
	recorder.mu.Unlock()
	if execVersionCalls != 0 {
		t.Errorf("ExecuteFlowVersion called %d times, want 0 (companion rotated before exec)", execVersionCalls)
	}

	// The event must be failed terminally (MarkFailed called, not MarkProcessed).
	if len(failed) != 1 || failed[0].eventID != "b2-ev-1" {
		t.Errorf("failed = %v, want one entry for b2-ev-1", failed)
	}
	if len(processed) != 0 {
		t.Errorf("processed = %v, want empty (event must not be MarkProcessed on companion rotation)", processed)
	}
	// failTerminal: NO SyncState write.
	if len(syncstate.errors) != 0 {
		t.Errorf("want 0 SyncState errors (failTerminal path), got %d: %v", len(syncstate.errors), syncstate.errors)
	}
}

// TestBatchDrain_ClaimByIDErrorSkipsRow verifies that the central scheduler
// moves past a candidate WorkItem CAS error without a terminal/retry mutation,
// then completes later candidates in order through the mandatory WorkItem path.
func TestBatchDrain_ClaimByIDErrorSkipsRow(t *testing.T) {
	evErr := livePendingEvent("b3-err", "noop.created")
	evGood := livePendingEvent("b3-err-good", "noop.created")
	evGood2 := livePendingEvent("b3-err-good2", "noop.created")
	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-b3-err", evErr.ID, connector_workqueue.WorkTypeConnectorEvent, 0, ""),
		liveWorkItem("work-b3-err-good", evGood.ID, connector_workqueue.WorkTypeConnectorEvent, 0, ""),
		liveWorkItem("work-b3-err-good2", evGood2.ID, connector_workqueue.WorkTypeConnectorEvent, 0, ""),
	}}
	service := &candidateClaimOutcomeWorkItemService{
		completeWorkItemServiceFake: newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{events: map[string]*connector_events.ConnectorEvent{
			evErr.ID:   evErr,
			evGood.ID:  evGood,
			evGood2.ID: evGood2,
		}}, nil),
		errors: map[string]error{evErr.ID: errors.New("claim db error")},
	}
	flow := &fakeFlowInvoker{successResult: &FlowResult{IsSuccess: true}}
	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}
	cfg := liveWorkItemConfig()
	cfg.MaxClaims = 2
	cfg.BatchSize = 1
	w, err := NewWorkItemWorker(context.Background(), cfg, service, &fakeSyncState{}, fakeBatchRead{}, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}
	scheduler := NewCentralClaimScheduler()
	w.UseCentralClaimScheduler(scheduler)
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	if err := w.RunUntilDrained(ctx, nil); err != nil {
		t.Fatalf("RunUntilDrained: %v", err)
	}
	if got := strings.Join(service.ClaimAttempts(), ","); got != "b3-err,b3-err-good,b3-err-good2" {
		t.Fatalf("central scheduler claim attempts = %q, want errored then later candidates", got)
	}
	if stats := scheduler.Stats(); stats.ClaimAttempts != 3 || stats.ClaimErrors != 1 || stats.ClaimCASLost != 0 || stats.ClaimSuccess != 2 {
		t.Fatalf("central scheduler stats = %+v, want 3 attempts with one claim error and two successes", stats)
	}
	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-b3-err-good,work-b3-err-good2" {
		t.Fatalf("completed WorkItems = %q, want later candidates in order", got)
	}
	if got := strings.Join(claims.completeCalls, ","); got != strings.Join([]string{
		connector_workqueue.WorkStateProcessed,
		connector_workqueue.WorkStateProcessed,
	}, ",") {
		t.Fatalf("terminal states = %q, want processed sequence for later candidates", got)
	}
	if len(claims.failInputs) != 0 {
		t.Fatalf("FailWorkItem calls = %v, want none after a claim error", claims.failInputs)
	}
	if claims.retryCalls != 0 || len(claims.retryWorkIDs) != 0 {
		t.Fatalf("RetryWorkItem calls/IDs = %d/%v, want 0/[]", claims.retryCalls, claims.retryWorkIDs)
	}
	flow.mu.Lock()
	flowCalls := flow.calls
	flow.mu.Unlock()
	if flowCalls != 2 {
		t.Errorf("FlowInvoker called %d times, want 2 (claim-error candidate must not execute)", flowCalls)
	}
}

// partialBatchRead is a BatchReadClient that returns a partial map on success
// (sourceIDs that are absent from the map are simply not enriched — the worker
// must treat that as payload-only, not as a reason to do a second BatchRead).
type partialBatchRead struct {
	mu        sync.Mutex
	callCount int
	// partial is returned on every successful call; keys are sourceIDs present.
	partial map[string]json.RawMessage
}

func (p *partialBatchRead) BatchRead(_ context.Context, _, _ string, _ []string) (map[string]json.RawMessage, error) {
	p.mu.Lock()
	p.callCount++
	p.mu.Unlock()
	return p.partial, nil
}

// TestBatchDrain_PrefetchSucceededSourceIDAbsent verifies that when a
// kind-level prefetch BatchRead succeeds but returns a partial map (a sourceID
// present in the batch is absent from the prefetch result), the worker:
//
//	(a) calls BatchRead EXACTLY ONCE in total — no per-row single-read fallback
//	    for the absent sourceID (fallback fires only when the kind-level prefetch
//	    ERRORED, not when the sourceID is simply absent from the result map);
//	(b) completes both WorkItems through their durable lease-token CAS;
//	(c) fails neither WorkItem;
//	(d) invokes the collection flow once with both events.
func TestBatchDrain_PrefetchSucceededSourceIDAbsent(t *testing.T) {
	const objectKind = "partial_kind"

	// Build two events sharing the same objectKind so they share one prefetch call.
	makeEvent := func(id, sourceID string) *connector_events.ConnectorEvent {
		payload := noopPayload(objectKind, sourceID)
		return &connector_events.ConnectorEvent{
			ID:              id,
			Namespace:       workerTestNS,
			InstalledPackID: "pack-test",
			EventKind:       "noop.created",
			IdempotencyKey:  id,
			Status:          connector_workqueue.WorkStatePending,
			Payload:         payload,
			RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
		}
	}

	evPresent := makeEvent("pf-present", "src-present")
	evAbsent := makeEvent("pf-absent", "src-absent")

	claims := &fakeWorkItemClaims{works: []*connector_workqueue.WorkItem{
		liveWorkItem("work-pf-present", evPresent.ID, connector_workqueue.WorkTypeWorkflowActionApply, 0, "lease-pf-present"),
		liveWorkItem("work-pf-absent", evAbsent.ID, connector_workqueue.WorkTypeWorkflowActionApply, 0, "lease-pf-absent"),
	}}
	service := newCompleteWorkItemServiceFake(claims, &fakeWorkItemEventLoader{events: map[string]*connector_events.ConnectorEvent{
		evPresent.ID: evPresent,
		evAbsent.ID:  evAbsent,
	}}, nil)
	flow := &dynamicBatchSuccessFlow{}

	// Prefetch returns success with only src-present; src-absent is omitted.
	batchRead := &partialBatchRead{
		partial: map[string]json.RawMessage{
			"src-present": json.RawMessage(`{"id":"src-present","name":"Alice"}`),
		},
	}

	resolver := &stubRouteResolver{specs: map[string]connector_inbound_routing.RouteExecutionSpec{
		workerTestNS + "/noop.created": {
			FlowDeveloperName: "NoopBatchFlow",
			InvocationMode:    connector_inbound_routing.InvocationModeBatchCollection,
			MaxBatchSize:      10,
		},
	}}
	cfg := liveWorkItemConfig()
	cfg.MaxClaims = 2
	cfg.BatchSize = 10
	w, err := NewWorkItemWorker(context.Background(), cfg, service, &fakeSyncState{}, batchRead, flow, resolver)
	if err != nil {
		t.Fatalf("NewWorkItemWorker: %v", err)
	}
	result, err := w.executeClaim(context.Background(), nil, claimExecutionRequest{MaxClaims: 2})
	if err != nil || !result.Claimed || result.ClaimedRows != 2 {
		t.Fatalf("executeClaim = (%+v, %v), want two batched WorkItems", result, err)
	}

	batchRead.mu.Lock()
	totalCalls := batchRead.callCount
	batchRead.mu.Unlock()

	// (a) BatchRead called exactly once — kind-level prefetch only; no per-row
	// single-read retry for the absent sourceID.
	if totalCalls != 1 {
		t.Errorf("(a) want exactly 1 BatchRead call (kind-level prefetch only), got %d", totalCalls)
	}

	// (b) Both WorkItems are completed (absent-sourceID = payload-only success).
	if got := strings.Join(claims.completeWorkIDs, ","); got != "work-pf-present,work-pf-absent" {
		t.Errorf("(b) completed WorkItems = %q, want both rows", got)
	}
	wantStates := []string{
		connector_workqueue.WorkStateProcessed,
		connector_workqueue.WorkStateProcessed,
	}
	if got := strings.Join(claims.completeCalls, ","); got != strings.Join(wantStates, ",") {
		t.Errorf("(b) completion states = %q, want %q", got, strings.Join(wantStates, ","))
	}

	// (c) Failure and retry CAS must not be called at all.
	if len(claims.failInputs) != 0 || claims.retryCalls != 0 {
		t.Errorf("(c) failure/retry calls = %v/%d, want none", claims.failInputs, claims.retryCalls)
	}

	// (d) The batch route invokes Flow once with both events.
	flow.mu.Lock()
	flowCalls := flow.calls
	flow.mu.Unlock()
	if flowCalls != 1 {
		t.Errorf("(d) want 1 batch flow invocation, got %d", flowCalls)
	}
}

// ── P1-1 regression: companion-before-read contract ──────────────────────────
//
// RunUntilDrained must SKIP cross-pack rows (CompanionFlowVersionID != "") from
// the prefetch batch and read them per-row AFTER the companion incarnation gate
// passes in prepareEvent.  The companion-before-read contract: ZERO BatchRead
// calls must be issued for a cross-pack row whose companion check FAILS.

// orderingBatchRead is a BatchReadClient that records every BatchRead call with
// a monotonic sequence number so tests can assert ordering and distinguish
// single-ID per-row calls from multi-ID prefetch calls.
type orderingBatchRead struct {
	mu    sync.Mutex
	seq   int
	calls []orderedBatchReadCall
	// returnFn, when non-nil, maps (objectKind, sourceID) → json.RawMessage.
	// Returning nil for a sourceID means the key is absent in the response map.
	returnFn func(objectKind, sourceID string) json.RawMessage
}

type orderedBatchReadCall struct {
	seq        int
	objectKind string
	sourceIDs  []string
}

func (r *orderingBatchRead) BatchRead(_ context.Context, _, objectKind string, sourceIDs []string) (map[string]json.RawMessage, error) {
	r.mu.Lock()
	r.seq++
	ids := make([]string, len(sourceIDs))
	copy(ids, sourceIDs)
	r.calls = append(r.calls, orderedBatchReadCall{seq: r.seq, objectKind: objectKind, sourceIDs: ids})
	r.mu.Unlock()

	result := make(map[string]json.RawMessage)
	if r.returnFn != nil {
		for _, id := range sourceIDs {
			if v := r.returnFn(objectKind, id); v != nil {
				result[id] = v
			}
		}
	}
	return result, nil
}

// TestBatchDrain_CrossPackReadAfterCompanionGate verifies the companion-before-read
// contract introduced by the P1-1 fix:
//   - Cross-pack rows are SKIPPED from the prefetch batch.
//   - Case A: companion present → the cross-pack row executes, and exactly one
//     single-ID BatchRead is issued for it AFTER prepareEvent's companion gate.
//   - Case B: companion absent → failTerminal, and ZERO BatchRead calls are
//     issued for that row (the companion gate fires before any enrichment read).
//   - Case C: cross-pack row sharing an objectKind with a same-pack sibling →
//     the same-pack sibling appears in the prefetch batch call (multi-ID), and
//     the cross-pack row issues its own separate single-ID call — it never
//     reuses the same-pack prefetch cache.
func TestBatchDrain_CrossPackReadAfterCompanionGate(t *testing.T) {
	// ── shared objectKind used by Case C ──────────────────────────────────────
	const sharedKind = "shared_object"

	// ── Case A: cross-pack row whose companion IS installed ───────────────────
	const (
		companionPresentInstallID = "companion-install-present"
		companionAbsentInstallID  = "companion-install-absent"
		companionVersionID        = "companion-flow-v1"
	)

	mkCrossPackPayload := func(id, sourceID string) json.RawMessage {
		return noopPayload(sharedKind, sourceID)
	}

	evCrossA := &connector_events.ConnectorEvent{
		ID:                     "cpgr-a",
		Namespace:              workerTestNS,
		InstalledPackID:        "pack-test",
		EventKind:              "noop.created",
		IdempotencyKey:         "cpgr-a",
		Status:                 "processing",
		ClaimToken:             "token-cpgr-a",
		Payload:                mkCrossPackPayload("cpgr-a", "src-cpgr-a"),
		RouteVersion:           sql.NullInt64{Valid: true, Int64: 1},
		CompanionFlowVersionID: companionVersionID,
		CompanionPackInstallID: companionPresentInstallID,
	}

	// ── Case B: cross-pack row whose companion is NOT installed ───────────────
	evCrossB := &connector_events.ConnectorEvent{
		ID:                     "cpgr-b",
		Namespace:              workerTestNS,
		InstalledPackID:        "pack-test",
		EventKind:              "noop.created",
		IdempotencyKey:         "cpgr-b",
		Status:                 "processing",
		ClaimToken:             "token-cpgr-b",
		Payload:                mkCrossPackPayload("cpgr-b", "src-cpgr-b"),
		RouteVersion:           sql.NullInt64{Valid: true, Int64: 1},
		CompanionFlowVersionID: companionVersionID,
		CompanionPackInstallID: companionAbsentInstallID,
	}

	// ── Case C: same-pack row sharing sharedKind with the cross-pack rows ─────
	evSamePack := &connector_events.ConnectorEvent{
		ID:              "cpgr-c",
		Namespace:       workerTestNS,
		InstalledPackID: "pack-test",
		EventKind:       "noop.created",
		IdempotencyKey:  "cpgr-c",
		Status:          "processing",
		ClaimToken:      "token-cpgr-c",
		Payload:         noopPayload(sharedKind, "src-cpgr-c"),
		RouteVersion:    sql.NullInt64{Valid: true, Int64: 1},
		// CompanionFlowVersionID == "" → same-pack, goes into prefetch
	}

	fakes := newFakeEvents(evCrossA, evCrossB, evSamePack)
	syncstate := &fakeSyncState{}
	flow := &capturingVersionFlowInvoker{result: &FlowResult{IsSuccess: true}}

	recorder := &orderingBatchRead{
		returnFn: func(_, sourceID string) json.RawMessage {
			return json.RawMessage(`{"id":"` + sourceID + `"}`)
		},
	}

	// Companion checker: present for companionPresentInstallID, absent for the
	// other.  Called twice for evCrossA (prepareEvent + executeEvent), once for
	// evCrossB (prepareEvent only — executeEvent never reached on failTerminal).
	checker := func(_ context.Context, installID string) (bool, error) {
		return installID == companionPresentInstallID, nil
	}

	resolver := &stubRouteResolver{routes: map[string]string{
		workerTestNS + "/noop.created": "NoopFlow__c",
	}}

	w, err := newWorkerForTest(
		WorkerConfig{
			Namespace:     workerTestNS,
			PackInstallID: "pack-test",
			WorkerID:      "test-worker",
			PollInterval:  1 * time.Millisecond,
			MaxClaims:     3,
			BatchSize:     10,
		},
		fakes, syncstate, recorder, flow, resolver,
	)
	if err != nil {
		t.Fatalf("NewWorker: %v", err)
	}
	w.SetCompanionInstalledChecker(checker)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := w.Run(ctx); err != nil {
		t.Fatalf("Run: %v", err)
	}

	fakes.mu.Lock()
	processed := append([]string{}, fakes.processed...)
	failed := append([]failedCall{}, fakes.failed...)
	fakes.mu.Unlock()

	recorder.mu.Lock()
	allCalls := append([]orderedBatchReadCall{}, recorder.calls...)
	recorder.mu.Unlock()

	// ── Case A assertions ─────────────────────────────────────────────────────

	// (A-1) evCrossA must be MarkProcessed (companion present).
	var processedA bool
	for _, id := range processed {
		if id == "cpgr-a" {
			processedA = true
		}
	}
	if !processedA {
		t.Errorf("(A-1) cpgr-a must be MarkProcessed (companion present), got processed=%v", processed)
	}

	// (A-2) Exactly one BatchRead call issued for src-cpgr-a (single-ID, per-row).
	var crossAReads []orderedBatchReadCall
	for _, c := range allCalls {
		for _, id := range c.sourceIDs {
			if id == "src-cpgr-a" {
				crossAReads = append(crossAReads, c)
			}
		}
	}
	if len(crossAReads) != 1 {
		t.Errorf("(A-2) want exactly 1 BatchRead for src-cpgr-a, got %d (calls: %+v)", len(crossAReads), allCalls)
	} else if len(crossAReads[0].sourceIDs) != 1 {
		// The cross-pack read must be a single-ID call, not a batch.
		t.Errorf("(A-2) cross-pack BatchRead for src-cpgr-a must be single-ID; got sourceIDs=%v", crossAReads[0].sourceIDs)
	}

	// ── Case B assertions ─────────────────────────────────────────────────────

	// (B-1) evCrossB must be MarkFailed (companion absent → failTerminal).
	var failedB bool
	for _, f := range failed {
		if f.eventID == "cpgr-b" {
			failedB = true
		}
	}
	if !failedB {
		t.Errorf("(B-1) cpgr-b must be MarkFailed (companion absent), got failed=%v", failed)
	}

	// (B-2) ZERO BatchRead calls for src-cpgr-b — companion gate fires BEFORE
	// any enrichment read.  This is the core regression assertion for the
	// companion-before-read contract: no enrichment is read for a doomed row.
	for _, c := range allCalls {
		for _, id := range c.sourceIDs {
			if id == "src-cpgr-b" {
				t.Errorf("(B-2) BatchRead must NOT be called for src-cpgr-b (companion gate failed), but got call: %+v", c)
			}
		}
	}

	// ── Case C assertions ─────────────────────────────────────────────────────

	// (C-1) evSamePack (same-pack, sharedKind) must be MarkProcessed.
	var processedC bool
	for _, id := range processed {
		if id == "cpgr-c" {
			processedC = true
		}
	}
	if !processedC {
		t.Errorf("(C-1) cpgr-c must be MarkProcessed (same-pack path), got processed=%v", processed)
	}

	// (C-2) The same-pack src-cpgr-c must appear in the prefetch batch call
	// (a multi-ID call or a call that is NOT the single-ID cross-pack call for
	// src-cpgr-a).  Specifically: the call containing src-cpgr-c must be the
	// PREFETCH pass (same-pack batch), not a per-row single read.
	//
	// (C-3) src-cpgr-a must appear in a SEPARATE call from src-cpgr-c, proving
	// the cross-pack row never reused the same-pack prefetch cache.
	var sameCalls, crossACalls []orderedBatchReadCall
	for _, c := range allCalls {
		for _, id := range c.sourceIDs {
			if id == "src-cpgr-c" {
				sameCalls = append(sameCalls, c)
			}
			if id == "src-cpgr-a" {
				crossACalls = append(crossACalls, c)
			}
		}
	}
	if len(sameCalls) == 0 {
		t.Errorf("(C-2) want at least one BatchRead containing src-cpgr-c (same-pack prefetch)")
	}
	if len(crossACalls) == 0 {
		t.Errorf("(C-3) want at least one BatchRead containing src-cpgr-a")
	}
	if len(sameCalls) > 0 && len(crossACalls) > 0 {
		// They must be different calls (different seq numbers).
		if sameCalls[0].seq == crossACalls[0].seq {
			t.Errorf("(C-3) src-cpgr-c and src-cpgr-a appeared in the SAME BatchRead call (seq %d); "+
				"cross-pack row must use its own per-row single read, not the same-pack prefetch call",
				sameCalls[0].seq)
		}
	}

	// (C-4) No MarkFailed for cpgr-c.
	for _, f := range failed {
		if f.eventID == "cpgr-c" {
			t.Errorf("(C-4) cpgr-c must not be MarkFailed, got: %+v", f)
		}
	}
}

func (*concurrentWorkItemClaims) RequireSourceProjectionProtocol(context.Context, string) error {
	return nil
}
