Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@ Total Achievements: 0
- 🔥 **On a Roll** - Connected for 7 consecutive days (5+ min/day)
- ⚡ **Dedicated AF** - Connected for 14 consecutive days (5+ min/day)
- 💀 **No-Lifer** - Connected for 30 consecutive days (5+ min/day)
- 🗣️ **Quotable** - Been quoted for the first time
- 📝 **Quote Master** - Added 10 quotes to the collection
- 📚 **Quote Collector** - Added 50 quotes to the collection
- 🏆 **Quote Legend** - Added 100 quotes to the collection
- ⭐ **Widely Quoted** - Been quoted 25 times
- 💫 **Quote Icon** - Been quoted 50 times
- 🔥 **Viral Quote** - Have a quote with 10+ likes

**Note on Time-Based Accolades:**

Expand Down
79 changes: 79 additions & 0 deletions __tests__/services/achievements-quote-accolades.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, expect } from "@jest/globals";

describe("AchievementsService - Quote Accolades", () => {
describe("Quote Accolade Structure", () => {
it("should have seven quote-related accolade types", () => {
const quoteAccoladeCount = 7;
expect(quoteAccoladeCount).toBe(7);
});

it("should have appropriate thresholds for quote accolades", () => {
const thresholds = {
quotable: 1, // First quote
quote_master: 10, // 10 quotes added
quote_collector: 50, // 50 quotes added
quote_legend: 100, // 100 quotes added
widely_quoted: 25, // 25 times quoted
quote_icon: 50, // 50 times quoted
viral_quote: 10, // 10+ likes
};

// Validate thresholds are reasonable
Object.values(thresholds).forEach((threshold) => {
expect(threshold).toBeGreaterThan(0);
});

// Ensure progression makes sense for adding quotes
expect(thresholds.quotable).toBeLessThan(thresholds.quote_master);
expect(thresholds.quote_master).toBeLessThan(
thresholds.quote_collector,
);
expect(thresholds.quote_collector).toBeLessThan(thresholds.quote_legend);

// Ensure progression makes sense for being quoted
expect(thresholds.widely_quoted).toBeLessThan(thresholds.quote_icon);
});

it("should use distinct emoji for each quote accolade", () => {
const expectedEmojis = {
quotable: "🗣️",
quote_master: "📝",
quote_collector: "📚",
quote_legend: "🏆",
widely_quoted: "⭐",
quote_icon: "💫",
viral_quote: "🔥",
};

// Validate all emojis are unique
const emojiValues = Object.values(expectedEmojis);
const uniqueEmojis = new Set(emojiValues);
expect(uniqueEmojis.size).toBe(emojiValues.length);
});
Comment on lines +1 to +52
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests currently only assert hard-coded constants/arrays and don’t exercise AchievementsService or verify that the new accolade definitions are wired correctly (names/emojis/descriptions + that check/metadata functions call QuoteService as expected). Add tests that instantiate the service (or access definitions) with a mocked quoteService and assert the quote accolade behaviors so refactors won’t silently break unlocking.

Copilot generated this review using guidance from repository custom instructions.
});

describe("Quote Accolade Categories", () => {
it("should have three accolades for adding quotes", () => {
const addingQuoteAccolades = [
"quote_master",
"quote_collector",
"quote_legend",
];
expect(addingQuoteAccolades.length).toBe(3);
});

it("should have three accolades for being quoted", () => {
const beingQuotedAccolades = [
"quotable",
"widely_quoted",
"quote_icon",
];
expect(beingQuotedAccolades.length).toBe(3);
});

it("should have one accolade for quote engagement", () => {
const engagementAccolades = ["viral_quote"];
expect(engagementAccolades.length).toBe(1);
});
});
});
39 changes: 39 additions & 0 deletions __tests__/services/quote-service-achievements.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
import { QuoteService } from "../../src/services/quote-service.js";

// Mock mongoose and dependencies
jest.mock("mongoose");
jest.mock("../../src/database/schema.js");
jest.mock("../../src/services/config-service.js");
jest.mock("../../src/services/cooldown-manager.js");

describe("QuoteService - Achievement Methods", () => {
let quoteService: QuoteService;

beforeEach(() => {
jest.clearAllMocks();
quoteService = new QuoteService();
});

describe("achievement-related methods", () => {
it("should have getQuotesAddedByUser method", () => {
expect(typeof quoteService.getQuotesAddedByUser).toBe("function");
expect(quoteService.getQuotesAddedByUser.length).toBe(1);
});

it("should have getQuotesAuthoredByUser method", () => {
expect(typeof quoteService.getQuotesAuthoredByUser).toBe("function");
expect(quoteService.getQuotesAuthoredByUser.length).toBe(1);
});

it("should have getMostLikedQuoteByAuthor method", () => {
expect(typeof quoteService.getMostLikedQuoteByAuthor).toBe("function");
expect(quoteService.getMostLikedQuoteByAuthor.length).toBe(1);
});

it("should have hasQuoteWithLikes method", () => {
expect(typeof quoteService.hasQuoteWithLikes).toBe("function");
expect(quoteService.hasQuoteWithLikes.length).toBe(2);
});
Comment on lines +18 to +37
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test suite only checks that methods exist / arity, but never asserts the actual query behavior (e.g., that the correct filters are passed to countDocuments, that likes sorting is descending, etc.). Since these methods directly drive achievements unlocking, add behavior-focused tests by mocking the mongoose model and asserting the right calls/results.

Copilot generated this review using guidance from repository custom instructions.
});
});
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

