mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-11-14 11:03:53 +01:00
fix(chat): correct legacy payout tip when unavailable
This commit is contained in:
@@ -25,6 +25,7 @@ class ChatHandler {
|
||||
this.chatHistory = new ChatHistoryManager({
|
||||
db: db || (gameModeManager?.db),
|
||||
debugManager: debugManager,
|
||||
gameModeManager,
|
||||
maxHistoryMessages: 50
|
||||
});
|
||||
|
||||
@@ -453,4 +454,4 @@ class ChatHandler {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChatHandler;
|
||||
module.exports = ChatHandler;
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
* Handles persistent storage and retrieval of chat messages
|
||||
*/
|
||||
|
||||
const { isWowPaymentsPaused } = require('../config/operatedProductProfiles');
|
||||
|
||||
const LEGACY_PAYOUT_TIP = 'Escape the dungeon to win. Collect treasure for bonus payout!';
|
||||
const NO_PAYOUT_TIP = 'Escape the dungeon to win. Treasure can improve your score.';
|
||||
|
||||
class ChatHistoryManager {
|
||||
constructor({ db, debugManager, maxHistoryMessages = 50, inMemoryFallbackSize = 100 }) {
|
||||
constructor({ db, debugManager, gameModeManager = null, maxHistoryMessages = 50, inMemoryFallbackSize = 100 }) {
|
||||
this.db = db;
|
||||
this.debugManager = debugManager;
|
||||
this.gameModeManager = gameModeManager;
|
||||
this.maxHistoryMessages = maxHistoryMessages;
|
||||
|
||||
// In-memory fallback for when DB is not available
|
||||
@@ -23,6 +29,25 @@ class ChatHistoryManager {
|
||||
this._maxMessageAge = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
|
||||
}
|
||||
|
||||
_presentMessage(message) {
|
||||
// Migration 011 seeded this one system tip before free/prestige-only policy existed.
|
||||
// Keep the stored row intact and leave player-authored copies untouched.
|
||||
if (message.socketId !== 'system' || message.username !== 'SERVER'
|
||||
|| message.type !== 'system' || message.message !== LEGACY_PAYOUT_TIP) return message;
|
||||
const manager = this.gameModeManager;
|
||||
if (!manager || typeof manager.isPayoutEnabledForMode !== 'function') return message;
|
||||
let payoutAvailable = false;
|
||||
try {
|
||||
payoutAvailable = !isWowPaymentsPaused(process.env) && manager.paymentsEnabled && (
|
||||
(manager.directModeEnabled && manager.isPayoutEnabledForMode('PAID_SINGLE'))
|
||||
|| (manager.creditsModeEnabled && manager.isPayoutEnabledForMode('PAID_CREDITS'))
|
||||
);
|
||||
} catch (_) {
|
||||
// A failed policy read cannot justify advertising an old payout promise.
|
||||
}
|
||||
return payoutAvailable ? message : { ...message, message: NO_PAYOUT_TIP };
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the manager - load recent messages into cache
|
||||
*/
|
||||
@@ -206,7 +231,7 @@ class ChatHistoryManager {
|
||||
LIMIT $1
|
||||
`, [maxCount, minCount]);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
return result.rows.map(row => this._presentMessage({
|
||||
id: row.id,
|
||||
socketId: row.socket_id,
|
||||
username: row.username,
|
||||
@@ -225,13 +250,13 @@ class ChatHistoryManager {
|
||||
|
||||
if (this._cacheInitialized && this._recentMessagesCache.length > 0) {
|
||||
const activeMessages = this._recentMessagesCache.filter(m => !m.deletedAt);
|
||||
return activeMessages.slice(-limit);
|
||||
return activeMessages.slice(-limit).map(message => this._presentMessage(message));
|
||||
}
|
||||
|
||||
// Final fallback to in-memory
|
||||
if (this._inMemoryHistory.length > 0) {
|
||||
const activeMessages = this._inMemoryHistory.filter(m => !m.deletedAt);
|
||||
return activeMessages.slice(-limit);
|
||||
return activeMessages.slice(-limit).map(message => this._presentMessage(message));
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
const ChatHistoryManager = require('../src/network/chatHistoryManager');
|
||||
|
||||
const seededTip = 'Escape the dungeon to win. Collect treasure for bonus payout!';
|
||||
const seededRow = Object.freeze({
|
||||
id: 1, socket_id: 'system', username: 'SERVER', player_id: 'SERVER',
|
||||
message_type: 'system', message: seededTip, created_at: new Date()
|
||||
});
|
||||
const userRow = Object.freeze({
|
||||
...seededRow, id: 2, socket_id: 'real-player-socket', username: 'Player',
|
||||
player_id: 'Player', message_type: 'chat'
|
||||
});
|
||||
|
||||
function manager({ payoutsAvailable, db }) {
|
||||
const gameModeManager = {
|
||||
paymentsEnabled: true,
|
||||
directModeEnabled: true,
|
||||
creditsModeEnabled: false,
|
||||
isPayoutEnabledForMode: () => payoutsAvailable
|
||||
};
|
||||
return new ChatHistoryManager({ db, gameModeManager });
|
||||
}
|
||||
|
||||
describe('historical system chat tip presentation', () => {
|
||||
test('the read path corrects only the seeded system tip when paid solo payouts are unavailable', async () => {
|
||||
const rows = [seededRow, userRow];
|
||||
const db = { query: jest.fn().mockResolvedValue({ rows }) };
|
||||
const history = manager({ payoutsAvailable: false, db });
|
||||
|
||||
const messages = await history.getRecentMessages();
|
||||
|
||||
expect(messages[0].message).not.toMatch(/bonus payout/i);
|
||||
expect(messages[0].message).toMatch(/score/i);
|
||||
expect(messages[1].message).toBe(seededTip);
|
||||
expect(rows[0].message).toBe(seededTip);
|
||||
expect(db.query).toHaveBeenCalledTimes(1);
|
||||
expect(db.query.mock.calls[0][0]).toMatch(/SELECT/);
|
||||
});
|
||||
|
||||
test('cache fallback makes the same correction without changing cached bytes', async () => {
|
||||
const db = { query: jest.fn()
|
||||
.mockResolvedValueOnce({ rows: [userRow, seededRow] })
|
||||
.mockRejectedValueOnce(new Error('disposable database unavailable')) };
|
||||
const history = manager({ payoutsAvailable: false, db });
|
||||
const errorLog = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
try {
|
||||
await history._refreshCache();
|
||||
history._cacheInitialized = true;
|
||||
const messages = await history.getRecentMessages();
|
||||
expect(messages[0].message).not.toMatch(/bonus payout/i);
|
||||
expect(messages[1].message).toBe(seededTip);
|
||||
expect(history._recentMessagesCache[0].message).toBe(seededTip);
|
||||
} finally {
|
||||
errorLog.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the original system tip remains when a paid solo payout is actually available', async () => {
|
||||
const db = { query: jest.fn().mockResolvedValue({ rows: [seededRow] }) };
|
||||
const history = manager({ payoutsAvailable: true, db });
|
||||
expect((await history.getRecentMessages())[0].message).toBe(seededTip);
|
||||
});
|
||||
|
||||
test('the official WOW pause suppresses the old promise even with stale manager flags', async () => {
|
||||
const previous = {
|
||||
OPERATED_PRODUCT_PROFILE: process.env.OPERATED_PRODUCT_PROFILE,
|
||||
PAYMENTS_ENABLED: process.env.PAYMENTS_ENABLED
|
||||
};
|
||||
try {
|
||||
process.env.OPERATED_PRODUCT_PROFILE = 'such-play-wow-prestige';
|
||||
process.env.PAYMENTS_ENABLED = 'false';
|
||||
const db = { query: jest.fn().mockResolvedValue({ rows: [seededRow] }) };
|
||||
const history = manager({ payoutsAvailable: true, db });
|
||||
expect((await history.getRecentMessages())[0].message).not.toMatch(/bonus payout/i);
|
||||
} finally {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('a system-looking row without the seed provenance is not rewritten', async () => {
|
||||
const row = { ...seededRow, username: 'Player' };
|
||||
const db = { query: jest.fn().mockResolvedValue({ rows: [row] }) };
|
||||
const history = manager({ payoutsAvailable: false, db });
|
||||
expect((await history.getRecentMessages())[0].message).toBe(seededTip);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user