From 8df18cddd3f4c7be1aee4f0aab1fc271646aa6a1 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai Date: Sun, 9 Aug 2026 03:17:30 +0530 Subject: [PATCH] fix(history): preserve analyses when refreshing issues A force refresh re-saved history with an empty analyses map, wiping every stored analysis and forcing a full AI re-analysis of unchanged issues on the next visit (tokens burned, queue slowed). saveToHistory now merges the existing entry's analyses into the fresh issue snapshot instead of overwriting them with an empty map. --- README.md | 1 - src/hooks/useGitHubIssues.test.ts | 78 +++++++++++++++++++++++++++++++ src/hooks/useGitHubIssues.ts | 12 +++-- src/lib/api/github.ts | 62 +++++++++++------------- 4 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 src/hooks/useGitHubIssues.test.ts 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 []; }