Skip to content

Commit a3cf9e5

Browse files
authored
Merge branch 'main' into fix/dynamic-negative-delta-color
2 parents f89890b + 55488c1 commit a3cf9e5

34 files changed

Lines changed: 1887 additions & 239 deletions

.github/scripts/issue-management/claim-handler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ async function handleClaim({ github, context }) {
7474
owner,
7575
repo,
7676
issue_number: issueNumber,
77-
body: `❌ You already have **${existingIssues.length}/${MAX_ASSIGNED_ISSUES}** active assigned issues (the maximum allowed).\nPlease complete or unassign one of your current issues before claiming another.\n\n${issueList}`,
77+
body: `❌ You already have **${existingIssues.length}/${MAX_ASSIGNED_ISSUES}** active assigned issues (the maximum allowed).\nPlease complete or \`/unclaim\` one of your current issues before claiming another.\n\n${issueList}`,
7878
});
7979
return;
8080
}

.github/scripts/issue-management/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { handleAssign } = require('./assign-handler');
44
const { handleUnassign } = require('./unassign-handler');
55
const { handleAddLabel } = require('./addlabel-handler');
66
const { handleClaim } = require('./claim-handler');
7+
const { handleUnclaim } = require('./unclaim-handler');
78

89
module.exports = async ({ github, context, core }) => {
910
const commentBody = context.payload.comment?.body;
@@ -48,6 +49,9 @@ module.exports = async ({ github, context, core }) => {
4849
case 'claim':
4950
await handleClaim({ github, context });
5051
break;
52+
case 'unclaim':
53+
await handleUnclaim({ github, context });
54+
break;
5155
case 'ping':
5256
await github.rest.issues.createComment({
5357
owner,

.github/scripts/issue-management/parse-command.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ function parseCommand(commentBody) {
1515
const claimMatch = line.match(/^\/claim\s*$/i);
1616
if (claimMatch) return { command: 'claim' };
1717

18+
const unclaimMatch = line.match(/^\/unclaim\s*$/i);
19+
if (unclaimMatch) return { command: 'unclaim' };
20+
1821
const pingMatch = line.match(/^\/ping\s*$/i);
1922
if (pingMatch) return { command: 'ping' };
2023

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
async function handleUnclaim({ github, context }) {
2+
const { owner, repo } = context.repo;
3+
const issueNumber = context.payload.issue.number;
4+
const commenter = context.payload.comment.user.login;
5+
const issueState = context.payload.issue.state;
6+
7+
if (issueState === 'closed') {
8+
await github.rest.issues.createComment({
9+
owner,
10+
repo,
11+
issue_number: issueNumber,
12+
body: `❌ Commands cannot be used on closed issues.`,
13+
});
14+
return;
15+
}
16+
17+
const currentAssignees = context.payload.issue.assignees.map((a) => a.login.toLowerCase());
18+
19+
if (!currentAssignees.includes(commenter.toLowerCase())) {
20+
await github.rest.issues.createComment({
21+
owner,
22+
repo,
23+
issue_number: issueNumber,
24+
body: `ℹ️ @${commenter}, you are not currently assigned to this issue, so there's nothing to unclaim.`,
25+
});
26+
return;
27+
}
28+
29+
await github.rest.issues.removeAssignees({
30+
owner,
31+
repo,
32+
issue_number: issueNumber,
33+
assignees: [commenter],
34+
});
35+
36+
await github.rest.issues.createComment({
37+
owner,
38+
repo,
39+
issue_number: issueNumber,
40+
body: `✅ Successfully unclaimed this issue for @${commenter}.\n\n> 🔓 The issue is now open for others to claim.`,
41+
});
42+
}
43+
44+
module.exports = { handleUnclaim };

.github/workflows/issue-management.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ jobs:
2222
contains(github.event.comment.body, '/assign') ||
2323
contains(github.event.comment.body, '/unassign') ||
2424
contains(github.event.comment.body, '/addlabel') ||
25-
contains(github.event.comment.body, '/claim')
25+
contains(github.event.comment.body, '/claim') ||
26+
contains(github.event.comment.body, '/unclaim')
2627
)
2728
steps:
2829
- name: Checkout Repository

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ Our automation runs entirely through issue comments. Here is how you interact wi
338338
| Command | Who Can Use It? | What It Does |
339339
| ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------- |
340340
| `/claim` | **Issue Author (or Anyone if authored by jhasourav07)** | Self-assigns the issue to you. |
341+
| `/unclaim` | **Assigned Contributor** | Removes the assignment from yourself (opens it back up). |
341342
| `/addlabel <label1> <label2>` | **Anyone** | Adds labels to the issue (e.g. `/addlabel frontend bug`). |
342343
| `/unassign @username` | **Maintainers Only** | Removes the assignee from an issue. |
343344
| `/assign @username` | **Maintainers Only** | Manually assigns someone to an issue. |
@@ -363,7 +364,7 @@ To keep the project moving, assignments are not permanent.
363364
If the bot rejects your command, check these common scenarios:
364365

365366
- **"Commands cannot be used on closed issues"**: You cannot claim, assign, or unassign on closed issues. Find an open one!
366-
- **"You already have X/3 active assigned issues"**: You have reached the maximum of 3 concurrent assignments. Finish one of your current tasks before claiming a new issue. If you're stuck, ask a maintainer to `/unassign` you from one.
367+
- **"You already have X/3 active assigned issues"**: You have reached the maximum of 3 concurrent assignments. Finish one of your current tasks before claiming a new issue. If you're stuck, use the `/unclaim` command to unassign yourself from an issue, or ask a maintainer to `/unassign` you.
367368
- **"This issue is already assigned to @username"**: Be faster next time! Look for issues without assignees.
368369
- **"Only the author of this issue can claim it"**: You tried to `/claim` an issue you did not create. You can only claim issues that you authored (unless the issue was authored by `jhasourav07`, which anyone can claim).
369370
- **"The following label(s) do not exist"**: You can only add existing repo labels. The bot will reply with a list of valid labels you can use.

app/(root)/dashboard/[username]/page.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ describe('DashboardPage', () => {
9191
insights: [],
9292
achievements: [],
9393
commitClock: [],
94+
graphData: { nodes: [], links: [] },
9495
};
9596

9697
beforeEach(() => {

app/api/streak/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export async function GET(request: Request) {
9696
versus,
9797
shading,
9898
gradient,
99+
disable_particles,
99100
} = parseResult.data;
100101

101102
const themeName = theme || 'dark';
@@ -169,6 +170,7 @@ export async function GET(request: Request) {
169170
versus,
170171
shading,
171172
gradient,
173+
disable_particles,
172174
};
173175

174176
let calendar;

app/api/track-user/route.test.ts

Lines changed: 15 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { User } from '@/models/User';
44
import dbConnect from '@/lib/mongodb';
55

66
// Mock dependencies
7+
vi.mock('@/lib/rate-limit', () => ({
8+
trackUserRateLimiter: {
9+
check: vi.fn().mockResolvedValue(true),
10+
},
11+
}));
712
vi.mock('@/lib/mongodb', () => ({
813
default: vi.fn(),
914
}));
@@ -23,30 +28,13 @@ function makeRequest(body: Record<string, unknown>): Request {
2328
}
2429

2530
describe('POST /api/track-user', () => {
26-
let originalNodeEnv: string | undefined;
27-
let originalMongoUri: string | undefined;
28-
2931
beforeEach(() => {
30-
originalNodeEnv = process.env.NODE_ENV;
31-
originalMongoUri = process.env.MONGODB_URI;
3232
vi.clearAllMocks();
3333
});
3434

3535
afterEach(() => {
36-
vi.restoreAllMocks();
37-
38-
// Restore environment variables
39-
if (originalNodeEnv === undefined) {
40-
Reflect.deleteProperty(process.env, 'NODE_ENV');
41-
} else {
42-
Reflect.set(process.env, 'NODE_ENV', originalNodeEnv);
43-
}
44-
45-
if (originalMongoUri === undefined) {
46-
delete process.env.MONGODB_URI;
47-
} else {
48-
process.env.MONGODB_URI = originalMongoUri;
49-
}
36+
// Clean up environment variables
37+
delete process.env.MONGODB_URI;
5038
});
5139

5240
describe('Validation', () => {
@@ -65,17 +53,6 @@ describe('POST /api/track-user', () => {
6553
expect(data.success).toBe(false);
6654
expect(data.error).toBe('Malformed JSON request body');
6755
});
68-
it('returns 400 when body is plain text (not JSON)', async () => {
69-
const req = new Request('http://localhost/api/track-user', {
70-
method: 'POST',
71-
headers: { 'Content-Type': 'text/plain' },
72-
body: 'not json',
73-
});
74-
const response = await POST(req);
75-
expect(response.status).toBe(400);
76-
const data = await response.json();
77-
expect(data.success).toBe(false);
78-
});
7956

8057
it('returns 400 when username is missing', async () => {
8158
const response = await POST(makeRequest({}));
@@ -111,32 +88,8 @@ describe('POST /api/track-user', () => {
11188
'MONGODB_URI is not set. Bypassing user tracking for local development.'
11289
);
11390
expect(dbConnect).not.toHaveBeenCalled();
114-
});
115-
});
116-
117-
describe('Without MONGODB_URI (Production Environment)', () => {
118-
it('returns 500 error when MONGODB_URI is missing in production', async () => {
119-
(process.env as Record<string, string | undefined>).NODE_ENV = 'production';
120-
delete process.env.MONGODB_URI;
121-
122-
// Spy on console.error
123-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
12491

125-
const response = await POST(makeRequest({ username: 'octocat' }));
126-
127-
expect(response.status).toBe(500);
128-
const data = await response.json();
129-
expect(data.success).toBe(false);
130-
expect(data.error).toBe('Database configuration error');
131-
expect(data.bypassed).toBeUndefined();
132-
133-
// Verify critical error was logged for monitoring/alerting
134-
expect(consoleErrorSpy).toHaveBeenCalledWith(
135-
'CRITICAL: MONGODB_URI is not set in production environment. User tracking is disabled.'
136-
);
137-
138-
// Verify database connection was never attempted
139-
expect(dbConnect).not.toHaveBeenCalled();
92+
consoleSpy.mockRestore();
14093
});
14194
});
14295

@@ -153,7 +106,11 @@ describe('POST /api/track-user', () => {
153106
// Trims and lowercases
154107
expect(User.updateOne).toHaveBeenCalledWith(
155108
{ username: 'octocat' },
156-
{ $setOnInsert: { username: 'octocat' } },
109+
{
110+
$setOnInsert: { username: 'octocat' },
111+
$set: { lastSeen: expect.any(Date) },
112+
$inc: { visitCount: 1 },
113+
},
157114
{ upsert: true }
158115
);
159116

@@ -163,26 +120,8 @@ describe('POST /api/track-user', () => {
163120
expect(data.bypassed).toBeUndefined();
164121
});
165122

166-
it('normalizes purely uppercase usernames to lowercase', async () => {
167-
const response = await POST(makeRequest({ username: 'GITHUB' }));
168-
169-
expect(dbConnect).toHaveBeenCalled();
170-
171-
expect(User.updateOne).toHaveBeenCalledWith(
172-
{ username: 'github' },
173-
{ $setOnInsert: { username: 'github' } },
174-
{ upsert: true }
175-
);
176-
177-
expect(response.status).toBe(200);
178-
179-
const data = await response.json();
180-
181-
expect(data.success).toBe(true);
182-
});
183-
184123
it('returns 500 when database connection fails', async () => {
185-
vi.spyOn(console, 'error').mockImplementation(() => {});
124+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
186125
vi.mocked(dbConnect).mockRejectedValueOnce(new Error('DB Down'));
187126

188127
const response = await POST(makeRequest({ username: 'octocat' }));
@@ -191,47 +130,8 @@ describe('POST /api/track-user', () => {
191130
const data = await response.json();
192131
expect(data.success).toBe(false);
193132
expect(data.error).toBe('Internal server error');
194-
});
195-
196-
it('gracefully handles concurrent duplicate key (code 11000) race conditions', async () => {
197-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
198-
199-
const mongoError = new Error('E11000 duplicate key error collection: username') as Error & {
200-
code?: number;
201-
keyPattern?: Record<string, number>;
202-
};
203-
mongoError.code = 11000;
204-
mongoError.keyPattern = { username: 1 };
205-
vi.mocked(User.updateOne).mockRejectedValueOnce(mongoError);
206-
207-
const response = await POST(makeRequest({ username: 'octocat' }));
208-
209-
expect(response.status).toBe(200);
210-
const data = await response.json();
211-
expect(data.success).toBe(true);
212-
expect(consoleErrorSpy).not.toHaveBeenCalled();
213-
});
214-
215-
it('rethrows duplicate key (code 11000) error if it is not related to username', async () => {
216-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
217133

218-
const mongoError = new Error(
219-
'E11000 duplicate key error collection: other_field'
220-
) as Error & {
221-
code?: number;
222-
keyPattern?: Record<string, number>;
223-
};
224-
mongoError.code = 11000;
225-
mongoError.keyPattern = { other_field: 1 };
226-
vi.mocked(User.updateOne).mockRejectedValueOnce(mongoError);
227-
228-
const response = await POST(makeRequest({ username: 'octocat' }));
229-
230-
expect(response.status).toBe(500);
231-
const data = await response.json();
232-
expect(data.success).toBe(false);
233-
expect(data.error).toBe('Internal server error');
234-
expect(consoleErrorSpy).toHaveBeenCalled();
134+
consoleErrorSpy.mockRestore();
235135
});
236136
});
237137
});

app/api/track-user/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ export async function POST(req: Request) {
6262
// Upsert the user: create if doesn't exist, do nothing if exists
6363
await User.updateOne(
6464
{ username: trimmedUsername },
65-
{ $setOnInsert: { username: trimmedUsername } },
65+
{
66+
$setOnInsert: { username: trimmedUsername },
67+
$set: { lastSeen: new Date() },
68+
$inc: { visitCount: 1 },
69+
},
6670
{ upsert: true }
6771
);
6872
} catch (upsertError) {

0 commit comments

Comments
 (0)