diff --git a/src/pages/AnalyticsPage.jsx b/src/pages/AnalyticsPage.jsx
index 5b514a9..d5e6f3b 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 7a209f6..b32c05c 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 0000000..d3ac289
--- /dev/null
+++ b/src/pages/RepositoryMetrics.test.jsx
@@ -0,0 +1,97 @@
+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')
+ expect(screen.getByText((_, element) => element?.textContent === '1 merged ยท 1 rejected'))
+ .toBeInTheDocument()
+ })
+
+ 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()
+ }
+ })
+})