feat: Implement Tour Schedule Engine with queue management and announcement features
- 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:
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=ping.test.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"ping.test.d.ts","sourceRoot":"","sources":["ping.test.ts"],"names":[],"mappings":""}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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!");
|
||||
});
|
||||
});
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=sign-up.test.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sign-up.test.d.ts","sourceRoot":"","sources":["sign-up.test.ts"],"names":[],"mappings":""}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sign-up.test.js","sourceRoot":"","sources":["sign-up.test.ts"],"names":[],"mappings":"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"}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=mileage-engine-flow.test.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"mileage-engine-flow.test.d.ts","sourceRoot":"","sources":["mileage-engine-flow.test.ts"],"names":[],"mappings":""}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const mockClient = {
|
||||
query: jest.fn(),
|
||||
release: jest.fn(),
|
||||
};
|
||||
const mockPool = {
|
||||
connect: jest.fn(async () => mockClient),
|
||||
query: jest.fn(),
|
||||
end: jest.fn(),
|
||||
};
|
||||
jest.mock("pg", () => ({
|
||||
Pool: jest.fn(() => mockPool),
|
||||
}));
|
||||
const mileage_1 = require("../../src/mileage");
|
||||
describe("MileageEngine flow", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient.query.mockImplementation(async (query) => {
|
||||
if (query.includes("RETURNING total_miles")) {
|
||||
return { rows: [{ total_miles: 120 }] };
|
||||
}
|
||||
return { rows: [] };
|
||||
});
|
||||
});
|
||||
it("awards miles, persists event, and upgrades role when threshold reached", async () => {
|
||||
const roleAdd = jest.fn().mockResolvedValue(undefined);
|
||||
const member = {
|
||||
roles: {
|
||||
cache: {
|
||||
has: jest.fn().mockReturnValue(false),
|
||||
},
|
||||
add: roleAdd,
|
||||
},
|
||||
guild: {
|
||||
members: {
|
||||
me: {
|
||||
roles: {
|
||||
highest: {
|
||||
position: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
id: "role-1",
|
||||
position: 10,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
const engine = new mileage_1.MileageEngine({
|
||||
databaseUrl: "postgres://test",
|
||||
roleTiers: [{ roleId: "role-1", minMiles: 100 }],
|
||||
eventScores: { command_execute: 10 },
|
||||
});
|
||||
const result = await engine.awardMiles({
|
||||
guildId: "guild-1",
|
||||
userId: "user-1",
|
||||
eventType: "command_execute",
|
||||
member,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
awardedMiles: 10,
|
||||
totalMiles: 120,
|
||||
upgradedRoleIds: ["role-1"],
|
||||
});
|
||||
expect(mockClient.query).toHaveBeenCalledWith("BEGIN");
|
||||
expect(mockClient.query).toHaveBeenCalledWith("COMMIT");
|
||||
expect(roleAdd).toHaveBeenCalledWith(expect.objectContaining({ id: "role-1" }), "Mileage upgrade: reached 100 miles");
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=mileage-engine-flow.test.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"mileage-engine-flow.test.js","sourceRoot":"","sources":["mileage-engine-flow.test.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,84 @@
|
||||
const mockClient = {
|
||||
query: jest.fn(),
|
||||
release: jest.fn(),
|
||||
};
|
||||
|
||||
const mockPool = {
|
||||
connect: jest.fn(async () => mockClient),
|
||||
query: jest.fn(),
|
||||
end: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock("pg", () => ({
|
||||
Pool: jest.fn(() => mockPool),
|
||||
}));
|
||||
|
||||
import { MileageEngine } from "../../src/mileage";
|
||||
|
||||
describe("MileageEngine flow", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient.query.mockImplementation(async (query: string) => {
|
||||
if (query.includes("RETURNING total_miles")) {
|
||||
return { rows: [{ total_miles: 120 }] };
|
||||
}
|
||||
|
||||
return { rows: [] };
|
||||
});
|
||||
});
|
||||
|
||||
it("awards miles, persists event, and upgrades role when threshold reached", async () => {
|
||||
const roleAdd = jest.fn().mockResolvedValue(undefined);
|
||||
const member = {
|
||||
roles: {
|
||||
cache: {
|
||||
has: jest.fn().mockReturnValue(false),
|
||||
},
|
||||
add: roleAdd,
|
||||
},
|
||||
guild: {
|
||||
members: {
|
||||
me: {
|
||||
roles: {
|
||||
highest: {
|
||||
position: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
id: "role-1",
|
||||
position: 10,
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const engine = new MileageEngine({
|
||||
databaseUrl: "postgres://test",
|
||||
roleTiers: [{ roleId: "role-1", minMiles: 100 }],
|
||||
eventScores: { command_execute: 10 },
|
||||
});
|
||||
|
||||
const result = await engine.awardMiles({
|
||||
guildId: "guild-1",
|
||||
userId: "user-1",
|
||||
eventType: "command_execute",
|
||||
member,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
awardedMiles: 10,
|
||||
totalMiles: 120,
|
||||
upgradedRoleIds: ["role-1"],
|
||||
});
|
||||
|
||||
expect(mockClient.query).toHaveBeenCalledWith("BEGIN");
|
||||
expect(mockClient.query).toHaveBeenCalledWith("COMMIT");
|
||||
expect(roleAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: "role-1" }),
|
||||
"Mileage upgrade: reached 100 miles",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=tour-schedule-flow.test.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"tour-schedule-flow.test.d.ts","sourceRoot":"","sources":["tour-schedule-flow.test.ts"],"names":[],"mappings":""}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const discord_js_1 = require("discord.js");
|
||||
const tour_schedule_1 = require("../../src/tour-schedule");
|
||||
describe("TourScheduleEngine flow", () => {
|
||||
it("handles FIFO next flow and announcement dispatch", async () => {
|
||||
const send = jest.fn().mockResolvedValue(undefined);
|
||||
const guild = {
|
||||
id: "guild-1",
|
||||
members: {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
voice: {
|
||||
channelId: "stage-1",
|
||||
setSuppressed: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
}),
|
||||
},
|
||||
channels: {
|
||||
fetch: jest
|
||||
.fn()
|
||||
.mockImplementation(async (channelId) => {
|
||||
if (channelId === "stage-1") {
|
||||
return {
|
||||
type: discord_js_1.ChannelType.GuildStageVoice,
|
||||
};
|
||||
}
|
||||
if (channelId === "announce-1") {
|
||||
return {
|
||||
isSendable: () => true,
|
||||
send,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
};
|
||||
const engine = new tour_schedule_1.TourScheduleEngine({
|
||||
stageChannelId: "stage-1",
|
||||
announceChannelId: "announce-1",
|
||||
});
|
||||
engine.join("guild-1", "user-a");
|
||||
engine.join("guild-1", "user-b");
|
||||
const first = await engine.next(guild);
|
||||
const second = await engine.next(guild);
|
||||
expect(first).toEqual({
|
||||
nextUserId: "user-a",
|
||||
remaining: 1,
|
||||
stageResult: "promoted",
|
||||
});
|
||||
expect(second).toEqual({
|
||||
nextUserId: "user-b",
|
||||
remaining: 0,
|
||||
stageResult: "promoted",
|
||||
});
|
||||
expect(send).toHaveBeenNthCalledWith(1, "Next up: <@user-a>. Queue remaining: 1.");
|
||||
expect(send).toHaveBeenNthCalledWith(2, "Next up: <@user-b>. Queue remaining: 0.");
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=tour-schedule-flow.test.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"tour-schedule-flow.test.js","sourceRoot":"","sources":["tour-schedule-flow.test.ts"],"names":[],"mappings":";;AAAA,2CAAyC;AACzC,2DAA6D;AAE7D,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QACpD,MAAM,KAAK,GAAG;YACZ,EAAE,EAAE,SAAS;YACb,OAAO,EAAE;gBACP,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC;oBACjC,KAAK,EAAE;wBACL,SAAS,EAAE,SAAS;wBACpB,aAAa,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;qBACtD;iBACF,CAAC;aACH;YACD,QAAQ,EAAE;gBACR,KAAK,EAAE,IAAI;qBACR,EAAE,EAAE;qBACJ,kBAAkB,CAAC,KAAK,EAAE,SAAiB,EAAE,EAAE;oBAC9C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;wBAC5B,OAAO;4BACL,IAAI,EAAE,wBAAW,CAAC,eAAe;yBAClC,CAAC;oBACJ,CAAC;oBAED,IAAI,SAAS,KAAK,YAAY,EAAE,CAAC;wBAC/B,OAAO;4BACL,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI;4BACtB,IAAI;yBACL,CAAC;oBACJ,CAAC;oBAED,OAAO,IAAI,CAAC;gBACd,CAAC,CAAC;aACL;SACK,CAAC;QAET,MAAM,MAAM,GAAG,IAAI,kCAAkB,CAAC;YACpC,cAAc,EAAE,SAAS;YACzB,iBAAiB,EAAE,YAAY;SAChC,CAAC,CAAC;QAEH,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAEjC,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAExC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;YACpB,UAAU,EAAE,QAAQ;YACpB,SAAS,EAAE,CAAC;YACZ,WAAW,EAAE,UAAU;SACxB,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,UAAU,EAAE,QAAQ;YACpB,SAAS,EAAE,CAAC;YACZ,WAAW,EAAE,UAAU;SACxB,CAAC,CAAC;QAEH,MAAM,CAAC,IAAI,CAAC,CAAC,uBAAuB,CAClC,CAAC,EACD,yCAAyC,CAC1C,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,CAAC,uBAAuB,CAClC,CAAC,EACD,yCAAyC,CAC1C,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ChannelType } from "discord.js";
|
||||
import { TourScheduleEngine } from "../../src/tour-schedule";
|
||||
|
||||
describe("TourScheduleEngine flow", () => {
|
||||
it("handles FIFO next flow and announcement dispatch", async () => {
|
||||
const send = jest.fn().mockResolvedValue(undefined);
|
||||
const guild = {
|
||||
id: "guild-1",
|
||||
members: {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
voice: {
|
||||
channelId: "stage-1",
|
||||
setSuppressed: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
}),
|
||||
},
|
||||
channels: {
|
||||
fetch: jest
|
||||
.fn()
|
||||
.mockImplementation(async (channelId: string) => {
|
||||
if (channelId === "stage-1") {
|
||||
return {
|
||||
type: ChannelType.GuildStageVoice,
|
||||
};
|
||||
}
|
||||
|
||||
if (channelId === "announce-1") {
|
||||
return {
|
||||
isSendable: () => true,
|
||||
send,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
} as any;
|
||||
|
||||
const engine = new TourScheduleEngine({
|
||||
stageChannelId: "stage-1",
|
||||
announceChannelId: "announce-1",
|
||||
});
|
||||
|
||||
engine.join("guild-1", "user-a");
|
||||
engine.join("guild-1", "user-b");
|
||||
|
||||
const first = await engine.next(guild);
|
||||
const second = await engine.next(guild);
|
||||
|
||||
expect(first).toEqual({
|
||||
nextUserId: "user-a",
|
||||
remaining: 1,
|
||||
stageResult: "promoted",
|
||||
});
|
||||
expect(second).toEqual({
|
||||
nextUserId: "user-b",
|
||||
remaining: 0,
|
||||
stageResult: "promoted",
|
||||
});
|
||||
|
||||
expect(send).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"Next up: <@user-a>. Queue remaining: 1.",
|
||||
);
|
||||
expect(send).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"Next up: <@user-b>. Queue remaining: 0.",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user