-
Notifications
You must be signed in to change notification settings - Fork 0
Add quote-related achievements system #288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| }); | ||
| }); | ||
|
|
||
| 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); | ||
| }); | ||
| }); | ||
| }); | ||
| 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
|
||
| }); | ||
| }); | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 = | ||||||
|
|
@@ -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" | ||||||
|
|
@@ -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); | ||||||
|
||||||
| return await quoteService.hasQuoteWithLikes(userId, 10); | |
| return quoteService.hasQuoteWithLikes(userId, 10); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
AchievementsServiceor verify that the new accolade definitions are wired correctly (names/emojis/descriptions + that check/metadata functions callQuoteServiceas expected). Add tests that instantiate the service (or access definitions) with a mockedquoteServiceand assert the quote accolade behaviors so refactors won’t silently break unlocking.