From ed311cd3f3753f9f2d61e58936103b4f8b925d0a Mon Sep 17 00:00:00 2001
From: Yash Anand <164618230+YashAnand69@users.noreply.github.com>
Date: Sun, 4 Oct 2026 15:51:38 +0530
Subject: [PATCH 1/2] fix: scope repository metrics by organization
---
src/pages/AnalyticsPage.jsx | 10 ++-
src/pages/GovernancePage.jsx | 2 +-
src/pages/RepositoryMetrics.test.jsx | 95 ++++++++++++++++++++++++++++
3 files changed, 100 insertions(+), 7 deletions(-)
create mode 100644 src/pages/RepositoryMetrics.test.jsx
diff --git a/src/pages/AnalyticsPage.jsx b/src/pages/AnalyticsPage.jsx
index 5b514a95..d5e6f3ba 100644
--- a/src/pages/AnalyticsPage.jsx
+++ b/src/pages/AnalyticsPage.jsx
@@ -37,8 +37,7 @@ export default function AnalyticsPage() {
const filteredIssues = useMemo(() => {
if (selectedRepo === 'All') return allIssues
- const key = Object.keys(issuesData || {}).find(k => k.split('/')[1] === selectedRepo)
- return key ? (issuesData[key] || []) : []
+ return issuesData?.[selectedRepo] || []
}, [allIssues, selectedRepo, issuesData])
const series = useMemo(() =>
@@ -48,8 +47,7 @@ export default function AnalyticsPage() {
const filteredPulls = useMemo(() => {
if (selectedRepoForAM === 'All Repositories') return Object.values(pullsData || {}).flat()
- const key = Object.keys(pullsData || {}).find(k => k.split('/')[1] === selectedRepoForAM)
- return key ? (pullsData[key] || []) : []
+ return pullsData?.[selectedRepoForAM] || []
}, [pullsData, selectedRepoForAM])
const advancedMetrics = useAdvancedMetrics(filteredPulls)
@@ -68,8 +66,8 @@ export default function AnalyticsPage() {
}
]
- const repoNames = ['All', ...Object.keys(issuesData || {}).map(k => k.split('/')[1])]
- const allRepoNames = ['All Repositories', ...Object.keys(pullsData || {}).map(k => k.split('/')[1])]
+ const repoNames = ['All', ...Object.keys(issuesData || {})]
+ const allRepoNames = ['All Repositories', ...Object.keys(pullsData || {})]
const hasData = Object.keys(issuesData || {}).length > 0
const hasPullsData = Object.keys(pullsData || {}).length > 0
const hasSeries = series.length > 0
diff --git a/src/pages/GovernancePage.jsx b/src/pages/GovernancePage.jsx
index 7a209f64..b32c05ca 100644
--- a/src/pages/GovernancePage.jsx
+++ b/src/pages/GovernancePage.jsx
@@ -174,7 +174,7 @@ export default function GovernancePage() {
Issue Resolution Rate
Resolution velocity across key repositories
{topRepos.map(r => {
- const repoIssues = allIssues.filter(i => i.repoName === r.name)
+ const repoIssues = issuesData?.[`${r.orgLogin}/${r.name}`] || []
const closed = repoIssues.filter(i => i.state === 'closed').length
const total = repoIssues.length
const rate = total ? Math.round(closed / total * 100) : null
diff --git a/src/pages/RepositoryMetrics.test.jsx b/src/pages/RepositoryMetrics.test.jsx
new file mode 100644
index 00000000..4ab07768
--- /dev/null
+++ b/src/pages/RepositoryMetrics.test.jsx
@@ -0,0 +1,95 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, within } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import AnalyticsPage from './AnalyticsPage'
+import GovernancePage from './GovernancePage'
+
+const { app } = vi.hoisted(() => ({ app: {} }))
+
+vi.mock('../context/AppContext', () => ({ useApp: () => app }))
+
+// Chart layout is not needed to exercise repository selection and its metrics.
+vi.mock('recharts', () => {
+ const EmptyChart = () => null
+ return {
+ AreaChart: ({ data }) => ,
+ ResponsiveContainer: ({ children }) => {children}
,
+ Area: EmptyChart, XAxis: EmptyChart, YAxis: EmptyChart,
+ CartesianGrid: EmptyChart, Tooltip: EmptyChart, Legend: EmptyChart,
+ PieChart: EmptyChart, Pie: EmptyChart, Cell: EmptyChart,
+ RadialBarChart: EmptyChart, RadialBar: EmptyChart, PolarAngleAxis: EmptyChart,
+ }
+})
+
+beforeEach(() => {
+ Object.assign(app, {
+ model: {
+ allRepos: [
+ { id: 1, name: 'docs', orgLogin: 'first-org', license: { key: 'mit' } },
+ { id: 2, name: 'docs', orgLogin: 'second-org', license: { key: 'mit' } },
+ { id: 3, name: 'docs', orgLogin: 'uncached-org', license: { key: 'mit' } },
+ ],
+ },
+ issuesData: {
+ 'first-org/docs': [{ id: 1, state: 'closed', created_at: '2026-01-01T00:00:00Z' }],
+ 'second-org/docs': [{ id: 2, state: 'open', created_at: '2026-02-01T00:00:00Z' }],
+ },
+ pullsData: {
+ 'first-org/docs': [{ state: 'closed', created_at: '2026-01-01T00:00:00Z', merged_at: '2026-01-03T00:00:00Z' }],
+ 'second-org/docs': [{ state: 'closed', created_at: '2026-02-01T00:00:00Z', merged_at: null }],
+ },
+ staleRepoStats: [],
+ loading: false,
+ govLoading: false,
+ advanceAnalyticsLoading: false,
+ auditComplete: true,
+ advanceAnalyticsComplete: true,
+ pat: '',
+ })
+})
+
+describe('repository metrics across organizations', () => {
+ it('selects each same-name repository independently in activity trends', async () => {
+ const user = userEvent.setup()
+ render()
+ const selector = screen.getAllByRole('combobox')[0]
+
+ expect(within(selector).getAllByRole('option').map(option => option.value))
+ .toEqual(['All', 'first-org/docs', 'second-org/docs'])
+
+ for (const [repo, month] of [['first-org/docs', '2026-01'], ['second-org/docs', '2026-02']]) {
+ await user.selectOptions(selector, repo)
+ const series = JSON.parse(screen.getAllByTestId('trend-series')[0].textContent)
+ expect(series).toHaveLength(1)
+ expect(series[0]).toMatchObject({ date: month, issues_created: 1 })
+ }
+
+ await user.selectOptions(selector, 'All')
+ expect(JSON.parse(screen.getAllByTestId('trend-series')[0].textContent)).toHaveLength(2)
+ })
+
+ it('uses the selected organization for advanced pull request metrics', async () => {
+ const user = userEvent.setup()
+ render()
+ const selector = screen.getAllByRole('combobox')[1]
+
+ expect(within(selector).getAllByRole('option').map(option => option.value))
+ .toEqual(['All Repositories', 'first-org/docs', 'second-org/docs'])
+
+ await user.selectOptions(selector, 'first-org/docs')
+ expect(screen.getByText('Based on', { exact: false })).toHaveTextContent('1 merged pull requests')
+ await user.selectOptions(selector, 'second-org/docs')
+ expect(screen.getByText('Based on', { exact: false })).toHaveTextContent('0 merged pull requests')
+ await user.selectOptions(selector, 'All Repositories')
+ expect(screen.getByText('Based on', { exact: false })).toHaveTextContent('1 merged pull requests')
+ })
+
+ it('keeps resolution rates and missing audit data scoped to their organization', () => {
+ render()
+
+ for (const [org, rate] of [['first-org', '100%'], ['second-org', '0%'], ['uncached-org', 'No data']]) {
+ const row = screen.getByText(org).parentElement.parentElement
+ expect(within(row).getByText(rate)).toBeInTheDocument()
+ }
+ })
+})
From b5248811af53492afc3213312fc36fc8c0a9495b Mon Sep 17 00:00:00 2001
From: Yash Anand <164618230+YashAnand69@users.noreply.github.com>
Date: Sun, 4 Oct 2026 16:01:30 +0530
Subject: [PATCH 2/2] test: verify both aggregate pull request totals
---
src/pages/RepositoryMetrics.test.jsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/pages/RepositoryMetrics.test.jsx b/src/pages/RepositoryMetrics.test.jsx
index 4ab07768..d3ac2890 100644
--- a/src/pages/RepositoryMetrics.test.jsx
+++ b/src/pages/RepositoryMetrics.test.jsx
@@ -82,6 +82,8 @@ describe('repository metrics across organizations', () => {
expect(screen.getByText('Based on', { exact: false })).toHaveTextContent('0 merged pull requests')
await user.selectOptions(selector, 'All Repositories')
expect(screen.getByText('Based on', { exact: false })).toHaveTextContent('1 merged pull requests')
+ expect(screen.getByText((_, element) => element?.textContent === '1 merged ยท 1 rejected'))
+ .toBeInTheDocument()
})
it('keeps resolution rates and missing audit data scoped to their organization', () => {