Skip to content

Commit 2c4856c

Browse files
committed
fix: resolve CI failures on main (lint, coverage, secret scanning)
- Migrate biome.json schema to 2.4.4 and fix formatting/lint violations - Replace isNaN with Number.isNaN in db.js - Remove unused getConfig import in ticket tests - Fix import ordering in scheduler and reload tests - Fix noArrayIndexKey lint in config-editor.tsx - Add welcome command and scheduler tick tests to meet 85% branch threshold - Allowlist historical false positive commit in .gitleaks.toml
1 parent 744c7e0 commit 2c4856c

10 files changed

Lines changed: 163 additions & 9 deletions

File tree

.gitleaks.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,7 @@ paths = [
4343
'''\.gitleaks\.toml$''',
4444
'''pnpm-lock\.yaml$''',
4545
]
46+
# Historical false positive: dummy token in deleted verify-sensitive-data-redaction.js
47+
commits = [
48+
"0286a77d7695030adffa322c55b4f6c20f420dd4",
49+
]

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.4.0/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
33
"files": {
44
"includes": [
55
"src/**/*.js",

src/commands/reload.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,15 @@ export async function execute(interaction) {
6565
},
6666
logLoaded: false,
6767
});
68-
results.push({ name: 'Commands', success: true, detail: `${interaction.client.commands.size} loaded` });
69-
info('Reload: commands reloaded', { count: interaction.client.commands.size, userId: interaction.user.id });
68+
results.push({
69+
name: 'Commands',
70+
success: true,
71+
detail: `${interaction.client.commands.size} loaded`,
72+
});
73+
info('Reload: commands reloaded', {
74+
count: interaction.client.commands.size,
75+
userId: interaction.user.id,
76+
});
7077
} catch (err) {
7178
results.push({ name: 'Commands', success: false, error: err.message });
7279
logError('Reload: command reload failed', { error: err.message });
@@ -76,7 +83,12 @@ export async function execute(interaction) {
7683
try {
7784
const commands = Array.from(interaction.client.commands.values());
7885
const guildId = process.env.GUILD_ID || null;
79-
await registerCommands(commands, interaction.client.user.id, process.env.DISCORD_TOKEN, guildId);
86+
await registerCommands(
87+
commands,
88+
interaction.client.user.id,
89+
process.env.DISCORD_TOKEN,
90+
guildId,
91+
);
8092
results.push({ name: 'Register', success: true });
8193
info('Reload: commands registered with Discord', { userId: interaction.user.id });
8294
} catch (err) {

src/db.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export async function initDb() {
224224
/** @param {string|undefined} env @param {number} defaultVal */
225225
const parsePositiveInt = (env, defaultVal) => {
226226
const val = parseInt(env, 10);
227-
return isNaN(val) || val < 0 ? defaultVal : val;
227+
return Number.isNaN(val) || val < 0 ? defaultVal : val;
228228
};
229229

230230
const poolSize = Math.max(1, parsePositiveInt(process.env.PG_POOL_SIZE, 5));

src/modules/scheduler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
import { getPool } from '../db.js';
99
import { info, error as logError, warn as logWarn } from '../logger.js';
10-
import { runMaintenance } from '../utils/dbMaintenance.js';
1110
import { getNextCronRun, parseCron } from '../utils/cronParser.js';
11+
import { runMaintenance } from '../utils/dbMaintenance.js';
1212
import { safeSend } from '../utils/safeSend.js';
1313
import { checkDailyChallenge } from './challengeScheduler.js';
1414
import { getConfig } from './config.js';

tests/commands/reload.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ vi.mock('../../src/utils/safeSend.js', () => ({
4747
safeEditReply: vi.fn().mockResolvedValue(undefined),
4848
}));
4949

50+
import { adminOnly, data, execute } from '../../src/commands/reload.js';
5051
import { loadConfig } from '../../src/modules/config.js';
5152
import { loadOptOuts } from '../../src/modules/optout.js';
5253
import { startTriage, stopTriage } from '../../src/modules/triage.js';
5354
import { loadCommandsFromDirectory } from '../../src/utils/loadCommands.js';
5455
import { isBotOwner } from '../../src/utils/permissions.js';
5556
import { registerCommands } from '../../src/utils/registerCommands.js';
5657
import { safeEditReply, safeReply } from '../../src/utils/safeSend.js';
57-
import { adminOnly, data, execute } from '../../src/commands/reload.js';
5858

5959
function mockInteraction(overrides = {}) {
6060
return {

tests/commands/ticket.test.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ vi.mock('discord.js', () => {
121121
};
122122
});
123123

124-
import { getConfig } from '../../src/modules/config.js';
125124
import { isModerator } from '../../src/utils/permissions.js';
126125
import { safeEditReply, safeSend } from '../../src/utils/safeSend.js';
127126

tests/commands/welcome.test.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
vi.mock('../../src/logger.js', () => ({
4+
info: vi.fn(),
5+
error: vi.fn(),
6+
warn: vi.fn(),
7+
debug: vi.fn(),
8+
}));
9+
10+
vi.mock('../../src/modules/config.js', () => ({
11+
getConfig: vi.fn().mockReturnValue({}),
12+
}));
13+
14+
vi.mock('../../src/modules/welcomeOnboarding.js', () => ({
15+
buildRoleMenuMessage: vi.fn().mockReturnValue(null),
16+
buildRulesAgreementMessage: vi.fn().mockReturnValue({ content: 'rules' }),
17+
normalizeWelcomeOnboardingConfig: vi.fn().mockReturnValue({}),
18+
}));
19+
20+
vi.mock('../../src/utils/permissions.js', () => ({
21+
isModerator: vi.fn().mockReturnValue(false),
22+
}));
23+
24+
vi.mock('../../src/utils/safeSend.js', () => ({
25+
safeSend: vi.fn().mockResolvedValue(undefined),
26+
safeEditReply: vi.fn().mockResolvedValue(undefined),
27+
}));
28+
29+
import { PermissionsBitField } from 'discord.js';
30+
import { adminOnly, data, execute } from '../../src/commands/welcome.js';
31+
import { isModerator } from '../../src/utils/permissions.js';
32+
import { safeEditReply } from '../../src/utils/safeSend.js';
33+
34+
function mockInteraction(overrides = {}) {
35+
return {
36+
member: {
37+
id: 'user-1',
38+
permissions: new PermissionsBitField(),
39+
},
40+
user: { id: 'user-1' },
41+
guildId: 'guild-1',
42+
guild: {
43+
channels: {
44+
cache: { get: vi.fn().mockReturnValue(null) },
45+
fetch: vi.fn().mockResolvedValue(null),
46+
},
47+
},
48+
deferReply: vi.fn().mockResolvedValue(undefined),
49+
...overrides,
50+
};
51+
}
52+
53+
describe('welcome command', () => {
54+
afterEach(() => {
55+
vi.clearAllMocks();
56+
});
57+
58+
it('should export data with name "welcome"', () => {
59+
expect(data.name).toBe('welcome');
60+
});
61+
62+
it('should export adminOnly = true', () => {
63+
expect(adminOnly).toBe(true);
64+
});
65+
66+
it('should reject non-admin non-moderator users', async () => {
67+
const interaction = mockInteraction();
68+
69+
await execute(interaction);
70+
71+
expect(safeEditReply).toHaveBeenCalledWith(
72+
interaction,
73+
expect.objectContaining({
74+
content: expect.stringContaining('moderator or administrator'),
75+
}),
76+
);
77+
});
78+
79+
it('should show not configured messages when welcome config is empty', async () => {
80+
isModerator.mockReturnValueOnce(true);
81+
const interaction = mockInteraction();
82+
83+
await execute(interaction);
84+
85+
expect(safeEditReply).toHaveBeenCalledWith(
86+
interaction,
87+
expect.objectContaining({
88+
content: expect.stringContaining('not configured'),
89+
}),
90+
);
91+
});
92+
});

tests/modules/scheduler.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,42 @@ vi.mock('../../src/utils/safeSend.js', () => ({
1717
safeEditReply: (t, opts) => t.editReply(opts),
1818
}));
1919

20+
vi.mock('../../src/modules/pollHandler.js', () => ({
21+
closeExpiredPolls: vi.fn().mockResolvedValue(undefined),
22+
}));
23+
24+
vi.mock('../../src/modules/reminderHandler.js', () => ({
25+
checkReminders: vi.fn().mockResolvedValue(undefined),
26+
}));
27+
28+
vi.mock('../../src/modules/challengeScheduler.js', () => ({
29+
checkDailyChallenge: vi.fn().mockResolvedValue(undefined),
30+
}));
31+
32+
vi.mock('../../src/modules/reviewHandler.js', () => ({
33+
expireStaleReviews: vi.fn().mockResolvedValue(undefined),
34+
}));
35+
36+
vi.mock('../../src/modules/ticketHandler.js', () => ({
37+
checkAutoClose: vi.fn().mockResolvedValue(undefined),
38+
}));
39+
40+
vi.mock('../../src/utils/dbMaintenance.js', () => ({
41+
runMaintenance: vi.fn().mockResolvedValue(undefined),
42+
}));
43+
44+
vi.mock('../../src/modules/config.js', () => ({
45+
getConfig: vi.fn().mockReturnValue({}),
46+
}));
47+
2048
import { getPool } from '../../src/db.js';
2149
import {
2250
getNextCronRun,
2351
parseCron,
2452
startScheduler,
2553
stopScheduler,
2654
} from '../../src/modules/scheduler.js';
55+
import { checkAutoClose } from '../../src/modules/ticketHandler.js';
2756
import { safeSend } from '../../src/utils/safeSend.js';
2857

2958
describe('scheduler module', () => {
@@ -404,5 +433,20 @@ describe('scheduler module', () => {
404433
// Should not throw
405434
expect(getPool).toHaveBeenCalled();
406435
});
436+
437+
it('should call checkAutoClose on every 5th tick', async () => {
438+
mockPool.query.mockResolvedValue({ rows: [] });
439+
440+
startScheduler(mockClient);
441+
442+
// Run enough ticks so that at least one is divisible by 5
443+
// tickCount starts at 0 and increments each poll
444+
for (let i = 0; i < 5; i++) {
445+
await vi.advanceTimersByTimeAsync(i === 0 ? 0 : 60_000);
446+
await vi.advanceTimersByTimeAsync(0);
447+
}
448+
449+
expect(checkAutoClose).toHaveBeenCalled();
450+
});
407451
});
408452
});

web/src/components/dashboard/config-editor.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1492,7 +1492,10 @@ export function ConfigEditor() {
14921492
</p>
14931493
{(draftConfig.engagement?.activityBadges ?? DEFAULT_ACTIVITY_BADGES).map(
14941494
(badge: { days?: number; label?: string }, i: number) => (
1495-
<div key={i} className="flex items-center gap-2">
1495+
<div
1496+
key={`badge-${badge.days ?? 0}-${badge.label ?? i}`}
1497+
className="flex items-center gap-2"
1498+
>
14961499
<Input
14971500
className="w-20"
14981501
type="number"

0 commit comments

Comments
 (0)