Skip to content

Commit 31a308a

Browse files
authored
Merge pull request #214 from coji/feat/review-stacks-closed-released
feat: 個人 workload 画面の週間アクティビティ表示を改善
2 parents 3bd7922 + 832a19a commit 31a308a

3 files changed

Lines changed: 497 additions & 130 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { mkdirSync, writeFileSync } from 'node:fs'
2+
import { tmpdir } from 'node:os'
3+
import path from 'node:path'
4+
import {
5+
afterAll,
6+
afterEach,
7+
beforeEach,
8+
describe,
9+
expect,
10+
test,
11+
vi,
12+
} from 'vitest'
13+
import { setupTenantSchema } from '~/test/setup-tenant-db'
14+
15+
const testDir = path.join(tmpdir(), `workload-login-queries-test-${Date.now()}`)
16+
mkdirSync(testDir, { recursive: true })
17+
const testDbPath = path.join(testDir, 'data.db')
18+
writeFileSync(testDbPath, '')
19+
20+
vi.stubEnv('NODE_ENV', 'production')
21+
vi.stubEnv('DATABASE_URL', `file://${testDbPath}`)
22+
23+
const { closeTenantDb, getTenantDb } =
24+
await import('~/app/services/tenant-db.server')
25+
const { getClosedPRs } = await import('./queries.server')
26+
type OrganizationId = import('~/app/types/organization').OrganizationId
27+
const toOrgId = (s: string) => s as OrganizationId
28+
29+
let testCounter = 0
30+
function createFreshOrg(): OrganizationId {
31+
testCounter++
32+
const orgId = `test-workload-login-${Date.now()}-${testCounter}`
33+
const dbPath = path.join(testDir, `tenant_${orgId}.db`)
34+
setupTenantSchema(dbPath)
35+
return toOrgId(orgId)
36+
}
37+
38+
async function seedRepository(orgId: OrganizationId) {
39+
const tenantDb = getTenantDb(orgId)
40+
41+
await tenantDb
42+
.insertInto('integrations')
43+
.values({
44+
id: 'integration-1',
45+
provider: 'github',
46+
method: 'token',
47+
privateToken: null,
48+
})
49+
.execute()
50+
51+
await tenantDb
52+
.insertInto('repositories')
53+
.values({
54+
id: 'repo-1',
55+
integrationId: 'integration-1',
56+
provider: 'github',
57+
owner: 'acme',
58+
repo: 'widget',
59+
releaseDetectionMethod: 'branch',
60+
releaseDetectionKey: 'production',
61+
updatedAt: '2026-03-01T00:00:00Z',
62+
})
63+
.execute()
64+
}
65+
66+
async function insertPullRequest(
67+
orgId: OrganizationId,
68+
overrides: Partial<{
69+
repo: string
70+
number: number
71+
sourceBranch: string
72+
targetBranch: string
73+
state: 'open' | 'closed' | 'merged'
74+
author: string
75+
title: string
76+
url: string
77+
pullRequestCreatedAt: string
78+
mergedAt: string | null
79+
closedAt: string | null
80+
releasedAt: string | null
81+
repositoryId: string
82+
complexity: string | null
83+
}> = {},
84+
) {
85+
const tenantDb = getTenantDb(orgId)
86+
await tenantDb
87+
.insertInto('pullRequests')
88+
.values({
89+
repo: 'widget',
90+
number: 1,
91+
sourceBranch: 'feature/test',
92+
targetBranch: 'main',
93+
state: 'closed',
94+
author: 'alice',
95+
title: 'Test PR',
96+
url: 'https://github.com/acme/widget/pull/1',
97+
firstCommittedAt: null,
98+
pullRequestCreatedAt: '2026-03-10T00:00:00Z',
99+
firstReviewedAt: null,
100+
mergedAt: null,
101+
closedAt: '2026-03-12T00:00:00Z',
102+
releasedAt: null,
103+
codingTime: null,
104+
pickupTime: null,
105+
reviewTime: null,
106+
deployTime: null,
107+
totalTime: null,
108+
repositoryId: 'repo-1',
109+
updatedAt: null,
110+
additions: null,
111+
deletions: null,
112+
changedFiles: null,
113+
complexity: 'M',
114+
complexityReason: null,
115+
riskAreas: null,
116+
classifiedAt: null,
117+
classifierModel: null,
118+
...overrides,
119+
})
120+
.execute()
121+
}
122+
123+
describe('workload member queries', () => {
124+
let orgId: OrganizationId
125+
126+
afterAll(() => {
127+
vi.unstubAllEnvs()
128+
})
129+
130+
beforeEach(async () => {
131+
orgId = createFreshOrg()
132+
await seedRepository(orgId)
133+
})
134+
135+
afterEach(async () => {
136+
await closeTenantDb(orgId)
137+
})
138+
139+
test('getClosedPRs returns only non-merged PRs closed in range for the author', async () => {
140+
await insertPullRequest(orgId, {
141+
number: 1,
142+
author: 'alice',
143+
closedAt: '2026-03-12T03:00:00Z',
144+
mergedAt: null,
145+
})
146+
await insertPullRequest(orgId, {
147+
number: 2,
148+
author: 'alice',
149+
closedAt: '2026-03-13T03:00:00Z',
150+
mergedAt: null,
151+
url: 'https://github.com/acme/widget/pull/2',
152+
title: 'Second in-range PR',
153+
})
154+
await insertPullRequest(orgId, {
155+
number: 5,
156+
author: 'alice',
157+
closedAt: '2026-03-13T04:00:00Z',
158+
mergedAt: '2026-03-13T02:00:00Z',
159+
releasedAt: '2026-03-14T02:00:00Z',
160+
url: 'https://github.com/acme/widget/pull/5',
161+
title: 'Merged PR should be excluded',
162+
})
163+
await insertPullRequest(orgId, {
164+
number: 3,
165+
author: 'bob',
166+
closedAt: '2026-03-12T04:00:00Z',
167+
mergedAt: null,
168+
url: 'https://github.com/acme/widget/pull/3',
169+
title: 'Other author PR',
170+
})
171+
await insertPullRequest(orgId, {
172+
number: 4,
173+
author: 'alice',
174+
closedAt: '2026-03-20T03:00:00Z',
175+
mergedAt: null,
176+
url: 'https://github.com/acme/widget/pull/4',
177+
title: 'Out of range PR',
178+
})
179+
180+
const prs = await getClosedPRs(
181+
orgId,
182+
'ALICE',
183+
'2026-03-10T00:00:00Z',
184+
'2026-03-16T23:59:59Z',
185+
)
186+
187+
expect(prs).toHaveLength(2)
188+
expect(prs[0].number).toBe(1)
189+
expect(prs[1].number).toBe(2)
190+
expect(prs[0].closedAt).toBe('2026-03-12T03:00:00Z')
191+
expect(prs[1].closedAt).toBe('2026-03-13T03:00:00Z')
192+
expect(prs[0].closedAt! < prs[1].closedAt!).toBe(true)
193+
})
194+
})

app/routes/$orgSlug/workload/$login/+functions/queries.server.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,35 @@ export const getMergedPRs = async (
7979
.execute()
8080
}
8181

82+
export const getClosedPRs = async (
83+
organizationId: OrganizationId,
84+
login: string,
85+
from: string,
86+
to: string,
87+
) => {
88+
const tenantDb = getTenantDb(organizationId)
89+
return await tenantDb
90+
.selectFrom('pullRequests')
91+
.where((eb) =>
92+
eb(eb.fn('lower', ['pullRequests.author']), '=', login.toLowerCase()),
93+
)
94+
.where('closedAt', '>=', from)
95+
.where('closedAt', '<=', to)
96+
.where('mergedAt', 'is', null)
97+
.select([
98+
'number',
99+
'repositoryId',
100+
'repo',
101+
'title',
102+
'url',
103+
'closedAt',
104+
'pullRequestCreatedAt',
105+
'complexity',
106+
])
107+
.orderBy('closedAt', 'asc')
108+
.execute()
109+
}
110+
82111
// Returns all review submissions (including multiple rounds on the same PR).
83112
// Each round is a distinct action for the "what did they do this week" view.
84113
export const getReviewsSubmitted = async (

0 commit comments

Comments
 (0)