Skip to content
Open
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
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ These instructions have been tested on a clean machine to ensure a reliable setu

> **Note:** If the repository already includes `.env.example`, you only need to copy it to `.env`. Creating a new `.env.example` is only necessary if the file is missing.


5. **Start the development server:**
```bash
bun dev
Expand Down
78 changes: 78 additions & 0 deletions src/hooks/useGitHubIssues.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useGitHubIssues } from './useGitHubIssues';
import { useAppStore } from '../store/appStore';
import * as github from '../lib/api/github';
import { historyService } from '../lib/history/historyService';

describe('useGitHubIssues', () => {
beforeEach(() => {
vi.restoreAllMocks();
useAppStore.setState({
repoInput: 'owner/repo',
maxIssues: 50,
issues: [],
analyses: new Map(),
});

vi.spyOn(github, 'searchIssues').mockResolvedValue([
{
number: 1,
title: 'A',
user: { login: 'u' },
labels: [],
assignees: [],
comments: 0,
},
] as any);
vi.spyOn(github, 'fetchAllIssueDetails').mockImplementation(
async (_owner: string, _repo: string, issues: any[]) => issues,
);
vi.spyOn(github, 'getRateLimitInfo').mockReturnValue({
remaining: 5000,
limit: 5000,
reset: 0,
});
vi.spyOn(historyService, 'saveToHistory').mockResolvedValue(undefined);
vi.spyOn(historyService, 'getAllHistory').mockResolvedValue([]);
});

it('preserves existing analyses when saving refreshed issues to history', async () => {
vi.spyOn(historyService, 'getHistoryEntry').mockResolvedValue({
valid: true,
data: {
key: 'owner/repo',
issues: [],
analyses: new Map([[1, { doability_score: 90 }]]) as any,
metadata: {} as any,
fetchedAt: Date.now(),
},
});

const { result } = renderHook(() => useGitHubIssues());

await act(async () => {
await result.current.fetchIssues(true);
});

expect(historyService.saveToHistory).toHaveBeenCalledTimes(1);
const savedAnalyses = (historyService.saveToHistory as any).mock.calls[0][3];
expect(savedAnalyses.get(1)).toEqual({ doability_score: 90 });
});

it('saves an empty analyses map when no history entry exists yet', async () => {
vi.spyOn(historyService, 'getHistoryEntry').mockResolvedValue({
valid: false,
reason: 'No history found',
});

const { result } = renderHook(() => useGitHubIssues());

await act(async () => {
await result.current.fetchIssues(true);
});

const savedAnalyses = (historyService.saveToHistory as any).mock.calls[0][3];
expect(savedAnalyses.size).toBe(0);
});
});
12 changes: 9 additions & 3 deletions src/hooks/useGitHubIssues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useAppStore } from '../store/appStore';
import { searchIssues, fetchAllIssueDetails, getRateLimitInfo } from '../lib/api/github';
import { parseRepoInput } from '../lib/utils/validators';
import { historyService } from '../lib/history/historyService';
import type { Issue } from '../lib/types';
import type { Issue, AnalysisResult } from '../lib/types';

export function useGitHubIssues() {
const {
Expand Down Expand Up @@ -109,9 +109,15 @@ export function useGitHubIssues() {
setIssues(detailedIssues);
addLog(`✓ All ${detailedIssues.length} issues fetched with details`, 'success');

// Save to history
// Save to history, preserving any analyses already stored so a
// refresh does not wipe them and force a full re-analysis
addLog('Saving to history...', 'info');
await historyService.saveToHistory(owner, repo, detailedIssues, new Map());
const existing = await historyService.getHistoryEntry(owner, repo);
const existingAnalyses =
existing.valid && existing.data
? existing.data.analyses
: new Map<number, AnalysisResult>();
await historyService.saveToHistory(owner, repo, detailedIssues, existingAnalyses);
await loadHistory();
addLog('✓ Data saved to history', 'success');

Expand Down
62 changes: 28 additions & 34 deletions src/lib/api/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,21 +137,19 @@ export async function searchIssues(

const issues = data.items
.filter((item) => !item.pull_request)
.map(
(item): Issue => ({
number: item.number,
title: item.title,
body: item.body,
user: item.user,
labels: item.labels,
assignees: item.assignees,
comments_count: item.comments,
created_at: item.created_at,
updated_at: item.updated_at,
html_url: item.html_url,
state: item.state,
}),
);
.map((item): Issue => ({
number: item.number,
title: item.title,
body: item.body,
user: item.user,
labels: item.labels,
assignees: item.assignees,
comments_count: item.comments,
created_at: item.created_at,
updated_at: item.updated_at,
html_url: item.html_url,
state: item.state,
}));

allIssues.push(...issues);

Expand Down Expand Up @@ -190,15 +188,13 @@ export async function fetchIssueComments(

const data = await response.json();

return data.map(
(c: Record<string, unknown>): Comment => ({
id: c.id as number,
user: c.user as Comment['user'],
body: c.body as string,
created_at: c.created_at as string,
updated_at: c.updated_at as string,
}),
);
return data.map((c: Record<string, unknown>): Comment => ({
id: c.id as number,
user: c.user as Comment['user'],
body: c.body as string,
created_at: c.created_at as string,
updated_at: c.updated_at as string,
}));
}

export async function fetchIssueTimeline(
Expand All @@ -217,16 +213,14 @@ export async function fetchIssueTimeline(

const data: Record<string, unknown>[] = await response.json();

return data.map(
(e): TimelineEvent => ({
event: e.event as string,
created_at: e.created_at as string,
actor: e.actor as TimelineEvent['actor'],
source: e.source as TimelineEvent['source'],
commit_id: e.commit_id as string | undefined,
label: e.label as TimelineEvent['label'],
}),
);
return data.map((e): TimelineEvent => ({
event: e.event as string,
created_at: e.created_at as string,
actor: e.actor as TimelineEvent['actor'],
source: e.source as TimelineEvent['source'],
commit_id: e.commit_id as string | undefined,
label: e.label as TimelineEvent['label'],
}));
} catch {
return [];
}
Expand Down
Loading