diff --git a/README.md b/README.md index 07c81d5..adfb2c6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/hooks/useGitHubIssues.test.ts b/src/hooks/useGitHubIssues.test.ts new file mode 100644 index 0000000..460731e --- /dev/null +++ b/src/hooks/useGitHubIssues.test.ts @@ -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); + }); +}); diff --git a/src/hooks/useGitHubIssues.ts b/src/hooks/useGitHubIssues.ts index d1757a5..2dc0cb5 100644 --- a/src/hooks/useGitHubIssues.ts +++ b/src/hooks/useGitHubIssues.ts @@ -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 { @@ -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(); + await historyService.saveToHistory(owner, repo, detailedIssues, existingAnalyses); await loadHistory(); addLog('✓ Data saved to history', 'success'); diff --git a/src/lib/api/github.ts b/src/lib/api/github.ts index 3816c83..d1cb00c 100644 --- a/src/lib/api/github.ts +++ b/src/lib/api/github.ts @@ -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); @@ -190,15 +188,13 @@ export async function fetchIssueComments( const data = await response.json(); - return data.map( - (c: Record): 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): 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( @@ -217,16 +213,14 @@ export async function fetchIssueTimeline( const data: Record[] = 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 []; }