fix(support): agent-demo self-seeds (join default queue + go online); tickets always get a queue

Root cause of 'waiting for escalations': no queue/membership meant tickets were
never assigned. Now: createTicket find-or-creates a default queue; new joinDefault
endpoint + useGoOnline hook; agent-demo joins+goes-AVAILABLE on load (assignPending
picks up any waiting ticket). smoke-support self-seeds (no seed script needed).
Also: vitest singleFork to end cross-file DB races (45 tests deterministic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 12:39:14 +05:30
parent eb04becc57
commit 9c6e2c76e8
9 changed files with 49 additions and 8 deletions
@@ -111,6 +111,15 @@ export class AssignmentService implements OnModuleInit {
return this.prisma.iiosSupportQueue.create({ data: { scopeId: scope.id, name } });
}
/** Find-or-create the scope's default queue and add the caller as a member. */
async joinDefault(agent: MessagePrincipal) {
const scope = await this.actors.resolveScope(agent);
const queue =
(await this.prisma.iiosSupportQueue.findFirst({ where: { scopeId: scope.id, enabled: true } })) ??
(await this.prisma.iiosSupportQueue.create({ data: { scopeId: scope.id, name: 'Support' } }));
return this.addMember(queue.id, agent);
}
async addMember(queueId: string, agent: MessagePrincipal, opts?: { maxActive?: number }) {
const queue = await this.prisma.iiosSupportQueue.findUniqueOrThrow({ where: { id: queueId } });
const actor = await this.actors.resolveActor(queue.scopeId, agent);
@@ -63,6 +63,11 @@ export class SupportController {
return this.assignment.addMember(id, this.principal(auth));
}
@Post('agents/me/join')
async joinDefault(@Headers('authorization') auth?: string) {
return this.assignment.joinDefault(this.principal(auth));
}
@Patch('members/me/availability')
async availability(@Body() body: AvailabilityDto, @Headers('authorization') auth?: string) {
return this.assignment.setAvailability(this.principal(auth), body.state);
@@ -40,10 +40,14 @@ export class SupportService {
const requester = await this.actors.resolveActor(scope.id, principal);
const traceId = randomUUID();
// Default to the scope's first enabled queue so assignment has candidates.
// Every ticket gets a queue so assignment has somewhere to route it — find
// the scope's default queue or create one.
const queueId =
input.queueId ??
(await this.prisma.iiosSupportQueue.findFirst({ where: { scopeId: scope.id, enabled: true } }))?.id;
(
(await this.prisma.iiosSupportQueue.findFirst({ where: { scopeId: scope.id, enabled: true } })) ??
(await this.prisma.iiosSupportQueue.create({ data: { scopeId: scope.id, name: 'Support' } }))
).id;
return this.prisma.$transaction(async (tx) => {
const ticket = await tx.iiosTicket.create({