128 changes: 127 additions & 1 deletion src/services/achievements-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { VoiceChannelTracking } from "../models/voice-channel-tracking.js";
import { ConfigService } from "./config-service.js";
import logger from "../utils/logger.js";
import mongoose from "mongoose";
import { quoteService } from "./quote-service.js";

// Badge type definitions
export type AccoladeType =
Expand All @@ -26,7 +27,14 @@ export type AccoladeType =
| "weekday_warrior"
| "consistent_week"
| "consistent_fortnight"
| "consistent_month";
| "consistent_month"
| "quotable"
| "quote_master"
| "quote_collector"
| "quote_legend"
| "widely_quoted"
| "quote_icon"
| "viral_quote";

export type AchievementType =
| "weekly_champion"
Expand Down Expand Up @@ -516,6 +524,124 @@ export class AchievementsService {
};
},
},
quotable: {
emoji: "🗣️",
name: "Quotable",
description: "Been quoted for the first time",
checkFunction: async (userId: string) => {
const count = await quoteService.getQuotesAuthoredByUser(userId);
return count >= 1;
},
metadataFunction: async (userId: string) => {
const count = await quoteService.getQuotesAuthoredByUser(userId);
return {
value: count,
description: "First quote",
unit: "quotes",
};
},
},
quote_master: {
emoji: "📝",
name: "Quote Master",
description: "Added 10 quotes to the collection",
checkFunction: async (userId: string) => {
const count = await quoteService.getQuotesAddedByUser(userId);
return count >= 10;
},
metadataFunction: async (userId: string) => {
const count = await quoteService.getQuotesAddedByUser(userId);
return {
value: count,
description: "10+ quotes added",
unit: "quotes",
};
},
},
quote_collector: {
emoji: "📚",
name: "Quote Collector",
description: "Added 50 quotes to the collection",
checkFunction: async (userId: string) => {
const count = await quoteService.getQuotesAddedByUser(userId);
return count >= 50;
},
metadataFunction: async (userId: string) => {
const count = await quoteService.getQuotesAddedByUser(userId);
return {
value: count,
description: "50+ quotes added",
unit: "quotes",
};
},
},
quote_legend: {
emoji: "🏆",
name: "Quote Legend",
description: "Added 100 quotes to the collection",
checkFunction: async (userId: string) => {
const count = await quoteService.getQuotesAddedByUser(userId);
return count >= 100;
},
metadataFunction: async (userId: string) => {
const count = await quoteService.getQuotesAddedByUser(userId);
return {
value: count,
description: "100+ quotes added",
unit: "quotes",
};
},
},
widely_quoted: {
emoji: "⭐",
name: "Widely Quoted",
description: "Been quoted 25 times",
checkFunction: async (userId: string) => {
const count = await quoteService.getQuotesAuthoredByUser(userId);
return count >= 25;
},
metadataFunction: async (userId: string) => {
const count = await quoteService.getQuotesAuthoredByUser(userId);
return {
value: count,
description: "25+ times quoted",
unit: "quotes",
};
},
},
quote_icon: {
emoji: "💫",
name: "Quote Icon",
description: "Been quoted 50 times",
checkFunction: async (userId: string) => {
const count = await quoteService.getQuotesAuthoredByUser(userId);
return count >= 50;
},
metadataFunction: async (userId: string) => {
const count = await quoteService.getQuotesAuthoredByUser(userId);
return {
value: count,
description: "50+ times quoted",
unit: "quotes",
};
},
},
viral_quote: {
emoji: "🔥",
name: "Viral Quote",
description: "Have a quote with 10+ likes",
checkFunction: async (userId: string) => {
return await quoteService.hasQuoteWithLikes(userId, 10);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant return await here adds an unnecessary async hop and may violate common lint rules (e.g., no-return-await). You can directly return the promise from quoteService.hasQuoteWithLikes(...).

Suggested change
return await quoteService.hasQuoteWithLikes(userId, 10);
return quoteService.hasQuoteWithLikes(userId, 10);

Copilot uses AI. Check for mistakes.
},
metadataFunction: async (userId: string) => {
const mostLiked = await quoteService.getMostLikedQuoteByAuthor(userId);
return {
value: mostLiked?.likes || 0,
description: "10+ likes on a quote",
unit: "likes",
};
},
Comment on lines +527 to +643
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During a single achievements check, these quote-related accolade definitions can trigger multiple redundant DB queries (e.g., 3 separate getQuotesAuthoredByUser counts + 3 separate getQuotesAddedByUser counts, plus viral checks). This can become expensive as user count grows. Consider precomputing quote stats once per user per run (authoredCount, addedCount, mostLikedLikes/viral) and reusing those values across check/metadata functions.

Copilot uses AI. Check for mistakes.
},
};

// Achievement definitions (time-based, not announced)
Expand Down
Loading
Loading