On main, every event resolves to zero targets, so nothing runs: integrations, webhooks, notifications, ai, summary, workflow, app-webhook, remote-status-push. The job logs outcome: succeeded and enqueues no hook jobs. Silent — no warning, no error, no retry.
runEventDispatch reads the registry but nothing populates it:
events/event-dispatch-queue.ts:20 — import { resolveTargets } from './resolvers/registry'
events/resolvers/index.ts:20 — registerAllResolvers() is the only caller of registerResolver
- its only call site is
events/targets.ts:188, inside the legacy getHookTargets() adapter, which this path does not use
jobs/definitions.ts:290 calls runEventDispatch(job) with no deps, so deps.resolve ?? resolveTargets takes the bare import
resolvers/registry.ts imports only logger and types, so nothing registers as a side effect
This is the only dispatch path: emit() writes dispatch_owner: 'job' (events/emit.ts:64) and convertRelayOwnedEvents rewrites leftover relay rows to job.
Introduced by #384 (2026-08-21), which added event-dispatch-queue.ts with this import and in the same change deleted "the legacy direct getHookTargets + bulk-add path" — i.e. it replaced the one caller that registered the resolvers with one that does not. Not in any release: main and the current feature branches only.
Registration currently lives incidentally inside getHookTargets(), so any path that bypasses that adapter loses every sink silently. Doing it at worker start, or inside resolveTargets itself, would make that structurally impossible.
Environment
ghcr.io/quackbackio/quackback:main, self-hosted via docker-compose.prod.yml on Ubuntu, single workspace, Postgres 17, Redis 7. Behaviour is server-side, so browser is not a factor.
Reproduce
Enable a Jira post.created mapping (or subscribe a second user to a post so a notification is expected), then create a post:
{"component":"dispatch","event_type":"post.created","msg":"dispatching event"}
{"queue":"event-dispatch","event":"job.started"}
{"queue":"event-dispatch","event":"job.finished","duration_ms":30,"outcome":"succeeded"}
Actual: no hook job follows, no resolver logs anything, and the event is marked published.
Expected: resolveTargets returns the targets of every registered sink interested in post.created, and hook jobs are enqueued for them — as happens on v0.13.x, where the same actions produced hook attempts and notification emails.
Not covered by tests
Every call in __tests__/event-dispatch-queue.test.ts injects a stub:
await runEventDispatch(job(eventId), { resolve: webhookAndWorkflow, enqueue })
so the ?? resolveTargets fallback — the only branch production takes — is untested.
Branches checked
Present and identical on main (0204d1e), fix/messenger-defaults-followup (ca80608), feat/simpler-messenger-defaults (7476908) and saas (003b06a).
Fix
import { registerAllResolvers, resolveTargets } from './resolvers'
registerAllResolvers() // idempotent
or import the barrel for its side effect, as its docblock describes.
Suggested regression test
The gap is that no test omits resolve. One that does would have caught this:
it('resolves through the real registry when no resolver is injected', async () => {
// registry is module state — isolate so another test file's barrel import
// cannot pre-populate it and mask the bug
vi.resetModules()
const { runEventDispatch } = await import('../event-dispatch-queue')
const enqueue = vi.fn()
await runEventDispatch(job(eventId), { enqueue }) // no `resolve`
expect(enqueue).toHaveBeenCalled()
})
Worth pairing with a guard so a future regression of this shape isn't silent — resolveTargets logging a warn (or throwing) when resolvers is empty, since "no sinks registered" and "sinks found nothing" are currently indistinguishable.
Yes, Claude wrote this for me!
On
main, every event resolves to zero targets, so nothing runs: integrations, webhooks, notifications, ai, summary, workflow, app-webhook, remote-status-push. The job logsoutcome: succeededand enqueues no hook jobs. Silent — no warning, no error, no retry.runEventDispatchreads the registry but nothing populates it:events/event-dispatch-queue.ts:20—import { resolveTargets } from './resolvers/registry'events/resolvers/index.ts:20—registerAllResolvers()is the only caller ofregisterResolverevents/targets.ts:188, inside the legacygetHookTargets()adapter, which this path does not usejobs/definitions.ts:290callsrunEventDispatch(job)with no deps, sodeps.resolve ?? resolveTargetstakes the bare importresolvers/registry.tsimports only logger and types, so nothing registers as a side effectThis is the only dispatch path:
emit()writesdispatch_owner: 'job'(events/emit.ts:64) andconvertRelayOwnedEventsrewrites leftover relay rows to job.Introduced by #384 (2026-08-21), which added
event-dispatch-queue.tswith this import and in the same change deleted "the legacy directgetHookTargets+ bulk-add path" — i.e. it replaced the one caller that registered the resolvers with one that does not. Not in any release:mainand the current feature branches only.Registration currently lives incidentally inside
getHookTargets(), so any path that bypasses that adapter loses every sink silently. Doing it at worker start, or insideresolveTargetsitself, would make that structurally impossible.Environment
ghcr.io/quackbackio/quackback:main, self-hosted viadocker-compose.prod.ymlon Ubuntu, single workspace, Postgres 17, Redis 7. Behaviour is server-side, so browser is not a factor.Reproduce
Enable a Jira
post.createdmapping (or subscribe a second user to a post so a notification is expected), then create a post:Actual: no hook job follows, no resolver logs anything, and the event is marked published.
Expected:
resolveTargetsreturns the targets of every registered sink interested inpost.created, and hook jobs are enqueued for them — as happens on v0.13.x, where the same actions produced hook attempts and notification emails.Not covered by tests
Every call in
__tests__/event-dispatch-queue.test.tsinjects a stub:so the
?? resolveTargetsfallback — the only branch production takes — is untested.Branches checked
Present and identical on
main(0204d1e),fix/messenger-defaults-followup(ca80608),feat/simpler-messenger-defaults(7476908) andsaas(003b06a).Fix
or import the barrel for its side effect, as its docblock describes.
Suggested regression test
The gap is that no test omits
resolve. One that does would have caught this:Worth pairing with a guard so a future regression of this shape isn't silent —
resolveTargetslogging a warn (or throwing) whenresolversis empty, since "no sinks registered" and "sinks found nothing" are currently indistinguishable.Yes, Claude wrote this for me!