feat: Implement Tour Schedule Engine with queue management and announcement features
CI-CD / Bot Lint Test Build (push) Failing after 2m10s
CI-CD / Dashboard Lint Build (push) Successful in 17s
CI-CD / Deploy to Coolify (push) Has been skipped

- Added TourScheduleEngine class for managing user queues in a guild.
- Implemented methods for joining, leaving, listing, and clearing queues.
- Added functionality to promote users to speaker in a stage channel and send announcements.
- Created integration tests for the TourScheduleEngine to verify FIFO behavior and announcement dispatch.

test: Add unit tests for ping and sign-up commands

- Created tests for ping command to ensure it replies with "Pong!".
- Implemented tests for sign-up command to verify queue joining, listing, and permission checks.

test: Add integration tests for mileage engine flow

- Developed tests to validate mileage awarding, event persistence, and role upgrades based on mileage thresholds.

chore: Update TypeScript configuration for ESLint

- Added tsconfig.eslint.json for ESLint integration.
- Modified tsconfig.json to exclude test files from the main compilation.
This commit is contained in:
2026-05-17 17:02:23 +02:00
parent 168f4ea13c
commit 8041a39dfd
71 changed files with 8906 additions and 90 deletions
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ping.test.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ping.test.d.ts","sourceRoot":"","sources":["ping.test.ts"],"names":[],"mappings":""}
+11
View File
@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ping_1 = require("../../src/commands/ping");
describe("pingCommand", () => {
it("replies with Pong", async () => {
const reply = jest.fn().mockResolvedValue(undefined);
await ping_1.pingCommand.execute({ reply });
expect(reply).toHaveBeenCalledWith("Pong!");
});
});
//# sourceMappingURL=ping.test.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ping.test.js","sourceRoot":"","sources":["ping.test.ts"],"names":[],"mappings":";;AAAA,kDAAsD;AAEtD,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,mBAAmB,EAAE,KAAK,IAAI,EAAE;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QAErD,MAAM,kBAAW,CAAC,OAAO,CAAC,EAAE,KAAK,EAAS,CAAC,CAAC;QAE5C,MAAM,CAAC,KAAK,CAAC,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
+11
View File
@@ -0,0 +1,11 @@
import { pingCommand } from "../../src/commands/ping";
describe("pingCommand", () => {
it("replies with Pong", async () => {
const reply = jest.fn().mockResolvedValue(undefined);
await pingCommand.execute({ reply } as any);
expect(reply).toHaveBeenCalledWith("Pong!");
});
});
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=sign-up.test.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"sign-up.test.d.ts","sourceRoot":"","sources":["sign-up.test.ts"],"names":[],"mappings":""}
+78
View File
@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const discord_js_1 = require("discord.js");
const sign_up_1 = require("../../src/commands/sign-up");
const tour_schedule_1 = require("../../src/tour-schedule");
jest.mock("../../src/tour-schedule", () => ({
getTourSchedule: jest.fn(),
}));
describe("signUpCommand", () => {
const queue = {
join: jest.fn(),
leave: jest.fn(),
list: jest.fn(),
clear: jest.fn(),
next: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
tour_schedule_1.getTourSchedule.mockReturnValue(queue);
});
function createInteraction(action) {
return {
inGuild: () => true,
guildId: "guild-1",
user: { id: "user-1" },
guild: { id: "guild-1" },
memberPermissions: {
has: jest.fn((permission) => permission === discord_js_1.PermissionFlagsBits.ManageChannels),
},
options: {
getSubcommand: () => action,
},
reply: jest.fn().mockResolvedValue(undefined),
};
}
it("joins queue and replies with position", async () => {
queue.join.mockReturnValue({ joined: true, position: 2 });
const interaction = createInteraction("join");
await sign_up_1.signUpCommand.execute(interaction);
expect(queue.join).toHaveBeenCalledWith("guild-1", "user-1");
expect(interaction.reply).toHaveBeenCalledWith({
content: "Added to queue at position 2.",
ephemeral: true,
});
});
it("lists queue in FIFO order", async () => {
queue.list.mockReturnValue(["user-1", "user-2"]);
const interaction = createInteraction("list");
await sign_up_1.signUpCommand.execute(interaction);
expect(interaction.reply).toHaveBeenCalledWith({
content: "1. <@user-1>\n2. <@user-2>",
ephemeral: true,
});
});
it("blocks next action when missing Manage Channels", async () => {
const interaction = createInteraction("next");
interaction.memberPermissions.has = jest.fn().mockReturnValue(false);
await sign_up_1.signUpCommand.execute(interaction);
expect(interaction.reply).toHaveBeenCalledWith({
content: "Need Manage Channels permission for this action.",
ephemeral: true,
});
});
it("advances queue and announces stage status", async () => {
queue.next.mockResolvedValue({
nextUserId: "user-7",
remaining: 3,
stageResult: "promoted",
});
const interaction = createInteraction("next");
await sign_up_1.signUpCommand.execute(interaction);
expect(interaction.reply).toHaveBeenCalledWith({
content: "Now up: <@user-7>\nQueue remaining: 3\nStage speaker promoted.",
ephemeral: false,
});
});
});
//# sourceMappingURL=sign-up.test.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"sign-up.test.js","sourceRoot":"","sources":["sign-up.test.ts"],"names":[],"mappings":"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"}
+93
View File
@@ -0,0 +1,93 @@
import { PermissionFlagsBits } from "discord.js";
import { signUpCommand } from "../../src/commands/sign-up";
import { getTourSchedule } from "../../src/tour-schedule";
jest.mock("../../src/tour-schedule", () => ({
getTourSchedule: jest.fn(),
}));
describe("signUpCommand", () => {
const queue = {
join: jest.fn(),
leave: jest.fn(),
list: jest.fn(),
clear: jest.fn(),
next: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
(getTourSchedule as jest.Mock).mockReturnValue(queue);
});
function createInteraction(action: string): any {
return {
inGuild: () => true,
guildId: "guild-1",
user: { id: "user-1" },
guild: { id: "guild-1" },
memberPermissions: {
has: jest.fn((permission: bigint) =>
permission === PermissionFlagsBits.ManageChannels,
),
},
options: {
getSubcommand: () => action,
},
reply: jest.fn().mockResolvedValue(undefined),
};
}
it("joins queue and replies with position", async () => {
queue.join.mockReturnValue({ joined: true, position: 2 });
const interaction = createInteraction("join");
await signUpCommand.execute(interaction);
expect(queue.join).toHaveBeenCalledWith("guild-1", "user-1");
expect(interaction.reply).toHaveBeenCalledWith({
content: "Added to queue at position 2.",
ephemeral: true,
});
});
it("lists queue in FIFO order", async () => {
queue.list.mockReturnValue(["user-1", "user-2"]);
const interaction = createInteraction("list");
await signUpCommand.execute(interaction);
expect(interaction.reply).toHaveBeenCalledWith({
content: "1. <@user-1>\n2. <@user-2>",
ephemeral: true,
});
});
it("blocks next action when missing Manage Channels", async () => {
const interaction = createInteraction("next");
interaction.memberPermissions.has = jest.fn().mockReturnValue(false);
await signUpCommand.execute(interaction);
expect(interaction.reply).toHaveBeenCalledWith({
content: "Need Manage Channels permission for this action.",
ephemeral: true,
});
});
it("advances queue and announces stage status", async () => {
queue.next.mockResolvedValue({
nextUserId: "user-7",
remaining: 3,
stageResult: "promoted",
});
const interaction = createInteraction("next");
await signUpCommand.execute(interaction);
expect(interaction.reply).toHaveBeenCalledWith({
content: "Now up: <@user-7>\nQueue remaining: 3\nStage speaker promoted.",
ephemeral: false,
});
});
});