Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Claude Code Review

on:
pull_request:
types: [opened, synchronize]
#on:
# pull_request:
# types: [opened, synchronize]
Comment on lines 1 to +5
Copy link

Choose a reason for hiding this comment

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

Workflow trigger is fully disabled

The entire on: block is commented out, which means this workflow will never fire automatically on any future pull request. The job definition, secrets, and step configuration remain, but without a trigger the workflow is permanently inert. If the intent is to replace this with Greptile reviews, the file should either be deleted or have the trigger replaced rather than simply commented out, so the intent is explicit and reviewable in git history.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/claude-review.yml
Line: 1-5

Comment:
**Workflow trigger is fully disabled**

The entire `on:` block is commented out, which means this workflow will never fire automatically on any future pull request. The job definition, secrets, and step configuration remain, but without a trigger the workflow is permanently inert. If the intent is to replace this with Greptile reviews, the file should either be deleted or have the trigger replaced rather than simply commented out, so the intent is explicit and reviewable in git history.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +3 to +5
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The workflow trigger (on: pull_request) has been commented out, which disables this workflow entirely. If the intent is to keep Claude reviews running, restore the on: block; if the intent is to pause it, consider using workflow_dispatch (manual) or limiting it via paths/branches so it’s explicit and doesn’t silently disable CI automation.

Copilot uses AI. Check for mistakes.

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down
4 changes: 4 additions & 0 deletions src/commands/voice.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ export async function execute(interaction) {
if (sub === 'leaderboard') return handleLeaderboard(interaction);
if (sub === 'stats') return handleStats(interaction);
if (sub === 'export') return handleExport(interaction);

return safeEditReply(interaction, {
content: `❌ Unknown subcommand: \`${sub}\``,
});
}

// ─── /voice leaderboard ───────────────────────────────────────────────────────
Expand Down
231 changes: 230 additions & 1 deletion tests/api/routes/tempRoles.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import { _resetSecretCache } from '../../../src/api/middleware/verifyJwt.js';
import { createApp } from '../../../src/api/server.js';
import { guildCache } from '../../../src/api/utils/discordApi.js';
import { sessionStore } from '../../../src/api/utils/sessionStore.js';
import { revokeTempRoleById } from '../../../src/modules/tempRoleHandler.js';
import {
assignTempRole,
listTempRoles,
revokeTempRoleById,
} from '../../../src/modules/tempRoleHandler.js';

