Conversation
event.Listener now takes the canonical event name, so SendShipmentNotification and the listener used by the feature tests gain the leading parameter. The feature suite covers the new dispatcher: string events, several events and listeners at once, wildcard patterns and the matched name a wildcard listener receives, both closure forms, the errors Dispatch collects from every listener rather than stopping at the first, a queued listener registered through Listen, and the registrations Listen rejects. Each scenario declares its own event type. Listeners are resolved by event name now, which is derived from the type, so scenarios sharing a type would share listeners. The registry snapshot the suite used for isolation no longer works either, GetEvents returns a copy. Job resolves listeners by event name as well, so it reaches the ones registered through Listen, which is covered too.
Nothing unregisters an event or a queue signature, so a suite counter that restarted with the suite collided with its own previous run. The counter is process wide now, and the scenarios derive their event names from it rather than sharing globals like user.created. The Job scenario has its own event. It used to reuse events.OrderShipped, whose bootstrapped listener then appended to the result the other test asserts on. The canonical event name a listener receives is asserted rather than assumed, for an object event dispatched, queued, and run through the deprecated Job. The queued scenario also checks the job reached the queue under its signature, which the sync driver would otherwise not prove. The rejected registrations compare the framework errors by identity instead of accepting any error, and the generated listener is checked for the migrated Handle signature. Added along the way: a panicking listener surfacing on the result while the listener behind it still runs, a queued wildcard listener carrying the matched name, several events registered from a slice of event values, and the bootstrapped event fired through Dispatch rather than the deprecated Job.
goravel-coder
left a comment
There was a problem hiding this comment.
🤖 Automated review — please double-check
This is an AI-generated code review. Please double-check each finding before acting — in particular M1 (the exact
require/replaceresolution against the current frameworkmaster), since the pinned commit and merge order are the parts most likely to have shifted.
🔴 Must Fix
M1. go.mod doesn't pin a framework with Listen/Dispatch — the branch won't compile
The PR changes SendShipmentNotification.Handle to Handle(eventName string, args ...any) error and calls facades.Event().Listen(...) / .Dispatch(...) plus six new frameworkerrors.* constants, none of which exist in the pinned framework.
- PR branch (
go.mod:11+:260):require github.com/goravel/framework v1.18.0with a no-opreplace => v1.18.0.v1.18.0has the oldListener.Handle(args ...any) error, noListen/Dispatch, noevent.Result, and onlyEventListenerNotBindinerrors/list.go. origin/master(go.mod:11require v1.18.1-0.20260819065651,:261replace => v1.18.1-0.20260830012104-f18e0811c413): that replace resolves to framework commitf18e0811c413= "fix: harden Orm.Connection() connection cache against concurrent cold-path races" (2026-08-30), a side-branch commit that does not contain the event feature (noapplication_dispatch.go/application_listen.go/result.go;Listener.Handle(args ...any) error). It isn't even reachable from frameworkmaster.- The feature lands at framework commit
dc98961c("feat: [#730] add Listen and Dispatch to the event module (#1541)", merged 2026-09-02).
Fix: point require/replace at the latest framework commit (>= dc98961c, or a v1.18.1 release containing #1541), then rebase.
Staleness note: the PR forks from e2ab7d7f (go 1.25.0) while master has advanced (go 1.26.1) through #137–#148 — including #140, which added TestCommandMakeEventBroadcast/TestCommandMakeEventBroadcastNow to the same tests/feature/event_test.go. The branch's copy doesn't have them, and the file was substantially rewritten here, so the rebase will conflict and must preserve those tests.
M2. TestListenRejectsInvalidRegistrations "NotAListener" expects the wrong error string
tests/feature/event_test.go:492:
listen: func() error { return facades.Event().Listen(eventName, "not a listener") },
expectedErr: frameworkerrors.EventInvalidListener.Args("string"),The framework's newListener falls through to EventInvalidListener.Args(typeName(reflect.TypeOf(l))), and typeName builds PkgPath() + "." + Name(). A builtin string has an empty PkgPath, so the message is invalid listener .string, ..., not invalid listener string, .... Confirmed against the framework's own test (event/application_listen_test.go uses errors.EventInvalidListener.Args(".string") for the identical input).
Fix: frameworkerrors.EventInvalidListener.Args(".string"). (The other four sub-cases — nil pointer, empty signature, typed-closure mismatch, empty event name — are correct.)
🟡 Should Fix
S1. Data race on listeners.TestResultOfSendShipmentNotification
app/listeners/send_shipment_notification.go:31 appends to a package-level []string with no synchronization, while the test goroutine resets it (event_test.go:35), polls len(...) (:52, :459), and reads it (:58, :461). The bootstrapped SendShipmentNotification is queued (Queue().Enable=true), so on a real queue (redis/database + worker) Handle runs on a worker goroutine — a genuine memory-model violation that go test -race will flag. Masked in CI only because the default driver is sync (inline). The new listenerCapture-based tests do this correctly with a mutex; this global is the one exception, and the PR extends the pattern by adding TestDispatchBootstrappedEventThroughDispatch. Fix with a mutex-guarded accessor (or mirror listenerCapture).
S2. Coverage gaps vs. the claim to cover "the registrations Listen rejects"
EventQueueDuplicateSignature— two queued listeners sharing oneSignature()viaListen. A first-classListenrejection (claimQueueJobreturns it); the most on-point gap.EventHandlePanic— a panic inside an event's ownHandle(the existing panic test only panics a listener, i.e.EventListenerPanic).EventInvalidEventfor non-string/non-struct events (Listen(42, ...)) and unsupported slice kinds (Listen([]int{...}, ...)) — only the empty-string case is covered.[]anyas the events argument —string/[]string/[]event.Eventare covered,[]anyisn't.- (
EventListenerTypeMismatchandEventQueueMissingEventare effectively unreachable from the publicListen/Dispatchsurface, so I don't count those against the PR.)
S3. Panic and collect-error assertions are weaker than the rest of the suite
TestDispatchRecoversFromAPanickingListener(:399) assertsErrorContains(result.Error(), "panicked")instead ofs.Equal(frameworkerrors.EventListenerPanic.Args(...), result.Error()), unlike every other error-path test.TestDispatchCollectsEveryListenerError(:379-382) checksErrors()only by count plus twoErrorContains, which would pass if the two errors were duplicated/swapped.s.ElementsMatch([]error{first, second}, result.Errors())pins it exactly.
S4. waitUntil reinvents require.Eventually
event_test.go:709-721 hand-rolls a time.Sleep poll loop, but the repo already uses s.Require().Eventually(...) (tests/feature/disabled_runners_test.go:80). s.True(waitUntil(...)) soft-asserts and then fails later on the s.Equal with a misleading empty-capture diff; s.Require().Eventually fails fast at the real timeout with a real message.
S5. listenEvent type is shared across two tests, contradicting the isolation rationale
listenEvent is registered in both TestListenEventValuesAndListener (:297) and TestListenClosures (:345) under the same canonical name goravel/tests/feature.listenEvent. Passes today (each test asserts only on its own capture, and alphabetical order is favorable), but it contradicts the SetupTest comment's "each scenario declares its own event type" rule and breaks under -shuffle. Give TestListenClosures its own type (as was done for jobSeesListenEvent).
S6. Example inconsistency: bootstrapped wiring stays on the deprecated Register flow
bootstrap/app.go:47-56 (WithEvents) and app/services/mock_examples.go:54 still use the deprecated Job/Register while the suite teaches Listen/Dispatch. Still works (the PR's own tests fire the bootstrapped listeners through both paths), but for the example package it reads as half-migrated. Either migrate the boot path or add a comment stating the deprecated path is retained deliberately to demonstrate backward compatibility.
🟢 Nits
castString(:699) duplicatesgithub.com/spf13/cast.ToString, already a dependency — drop the helper.uniqueName's receiversis now unused after moving the counter to the package var — could be a free function (:538).Names()usesappend([]string(nil), ...)whileHandled()usescopyAnySlice— pick one idiom (:678vs:665).eventTestCounteris declared after the method that uses it (:542).Queue()appends toqueueArgson every call, soQueueCallCount()would over-count if the framework ever callsQueue()at both register and dispatch time (currently only asserted for a sync listener called once — latent fragility, not flaky).
Coverage map
| Framework surface | Covered |
|---|---|
string event / []string / event.Event / []event.Event |
✅ |
| wildcard → listener receives the matched name (sync + queued) | ✅ |
func(evt any, args ...any) error closure |
✅ |
typed func(evt *SomeEvent) error closure (event inferred from param) |
✅ |
Dispatch runs every listener & collects all errors vs. Job stops-at-first |
✅ |
queued listener via Listen: event name leads payload; GetJob(signature) resolvable |
✅ |
deprecated Job reaches listeners registered via Listen |
✅ |
panicking listener → error on Result, survivors still run |
✅ |
bootstrapped event fired via Dispatch (not Job) |
✅ |
canonical object-event name (goravel/tests/feature.listenEvent) |
✅ |
Listen rejects: non-listener / nil-pointer / empty-signature / typed-closure-mismatch / empty-event-name |
✅ (with the M2 fix) |
Dispatch rejects a second payload |
✅ |
EventQueueDuplicateSignature, EventHandlePanic, EventInvalidEvent (non-string/slice-kind), []any events |
❌ missing |
Coverage verdict: sufficient and high-quality for the feature's core; the error-rejection table just needs one more case (EventQueueDuplicateSignature) and ideally a panicking-event case to fully match its own "registrations Listen rejects" claim.
Order of operations to land this
- Bump the example's
require/replaceto the latest framework commit (>=dc98961c, or av1.18.1release containing #1541). - Rebase onto master and reconcile the
event_test.goconflict with #140's broadcast tests. - Fix the
".string"expectation in M2. - Then the Should Fix items above (S1–S6).
📑 Description
Companion to goravel/framework#1541, which merges
QueueListenerintoevent.Listenerand givesHandlethe canonical event name. This repository implements the old signature, so its Test In Example jobs on that PR fail until this lands.It also answers the review ask on #1541 — full tests for the new
Listen/DispatchAPI.Migration
event.Listener.Handletakes a leadingeventName string. A listener receives only the event name, never the event object, so it behaves the same in process and when it comes back off the queue, where only scalar arguments survive.Signature()andQueue()are unchanged, and the payload keeps its positions.Tests
tests/feature/event_test.gonow covers the new dispatcher:Listenand fired withDispatchfunc(evt any, args ...any) error, and the typedfunc(evt *SomeEvent) errorwhose event is inferred from its parameterDispatchrunning every listener and collecting each error, where the deprecatedJobstops at the firstListen, asserting the event name leads the queued payloadListenrejects: a non-listener, a nil pointer listener, an empty signature, a typed closure on an event it does not name, and an empty event nameDispatchrejecting a second payloadJobreaching listeners registered throughListenResultwhile the listener behind it still runsOrderShippedevent fired throughDispatchrather than the deprecatedJobgoravel/tests/feature.listenEvent, which is also the queue wire formatTwo behaviour changes the existing tests had to absorb
integrationEventtype across every scenario, which madeTestDispatchUnregisteredEventfind the listeners another scenario had registered under that same name. Each scenario now declares its own event type.GetEventsreturns a copy, so the registry snapshot the suite used to unregister between tests was a no-op against the new framework. It was removed.Nothing unregisters an event or a queue signature, and
app.events,app.listenersandapp.registeredgrow for the life of the process. The suite therefore derives its event names and listener signatures from a process-wide counter, so a second run in the same process (-count=2) does not collide with its own registrations.CI and merge order
Neither repository can go green on its own.
go.modpins frameworkv1.18.0, which does not have this interface, and the framework's Test In Example job checks out examplemaster. The order is: merge goravel/framework#1541, bump this repository's framework version to the merged commit, then this PR goes green.Verified locally against that branch with a
replacedirective:go build ./...andgo vet ./...both pass. The suite itself was not executed here — it needs Docker for Postgres and Redis.