Add per-staff metrics: StaffAction event log + /stats command
Event-sourced tracking of staff/ticket lifecycle actions, plus a /stats command. Foundation for a future tickets-website analytics dashboard. Data: - StaffAction model (event log) + Ticket.game / Ticket.closedAt - STATS_ADMIN_IDS config (who may view others' stats) Recording (fire-and-forget, idempotent on real state transitions): - claim, response (channel reply + /response send), escalate, de-escalate, transfer, close (4 sites), reopen — each denormalizes ticketType, tier, priority, game, requester (senderEmail / creatorId), guildId - close events carry closerType / resolverId (claimer credit) / wasClaimed; transfer carries fromId / toId; reopen stamps resolverId - conditional close transition helper (atomic open->closed + closedAt) shared by all four close paths Query + command: - pure period parser (presets + free-text) and stats shaper (per-metric keys) - command-aware autocomplete dispatch - /stats: period (autocomplete) + member (admin-gated) + source (all/email/ discord), ManageMessages + staff-role gated, ephemeral, tier-labeled embed 288+ unit tests; timing/busiest-times data is collected but displayed later.
This commit is contained in:
66
tests/autocompleteDispatch.test.js
Normal file
66
tests/autocompleteDispatch.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Phase 9 — command-aware autocomplete dispatcher tests.
|
||||
*
|
||||
* Uses the injectable _handlers parameter to test the dispatch seam without
|
||||
* requiring real Tag/Ticket DB access.
|
||||
*
|
||||
* Covers:
|
||||
* (a) commandName 'response' → response handler called
|
||||
* (b) unknown commandName → no handler called (no-op)
|
||||
* (c) handler receives the interaction object unchanged
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import { handleAutocomplete } from '../handlers/commands/index.js';
|
||||
|
||||
function makeInteraction(commandName) {
|
||||
return {
|
||||
commandName,
|
||||
options: {
|
||||
getSubcommand: vi.fn().mockReturnValue('send'),
|
||||
getFocused: vi.fn().mockReturnValue('')
|
||||
},
|
||||
respond: vi.fn().mockResolvedValue(undefined)
|
||||
};
|
||||
}
|
||||
|
||||
describe('autocomplete dispatcher', () => {
|
||||
it('routes commandName "response" to the response handler', async () => {
|
||||
const responseHandler = vi.fn().mockResolvedValue(undefined);
|
||||
const interaction = makeInteraction('response');
|
||||
|
||||
await handleAutocomplete(interaction, { response: responseHandler });
|
||||
|
||||
expect(responseHandler).toHaveBeenCalledTimes(1);
|
||||
expect(responseHandler).toHaveBeenCalledWith(interaction);
|
||||
});
|
||||
|
||||
it('routes commandName "stats" to the stats handler', async () => {
|
||||
const statsHandler = vi.fn().mockResolvedValue(undefined);
|
||||
const interaction = makeInteraction('stats');
|
||||
|
||||
await handleAutocomplete(interaction, { stats: statsHandler });
|
||||
|
||||
expect(statsHandler).toHaveBeenCalledTimes(1);
|
||||
expect(statsHandler).toHaveBeenCalledWith(interaction);
|
||||
});
|
||||
|
||||
it('no-ops for an unknown commandName', async () => {
|
||||
const responseHandler = vi.fn().mockResolvedValue(undefined);
|
||||
const interaction = makeInteraction('unknown-command');
|
||||
|
||||
await handleAutocomplete(interaction, { response: responseHandler });
|
||||
|
||||
expect(responseHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes the interaction object through to the handler unchanged', async () => {
|
||||
let received = null;
|
||||
const handler = vi.fn().mockImplementation(async i => { received = i; });
|
||||
const interaction = makeInteraction('response');
|
||||
|
||||
await handleAutocomplete(interaction, { response: handler });
|
||||
|
||||
expect(received).toBe(interaction);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user