describe('temp roles routes', () => {
let app;
Expand Down Expand Up @@ -78,4 +82,229 @@ describe('temp roles routes', () => {
expect(res.body.error).toBe('You do not have moderator access to this guild');
expect(revokeTempRoleById).not.toHaveBeenCalled();
});

it('returns 400 when listing temp roles without guildId', async () => {
const res = await request(app).get('/api/v1/temp-roles').set('x-api-secret', 'test-secret');

expect(res.status).toBe(400);
expect(res.body.error).toContain('guildId is required');
expect(listTempRoles).not.toHaveBeenCalled();
});

it('lists temp roles with normalized pagination and user filter', async () => {
listTempRoles.mockResolvedValueOnce({
rows: [{ id: 1, user_id: 'user-1', role_id: 'role-1' }],
total: 21,
});

const res = await request(app)
.get('/api/v1/temp-roles?guildId=guild-123&userId=user-1&page=2&limit=10')
.set('x-api-secret', 'test-secret');

expect(res.status).toBe(200);
expect(res.body).toEqual({
data: [{ id: 1, user_id: 'user-1', role_id: 'role-1' }],
pagination: { page: 2, limit: 10, total: 21, pages: 3 },
});
expect(listTempRoles).toHaveBeenCalledWith('guild-123', {
userId: 'user-1',
limit: 10,
offset: 10,
});
});

it('returns 500 when listing temp roles fails', async () => {
listTempRoles.mockRejectedValueOnce(new Error('db down'));

const res = await request(app)
.get('/api/v1/temp-roles?guildId=guild-123')
.set('x-api-secret', 'test-secret');

expect(res.status).toBe(500);
expect(res.body.error).toBe('Failed to fetch temp roles');
});

it('returns 400 when deleting a temp role without guildId', async () => {
const res = await request(app)
.delete('/api/v1/temp-roles/55')
.set('x-api-secret', 'test-secret');

expect(res.status).toBe(400);
expect(res.body.error).toContain('guildId is required');
expect(revokeTempRoleById).not.toHaveBeenCalled();
});

it('returns 400 when deleting a temp role with an invalid id', async () => {
const res = await request(app)
.delete('/api/v1/temp-roles/nope?guildId=guild-123')
.set('x-api-secret', 'test-secret');

expect(res.status).toBe(400);
expect(res.body.error).toBe('Invalid id');
expect(revokeTempRoleById).not.toHaveBeenCalled();
});

it('returns 404 when the temp role record is missing', async () => {
revokeTempRoleById.mockResolvedValueOnce(null);

const res = await request(app)
.delete('/api/v1/temp-roles/55?guildId=guild-123')
.set('x-api-secret', 'test-secret');

expect(res.status).toBe(404);
expect(res.body.error).toContain('not found');
expect(revokeTempRoleById).toHaveBeenCalledWith(55, 'guild-123');
});

it('revokes a temp role and removes the role from Discord when possible', async () => {
const remove = vi.fn().mockResolvedValue(undefined);
const fetchMember = vi.fn().mockResolvedValue({
roles: { remove },
});
const fetchGuild = vi.fn().mockResolvedValue({
members: { fetch: fetchMember },
});

revokeTempRoleById.mockResolvedValueOnce({
user_id: 'user-1',
role_id: 'role-1',
});

app.locals.client.guilds.fetch = fetchGuild;

const res = await request(app)
.delete('/api/v1/temp-roles/55?guildId=guild-123')
.set('x-api-secret', 'test-secret');

expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(fetchGuild).toHaveBeenCalledWith('guild-123');
expect(fetchMember).toHaveBeenCalledWith('user-1');
expect(remove).toHaveBeenCalledWith('role-1', 'Temp role revoked via dashboard');
});

it('returns 400 when creating a temp role without required fields', async () => {
const res = await request(app)
.post('/api/v1/temp-roles')
.set('x-api-secret', 'test-secret')
.send({ guildId: 'guild-123', userId: 'user-1' });

expect(res.status).toBe(400);
expect(res.body.error).toContain('required');
expect(assignTempRole).not.toHaveBeenCalled();
});

it('returns 400 when creating a temp role with an invalid duration', async () => {
const res = await request(app)
.post('/api/v1/temp-roles')
.set('x-api-secret', 'test-secret')
.send({
guildId: 'guild-123',
userId: 'user-1',
roleId: 'role-1',
duration: 'banana',
});

expect(res.status).toBe(400);
expect(res.body.error).toContain('Invalid duration');
expect(assignTempRole).not.toHaveBeenCalled();
});

it('returns 400 when Discord objects cannot be resolved during creation', async () => {
app.locals.client.guilds.fetch.mockRejectedValueOnce(new Error('missing guild'));

const res = await request(app)
.post('/api/v1/temp-roles')
.set('x-api-secret', 'test-secret')
.send({
guildId: 'guild-123',
userId: 'user-1',
roleId: 'role-1',
duration: '1h',
});

expect(res.status).toBe(400);
expect(res.body.error).toBe('Invalid guild, user, or role');
});

it('returns 400 when the role fetch returns nothing', async () => {
const fetchMember = vi.fn().mockResolvedValue({
user: { tag: 'User#0001' },
roles: { add: vi.fn().mockResolvedValue(undefined) },
});
const fetchRole = vi.fn().mockResolvedValue(null);

app.locals.client.guilds.fetch.mockResolvedValueOnce({
members: { fetch: fetchMember },
roles: { fetch: fetchRole },
});

const res = await request(app)
.post('/api/v1/temp-roles')
.set('x-api-secret', 'test-secret')
.send({
guildId: 'guild-123',
userId: 'user-1',
roleId: 'role-1',
duration: '1h',
});

expect(res.status).toBe(400);
expect(res.body.error).toBe('Role not found');
});

it('creates a temp role assignment and returns the stored record', async () => {
const addRole = vi.fn().mockResolvedValue(undefined);
const member = {
user: { tag: 'User#0001' },
roles: { add: addRole },
};
const role = { name: 'Muted' };

app.locals.client.guilds.fetch.mockResolvedValueOnce({
members: { fetch: vi.fn().mockResolvedValue(member) },
roles: { fetch: vi.fn().mockResolvedValue(role) },
});
assignTempRole.mockResolvedValueOnce({
id: 77,
guildId: 'guild-123',
userId: 'user-1',
roleId: 'role-1',
});

const res = await request(app)
.post('/api/v1/temp-roles')
.set('x-api-secret', 'test-secret')
.send({
guildId: 'guild-123',
userId: 'user-1',
roleId: 'role-1',
duration: '2h',
reason: 'Testing',
});

expect(res.status).toBe(201);
expect(res.body).toEqual({
success: true,
data: {
id: 77,
guildId: 'guild-123',
userId: 'user-1',
roleId: 'role-1',
},
});
expect(addRole).toHaveBeenCalledWith('role-1', 'Testing');
expect(assignTempRole).toHaveBeenCalledWith(
expect.objectContaining({
guildId: 'guild-123',
userId: 'user-1',
roleId: 'role-1',
roleName: 'Muted',
userTag: 'User#0001',
reason: 'Testing',
moderatorId: 'dashboard',
moderatorTag: 'Dashboard',
}),
);
});
});
Loading
Loading