refactor: extract Discord.js code from shared services into bot layer

Move terminal.service.ts and prune.service.ts entirely to bot/modules/
since they are Discord-specific. Split lootdrop.service.ts: pure logic
(activity tracking, DB ops, claim) stays in shared/, Discord operations
(message sending, channel interactions) move to bot/modules/economy/
lootdrop.handler.ts. Move effect registry/handlers/types from bot/ to
shared/modules/inventory/ since they contain no Discord.js imports and
are needed by inventory.service.ts in shared.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-03-18 13:15:29 +01:00
parent 5a20ed23f4
commit abe25e0ceb
15 changed files with 175 additions and 137 deletions

View File

@@ -82,92 +82,81 @@ describe("lootdropService", () => {
mockModifyUserBalance.mockRestore();
});
describe("processMessage", () => {
it("should track activity but not spawn if minMessages not reached", async () => {
const mockChannel = { id: "chan1", send: mock() };
const mockMessage = {
author: { bot: false },
guild: {},
channel: mockChannel
};
describe("trackActivity", () => {
it("should track activity but not spawn if minMessages not reached", () => {
const result1 = lootdropService.trackActivity("chan1");
const result2 = lootdropService.trackActivity("chan1");
await lootdropService.processMessage(mockMessage as any);
await lootdropService.processMessage(mockMessage as any);
// Expect no spawn attempt
expect(mockChannel.send).not.toHaveBeenCalled();
// Internal state check if possible, or just behavior
expect(result1.shouldSpawn).toBe(false);
expect(result2.shouldSpawn).toBe(false);
});
it("should spawn lootdrop if minMessages reached and chance hits", async () => {
const mockChannel = { id: "chan1", send: mock() };
const mockMessage = {
author: { bot: false },
guild: {},
channel: mockChannel
};
mockChannel.send.mockResolvedValue({ id: "msg1" });
it("should spawn lootdrop if minMessages reached and chance hits", () => {
Math.random = () => 0.01; // Force hit (0.01 < 0.5)
// Send 3 messages
await lootdropService.processMessage(mockMessage as any);
await lootdropService.processMessage(mockMessage as any);
await lootdropService.processMessage(mockMessage as any);
lootdropService.trackActivity("chan1");
lootdropService.trackActivity("chan1");
const result = lootdropService.trackActivity("chan1");
expect(mockChannel.send).toHaveBeenCalled();
expect(mockInsert).toHaveBeenCalledWith(lootdrops);
// Verify DB insert
expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({
channelId: "chan1",
messageId: "msg1",
currency: "GOLD"
}));
expect(result.shouldSpawn).toBe(true);
});
it("should not spawn if chance fails", async () => {
const mockChannel = { id: "chan1", send: mock() };
const mockMessage = {
author: { bot: false },
guild: {},
channel: mockChannel
};
it("should not spawn if chance fails", () => {
Math.random = () => 0.99; // Force fail (0.99 > 0.5)
await lootdropService.processMessage(mockMessage as any);
await lootdropService.processMessage(mockMessage as any);
await lootdropService.processMessage(mockMessage as any);
lootdropService.trackActivity("chan1");
lootdropService.trackActivity("chan1");
const result = lootdropService.trackActivity("chan1");
expect(mockChannel.send).not.toHaveBeenCalled();
expect(result.shouldSpawn).toBe(false);
});
it("should respect cooldowns", async () => {
const mockChannel = { id: "chan1", send: mock() };
const mockMessage = {
author: { bot: false },
guild: {},
channel: mockChannel
};
mockChannel.send.mockResolvedValue({ id: "msg1" });
it("should respect cooldowns", () => {
Math.random = () => 0.01; // Force hit
// Trigger spawn
await lootdropService.processMessage(mockMessage as any);
await lootdropService.processMessage(mockMessage as any);
await lootdropService.processMessage(mockMessage as any);
lootdropService.trackActivity("chan1");
lootdropService.trackActivity("chan1");
const result1 = lootdropService.trackActivity("chan1");
expect(mockChannel.send).toHaveBeenCalledTimes(1);
mockChannel.send.mockClear();
expect(result1.shouldSpawn).toBe(true);
// Try again immediately (cooldown active)
await lootdropService.processMessage(mockMessage as any);
await lootdropService.processMessage(mockMessage as any);
await lootdropService.processMessage(mockMessage as any);
lootdropService.trackActivity("chan1");
lootdropService.trackActivity("chan1");
const result2 = lootdropService.trackActivity("chan1");
expect(mockChannel.send).not.toHaveBeenCalled();
expect(result2.shouldSpawn).toBe(false);
});
});
describe("calculateReward", () => {
it("should return override values when provided", () => {
const result = lootdropService.calculateReward(500, "SILVER");
expect(result.reward).toBe(500);
expect(result.currency).toBe("SILVER");
});
it("should return random reward within range when no override", () => {
const result = lootdropService.calculateReward();
expect(result.reward).toBeGreaterThanOrEqual(10);
expect(result.reward).toBeLessThanOrEqual(100);
expect(result.currency).toBe("GOLD");
});
});
describe("persistLootdrop", () => {
it("should insert lootdrop into database", async () => {
await lootdropService.persistLootdrop("msg1", "chan1", 50, "GOLD");
expect(mockInsert).toHaveBeenCalledWith(lootdrops);
expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({
messageId: "msg1",
channelId: "chan1",
rewardAmount: 50,
currency: "GOLD"
}));
});
});