* test(s3/lifecycle): pin Schedule edge cases beyond happy path
Pre-existing schedule_test covered the happy path (ordered Drain,
empty schedule, duplicates, boundary-inclusive). Five new tests pin
edge cases the dispatcher relies on:
- Drain at a time before any DueTime returns nil and leaves the heap
intact, so the dispatcher can't accidentally consume future-due
matches.
- NextDue after partial Drain points to the next earliest, catching
a Drain that forgets the heap invariant.
- Add after Drain bubbles a fresh earlier DueTime to the front, so
late-arriving high-priority matches don't sit behind older ones.
- Drain returns Matches in ascending DueTime order regardless of
insert order — explicit pinning of the documented contract.
- Concurrent Add+Drain across 64 goroutines under -race.
* test(s3/lifecycle): actually exercise Drain in AddAfterDrain test
Per coderabbit review on #9403: the test name promised "after Drain"
but the previous body only Add'd both items without ever calling
Drain in between. Insert a real Drain (popping "drain_me") before
the second Add, so the heap-invariant-across-Drain-then-Add path is
actually pinned. Bumps the after-Drain Match's DueTime out of the
way so the Drain in step 3 returns it deterministically.
feat(s3/lifecycle): event router + DueTime schedule
Router consumes per-shard reader events, looks up matching ActionKeys via
the engine's BucketActionKeys index, and emits Matches with DueTime =
event_time + action.Delay. Evaluation runs at DueTime so the age gate
passes for fresh events; the dispatcher's identity-CAS catches drift.
Schedule is a min-heap by DueTime; duplicates allowed (RPC CAS handles
the redundant dispatch as NOOP_RESOLVED). BucketActionKeys accessor
added to engine.Snapshot.