Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6b75846
feat(webhook): trigger createDependentPR job when PR is labeled\n\n- …
Dec 12, 2025
4d6081e
Implement createDependentPR worker orchestration and webhook handler …
Dec 12, 2025
c9237ea
Add handler test for autoResolveIssue workflow
youngchingjui Dec 12, 2025
01889ec
Add tests for auto resolve issue webhook handler
youngchingjui Dec 12, 2025
68169ef
Add GitHub webhook auto-resolve issue test
youngchingjui Dec 12, 2025
8c483dd
fixes tests
youngchingjui Dec 12, 2025
f9519e4
Add test for GitHub webhook auto resolve label handling (#1406)
youngchingjui Dec 12, 2025
d8a489b
Merge branch 'feature/trigger-workflow-on-pr-label' into codex/add-te…
youngchingjui Dec 12, 2025
b088e57
cleanup
youngchingjui Dec 14, 2025
93a522d
passes
youngchingjui Dec 14, 2025
e9ac049
Add tests for auto resolve issue webhook handler (#1405)
youngchingjui Dec 14, 2025
35bf454
Merge branch 'feature/trigger-workflow-on-pr-label' into codex/add-je…
youngchingjui Dec 14, 2025
625c6a1
prettier and cleanup
youngchingjui Dec 14, 2025
6e87c97
Merge remote-tracking branch 'origin/feature/trigger-workflow-on-pr-l…
youngchingjui Dec 14, 2025
30eebce
adds some tests (#1407)
youngchingjui Dec 14, 2025
f55f34a
Address review feedback for createDependentPR flow\n\n- execInContain…
Dec 15, 2025
f58762a
added check
youngchingjui Dec 15, 2025
67efc21
added error check
youngchingjui Dec 15, 2025
7beb046
Refactor workflow worker connections and enhance graceful shutdown pr…
youngchingjui Dec 15, 2025
44c96ab
Add git checkout commit adapter and update createDependentPR workflow
youngchingjui Dec 15, 2025
3a110a4
fix import
youngchingjui Dec 15, 2025
aa1d3ea
cleanup logic
youngchingjui Dec 15, 2025
b7c59e4
add try catch
youngchingjui Dec 15, 2025
5637ec2
add try catch
youngchingjui Dec 15, 2025
6b0dcad
use argv
youngchingjui Dec 15, 2025
a227d5a
cleanup
youngchingjui Dec 15, 2025
89a2fe5
Merge branch 'main' into feature/trigger-workflow-on-pr-label-followu…
youngchingjui Dec 16, 2025
4e879a5
Merge branch 'main' into feature/trigger-workflow-on-pr-label-followu…
youngchingjui Dec 17, 2025
c363662
Merge branch 'main' into feature/trigger-workflow-on-pr-label-followu…
youngchingjui Dec 29, 2025
3307c31
OK I genearlly think all of these changes are OK. let's keep moving
youngchingjui Dec 29, 2025
ac7d23e
update imports
youngchingjui Dec 29, 2025
e4682c1
Merge branch 'main' into feature/trigger-workflow-on-pr-label-followu…
youngchingjui Dec 30, 2025
f950848
Merge branch 'main' into feature/trigger-workflow-on-pr-label-followu…
youngchingjui Dec 30, 2025
a8e5ad4
Merge branch 'main' into feature/trigger-workflow-on-pr-label-followu…
youngchingjui Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions __tests__/api/webhook/github.route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,34 @@ jest.mock(
jest.mock("@/lib/webhook/github/handlers/issue/label.resolve.handler", () => ({
handleIssueLabelResolve: jest.fn(),
}))
jest.mock("@/lib/webhook/github/handlers/pullRequest/closed.removeContainer.handler", () => ({
handlePullRequestClosedRemoveContainer: jest.fn(),
}))
jest.mock("@/lib/webhook/github/handlers/pullRequest/label.createDependentPR.handler", () => ({
handlePullRequestLabelCreateDependentPR: jest.fn(),
}))
jest.mock("@/lib/webhook/github/handlers/repository/edited.revalidateRepoCache.handler", () => ({
handleRepositoryEditedRevalidate: jest.fn(),
}))
jest.mock(
"@/lib/webhook/github/handlers/pullRequest/closed.removeContainer.handler",
() => ({
handlePullRequestClosedRemoveContainer: jest.fn(),
})
)
jest.mock(
"@/lib/webhook/github/handlers/pullRequest/label.createDependentPR.handler",
() => ({
handlePullRequestLabelCreateDependentPR: jest.fn(),
})
)
jest.mock(
"@/lib/webhook/github/handlers/repository/edited.revalidateRepoCache.handler",
() => ({
handleRepositoryEditedRevalidate: jest.fn(),
})
)
import { POST } from "@/app/api/webhook/github/route"
import { handleIssueLabelAutoResolve } from "@/lib/webhook/github/handlers/issue/label.autoResolveIssue.handler"
import { handlePullRequestLabelCreateDependentPR } from "@/lib/webhook/github/handlers/pullRequest/label.createDependentPR.handler"

jest.mock("@/lib/webhook/github/handlers/issue/label.autoResolveIssue.handler", () => ({
handleIssueLabelAutoResolve: jest.fn(),
}))
jest.mock(
"@/lib/webhook/github/handlers/issue/label.autoResolveIssue.handler",
() => ({
handleIssueLabelAutoResolve: jest.fn(),
})
)

describe("POST /api/webhook/github", () => {
const secret = "test-secret"
Expand All @@ -55,7 +67,8 @@ describe("POST /api/webhook/github", () => {

const rawBody = Buffer.from(JSON.stringify(payload))
const signature =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex")

const headers = new Headers({
"x-hub-signature-256": signature,
Expand All @@ -75,7 +88,9 @@ describe("POST /api/webhook/github", () => {
const callArgs = jest.mocked(handleIssueLabelAutoResolve).mock.calls[0]?.[0]
expect(callArgs).toBeDefined()
expect(callArgs.installationId).toBe(String(payload.installation.id))
expect(callArgs.payload.repository.full_name).toBe(payload.repository.full_name)
expect(callArgs.payload.repository.full_name).toBe(
payload.repository.full_name
)
expect(callArgs.payload.issue.number).toBe(payload.issue.number)
expect(callArgs.payload.sender.login).toBe(payload.sender.login)
})
Expand All @@ -93,7 +108,8 @@ describe("POST /api/webhook/github", () => {

const rawBody = Buffer.from(JSON.stringify(payload))
const signature =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex")

const headers = new Headers({
"x-hub-signature-256": signature,
Expand All @@ -110,11 +126,12 @@ describe("POST /api/webhook/github", () => {
expect(response.status).toBe(200)

expect(handlePullRequestLabelCreateDependentPR).toHaveBeenCalledTimes(1)
const callArgs = jest.mocked(handlePullRequestLabelCreateDependentPR).mock.calls[0]?.[0]
const callArgs = jest.mocked(handlePullRequestLabelCreateDependentPR).mock
.calls[0]?.[0]
expect(callArgs).toBeDefined()
expect(callArgs.installationId).toBe(String(payload.installation.id))
expect(callArgs.payload.number).toBe(payload.number)
expect(callArgs.payload.label?.name).toBe(payload.label.name)
expect(callArgs.payload.sender?.login).toBe(payload.sender.login)
})
})
})
88 changes: 88 additions & 0 deletions __tests__/apps/workers/handler.autoResolveIssue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Mock @octokit packages to prevent ES module issues
jest.mock("@octokit/auth-app", () => ({
createAppAuth: jest.fn(() => () => Promise.resolve({ token: "fake-token" })),
}))

jest.mock("@octokit/auth-oauth-user", () => ({
createOAuthUserAuth: jest.fn(
() => () => Promise.resolve({ token: "fake-token" })
),
}))

jest.mock("@octokit/graphql", () => ({
graphql: jest.fn(),
}))

jest.mock("@octokit/rest", () => ({
Octokit: jest.fn().mockImplementation(() => ({
rest: {
apps: { getInstallation: jest.fn() },
repos: { get: jest.fn() },
},
})),
}))

jest.mock("octokit", () => ({
App: jest.fn().mockImplementation(() => ({
getInstallationOctokit: jest.fn(),
})),
}))

jest.mock(
"apps/workers/workflow-workers/src/orchestrators/autoResolveIssue",
() => ({
autoResolveIssue: jest.fn(),
})
)

jest.mock("apps/workers/workflow-workers/src/helper", () => ({
publishJobStatus: jest.fn(),
}))

import { handler } from "apps/workers/workflow-workers/src/handler"
import { publishJobStatus } from "apps/workers/workflow-workers/src/helper"
import { autoResolveIssue } from "apps/workers/workflow-workers/src/orchestrators/autoResolveIssue"
import type { Job } from "bullmq"

const mockAutoResolveIssue = jest.mocked(autoResolveIssue)
const mockPublishJobStatus = jest.mocked(publishJobStatus)

describe("handler - autoResolveIssue", () => {
beforeEach(() => {
jest.clearAllMocks()
})

it("routes autoResolveIssue jobs and publishes status updates", async () => {
const messages = [
{ role: "assistant" as const, content: "first message" },
{ role: "assistant" as const, content: "second message" },
]
mockAutoResolveIssue.mockResolvedValue(messages)

const job = {
id: "job-123",
name: "autoResolveIssue",
data: {
repoFullName: "owner/repo",
issueNumber: 42,
branch: "feature-branch",
githubLogin: "octocat",
githubInstallationId: "install-1",
},
}

const result = await handler(job as Job)

expect(mockAutoResolveIssue).toHaveBeenCalledWith(job.id, job.data)
expect(result).toBe("first message\nsecond message")
expect(mockPublishJobStatus).toHaveBeenCalledWith(job.id, "Parsing job")
expect(mockPublishJobStatus).toHaveBeenCalledWith(
job.id,
"Job: Auto resolve issue"
)
expect(mockPublishJobStatus).toHaveBeenCalledWith(
job.id,
"Completed: first message\nsecond message"
)
})
})
15 changes: 12 additions & 3 deletions __tests__/config/jest.config.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ const baseConfig: Config = {
moduleNameMapper: {
"^@/components/(.*)$": "<rootDir>/components/$1",
"^@/styles/(.*)$": "<rootDir>/styles/$1",
"^@/lib/(.*)$": "<rootDir>/lib/$1",
"^@/lib/(.*)$": "<rootDir>/lib/$1",
"^@/__tests__/(.*)$": "<rootDir>/__tests__/$1",
"^@/shared/(.*)$": "<rootDir>/shared/src/$1",
"^@shared/(.*)$": "<rootDir>/shared/src/$1",
"^shared/(.*)$": "<rootDir>/shared/src/$1",
"^apps/(.*)$": "<rootDir>/apps/$1",
"^@workers/(.*)$": "<rootDir>/apps/workers/src/$1",
"^@/(adapters|entities|ports|providers|services|ui|usecases|utils)(/.*)?$":
"<rootDir>/shared/src/$1$2",
"^@/adapters/(.*)$": "<rootDir>/shared/src/adapters/$1",
"^@/entities/(.*)$": "<rootDir>/shared/src/entities/$1",
"^@/ports/(.*)$": "<rootDir>/shared/src/ports/$1",
"^@/(.*)$": "<rootDir>/$1",
},
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
transformIgnorePatterns: [
"node_modules/(?!(shared|@octokit|universal-user-agent)/)"
],
coveragePathIgnorePatterns: ["/node_modules/", "/.next/", "/coverage/"],
rootDir: "../..",
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { handlePullRequestLabelCreateDependentPR } from "@/lib/webhook/github/handlers/pullRequest/label.createDependentPR.handler"
import type { PullRequestPayload } from "@/lib/webhook/github/types"
import { addJob } from "@/shared/services/job"

describe("handlePullRequestLabelCreateDependentPR (noop)", () => {
jest.mock("@/shared/services/job", () => ({
addJob: jest.fn().mockResolvedValue("job-id-123"),
}))

describe("handlePullRequestLabelCreateDependentPR", () => {
const installationId = "123456"

function makePayload(overrides: Partial<PullRequestPayload> = {}): PullRequestPayload {
beforeEach(() => {
jest.clearAllMocks()
})

function makePayload(
overrides: Partial<PullRequestPayload> = {}
): PullRequestPayload {
return {
action: "labeled",
number: 42,
Expand All @@ -17,15 +28,47 @@ describe("handlePullRequestLabelCreateDependentPR (noop)", () => {
}
}

beforeEach(() => {
jest.clearAllMocks()
it("enqueues a createDependentPR job with expected payload", async () => {
const payload = makePayload({ number: 100 })

process.env.REDIS_URL = "redis://localhost:6379"

await handlePullRequestLabelCreateDependentPR({ payload, installationId })

const mockedAddJob = jest.mocked(addJob)
expect(mockedAddJob).toHaveBeenCalledTimes(1)
const [queueName, jobEvent, _opts, redisUrl] = mockedAddJob.mock.calls[0]
expect(queueName).toBe("workflow-jobs")
expect(jobEvent.name).toBe("createDependentPR")
expect(jobEvent.data).toEqual({
repoFullName: "owner/repo",
pullNumber: 100,
githubLogin: "octocat",
githubInstallationId: installationId,
})
expect(redisUrl).toBe("redis://localhost:6379")
})

it("handles valid payload correctly", async () => {
process.env.REDIS_URL = "redis://localhost:6379"
const payload = makePayload({ number: 42 })

const result = await handlePullRequestLabelCreateDependentPR({
payload,
installationId,
})

expect(result.status).toBe("noop")
})

it("logs a noop message with expected context", async () => {
const logSpy = jest.spyOn(console, "log").mockImplementation(() => {})

const payload = makePayload({ number: 100 })
const result = await handlePullRequestLabelCreateDependentPR({ payload, installationId })
const result = await handlePullRequestLabelCreateDependentPR({
payload,
installationId,
})

expect(result).toEqual({
status: "noop",
Expand All @@ -44,13 +87,28 @@ describe("handlePullRequestLabelCreateDependentPR (noop)", () => {
logSpy.mockRestore()
})

it("throws if required fields are missing", async () => {
it("throws if repository fields are missing", async () => {
await expect(
handlePullRequestLabelCreateDependentPR({
payload: makePayload({ repository: { name: "", owner: { login: "" } } }),
payload: makePayload({
repository: { name: "", owner: { login: "" } },
}),
installationId,
})
).rejects.toThrow()
})
})

it("throws if REDIS_URL is not set", async () => {
const original = process.env.REDIS_URL
delete process.env.REDIS_URL

await expect(
handlePullRequestLabelCreateDependentPR({
payload: makePayload(),
installationId,
})
).rejects.toThrow("REDIS_URL is not set")

if (original) process.env.REDIS_URL = original
})
})
Loading
Loading