tests/concurrency/ticket-status-race.test.ts fires 20 genuinely concurrent
TicketsRepository.updateStatus calls from the same starting version against
real Postgres. Passes on the first run, confirming (rather than assuming)
003-ticketing's existing atomic version-checked updateMany already holds
under real concurrency — no implementation change needed (research.md §4).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tests/concurrency/escalation-idempotency.test.ts fires the same escalation
trigger (escalationService.handleBreach) concurrently more than once for the
same ticket against real Postgres, asserting exactly one EscalationEvent and
one current Assignment result every time — verified across 10 repeated runs
(SC-003). The database-level unique-violation is visibly caught and absorbed
in the logs, confirming the fix (previous commit) actually engages under a
genuine race rather than being untested code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SlaRunRepository.updateWithVersion replaces the old unguarded update(),
mirroring TicketsRepository.updateStatus's exact atomic-updateMany pattern.
pause/resume/complete now retry (bounded) against fresh state on a version
conflict; the breach sweep skips a run that lost the race to a concurrent
pause/resume/complete rather than clobbering it, deferring to the next
scheduled pass. Verified against real Postgres: concurrent pause/resume/sweep
activity against the same run now always leaves it in one
internally-consistent state, across repeated runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AssignmentRepository.createAssignment now catches the
assignments_one_current_per_ticket unique-violation and retries the whole
supersede-then-create transaction (bounded, with jitter) instead of
propagating a raw P2002 to the caller. Verified against real Postgres: before
this fix, 20 genuinely concurrent assignment attempts on the same ticket
reliably threw an unhandled unique-constraint error; after it, exactly one
current assignment results every time across 10 repeated runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the no-op fastify.authenticate stub and the never-implemented
identity/auth login with real bcrypt password verification, JWT session
issuance/verification (reusing the existing JWT_SECRET), and a Redis-backed
revocation denylist for logout. Adds requireRole('ADMIN') to admin-only
configuration writes across 002-009 that previously relied on a decorator
that never actually checked anything. Adds self-identity (GET /auth/me,
re-validated against live account state) and admin-provisioned accounts
(POST /admin/users).
Making the auth check genuinely reject invalid/missing tokens exposed that
~18 pre-existing integration test files called already-gated routes with no
Authorization header (safe against the old no-op stub, broken against a real
one) — fixed via a shared tests/helpers/auth.ts (loginAs/authHeader) and a
file-by-file pass, plus two related SLA-run cleanup races exposed once admin
setup calls in those files' own beforeAll blocks started actually succeeding.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 7 of the roadmap. On a ticket's automatic transition to
HUMAN_ESCALATION, routing resolves 006's hierarchy nodes and
capability-eligibility lookup directly (never a second matching
algorithm), and a pluggable strategy (ROUND_ROBIN, concurrency-safe via
atomic Redis INCR; LEAST_LOADED; SKILL_BASED) selects exactly one
eligible agent, persisted as a version-row-per-period Assignment plus an
append-only AssignmentHistory event log. MANUAL/DIRECT are never
auto-selected — only an explicit admin-supplied agentId reaches them.
On success the ticket moves HUMAN_ESCALATION -> IN_PROGRESS through
003's existing state machine. A ticket's "required skill" comes from
its most recent AI diagnosis's problemType (005) when one exists,
unioned with any matching hierarchy node's skills (006); when neither
exists, there's no skill constraint (every active agent eligible),
never zero.
Found and fixed two real, latent bugs in the shared event-bus
infrastructure while building this feature's own tests: (1)
EventBus.publish was built on EventEmitter.emit(), which never awaits
async listeners, so a caller had no guarantee any subscriber (005's
AI-session-ending hook, now also this feature's orchestration hook) had
actually finished — rewritten to track subscribers directly and await
them via Promise.all, same per-handler error isolation as before. (2)
registerDomainEventHandlers() was only called from server.ts's
production startup path, never from buildApp() — meaning every
integration test in this codebase had zero domain-event subscribers
registered at all. Now called (idempotently) from buildApp() itself,
since domain-event wiring is synchronous application behavior, not a
background-worker concern like the queue.
Adds 8 unit tests (each strategy's pure selection/tie-break logic), a
dedicated round-robin concurrency test verifying no two concurrent
selections collide under real parallel load, and 2 integration test
files covering all five user stories. Full regression (every
pre-existing 002-006 integration test plus every new 007 test) run
together against real Postgres/Redis/MinIO: 124 passed, 9 skipped
(005's AI-key-gated tests, unrelated), 0 failed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>