feat(ui): rich forge entity views — commits, files/diff, timeline, checks, metadata chips

- server: new read routes for PR/MR commits, timeline, reviews, commit-statuses across github/gitlab/gitea; enrich PR/issue summaries with labels/assignees/milestone/commentsCount
- ui: forge facade gains getCommits/getTimeline/getChecks; shared ForgeEntityDetailView + section components; mounted into PR/MR views and issue sections
This commit is contained in:
2026-08-16 16:29:24 +00:00
parent f02c33700b
commit 92f0eced34
44 changed files with 3996 additions and 684 deletions
@@ -824,6 +824,78 @@ describe('GitLab data routes', () => {
expect(response.body).toEqual({ error: 'Your GitLab token needs the api scope to create merge requests' });
});
test('mrs/commits maps merge request commits with shortSha and summary', async () => {
scriptedFetch([
(url) => (matches(/\/merge_requests\/9\/commits\?/)(url)
? jsonResponse([
{
id: 'abc123def456',
short_id: 'abc123d',
title: 'Add the API',
message: 'Add the API\n\nAdds the public API',
author_name: 'Alice Example',
author_email: 'alice@example.com',
committed_date: '2026-01-01T10:00:00Z',
parent_ids: ['parent-one'],
},
])
: null),
]);
const app = createApp();
const response = await request(app).get('/api/gitlab/mrs/commits?directory=%2Ftmp%2Fwork&number=9');
expect(response.status).toBe(200);
expect(response.body).toMatchObject({
connected: true,
repo: { namespace: 'group', project: 'sub' },
commits: [
{
sha: 'abc123def456',
shortSha: 'abc123d',
message: 'Add the API\n\nAdds the public API',
summary: 'Add the API',
authorName: 'Alice Example',
committedAt: '2026-01-01T10:00:00Z',
parents: ['parent-one'],
},
],
});
});
test('mrs/timeline keeps only system notes and infers event types', async () => {
scriptedFetch([
(url) => (matches(/\/merge_requests\/9\/notes\?/)(url)
? jsonResponse([
{ id: 1, body: 'alice merged changes', system: true, author: { id: 42, username: 'alice' }, created_at: '2026-01-02T11:00:00Z' },
{ id: 2, body: 'Looks good to me', system: false, author: { id: 42, username: 'alice' }, created_at: '2026-01-02T12:00:00Z' },
{ id: 3, body: 'removed label bug', system: true, author: { id: 1, username: 'system' }, created_at: '2026-01-02T13:00:00Z' },
])
: null),
]);
const app = createApp();
const response = await request(app).get('/api/gitlab/mrs/timeline?directory=%2Ftmp%2Fwork&number=9');
expect(response.status).toBe(200);
expect(response.body).toMatchObject({
connected: true,
events: [
{ id: '1', type: 'merged', body: 'alice merged changes', author: { username: 'alice', id: 42 }, createdAt: '2026-01-02T11:00:00Z' },
{ id: '3', type: 'unlabeled', body: 'removed label bug', createdAt: '2026-01-02T13:00:00Z' },
],
});
});
test('mrs/commits and mrs/timeline report connected:false when not authenticated', async () => {
clearGitLabAuth();
const app = createApp();
const commits = await request(app).get('/api/gitlab/mrs/commits?directory=%2Ftmp%2Fwork&number=9');
const timeline = await request(app).get('/api/gitlab/mrs/timeline?directory=%2Ftmp%2Fwork&number=9');
expect(commits.body).toMatchObject({ connected: false, commits: [] });
expect(timeline.body).toMatchObject({ connected: false, events: [] });
});
test('data routes surface a 503 when GitLab rate limits', async () => {
scriptedFetch([(url) => (matches(/\/issues\?/)(url) ? jsonResponse({}, { status: 429, headers: { 'retry-after': '30' } }) : null)]);