diff --git a/src/handler-dispatcher.test.ts b/src/handler-dispatcher.test.ts index 95833d2..2559d98 100644 --- a/src/handler-dispatcher.test.ts +++ b/src/handler-dispatcher.test.ts @@ -113,6 +113,18 @@ describe("HandlerDispatcher", () => { expect(coder.getCoderUserByGitHubId).toHaveBeenCalledWith(67890); }); + test("resolves coder user from sender (assigner), not issue author", async () => { + // senderId represents the person who assigned the bot, not the issue author + const ctx = { + ...createTaskContext, + senderLogin: "org-maintainer", + senderId: 99999, + }; + const result = makeDispatchedResult("create_task", ctx); + await dispatcher.dispatch(result); + expect(coder.getCoderUserByGitHubId).toHaveBeenCalledWith(99999); + }); + test("creates a task and returns ActionOutputs", async () => { coder.getTask.mockResolvedValue(null); coder.createTask.mockResolvedValue(mockTask); diff --git a/src/handlers/create-task.test.ts b/src/handlers/create-task.test.ts index f1f558a..02ae8f5 100644 --- a/src/handlers/create-task.test.ts +++ b/src/handlers/create-task.test.ts @@ -80,6 +80,63 @@ describe("CreateTaskHandler", () => { expect(coder.createTask).not.toHaveBeenCalled(); }); + // Permission check must use the sender (assigner), not the issue author. + // This covers the case where an external user without write access creates + // the issue and a maintainer with write access assigns the bot. + test("checks permission for senderLogin (the assigner)", async () => { + github.checkActorPermission.mockResolvedValue(true); + coder.getTask.mockResolvedValue(null); + coder.createTask.mockResolvedValue(mockTask); + + const ctx = { + ...issueContext, + senderLogin: "maintainer-who-assigned", + }; + const handler = new CreateTaskHandler( + coder, + github as unknown as import("../github-client").GitHubClient, + baseInputs, + ctx, + logger, + ); + await handler.run(); + + expect(github.checkActorPermission).toHaveBeenCalledWith( + ctx.owner, + ctx.repo, + "maintainer-who-assigned", + ); + }); + + test("creates task when assigner has write access regardless of issue author", async () => { + github.checkActorPermission.mockResolvedValue(true); + coder.getTask.mockResolvedValue(null); + coder.createTask.mockResolvedValue(mockTask); + + // senderLogin is the maintainer who assigned the bot, not the issue author + const ctx = { + ...issueContext, + senderLogin: "org-maintainer", + }; + const handler = new CreateTaskHandler( + coder, + github as unknown as import("../github-client").GitHubClient, + baseInputs, + ctx, + logger, + ); + const result = await handler.run(); + + expect(result.skipped).toBe(false); + expect(coder.createTask).toHaveBeenCalledTimes(1); + // Verify permission was checked for the assigner, not anyone else + expect(github.checkActorPermission).toHaveBeenCalledWith( + "xmtp", + "libxmtp", + "org-maintainer", + ); + }); + // AC #4: Issue URL appended to prompt test("appends issue URL to prompt", async () => { github.checkActorPermission.mockResolvedValue(true); diff --git a/src/webhook-router.test.ts b/src/webhook-router.test.ts index 26f24ce..b554309 100644 --- a/src/webhook-router.test.ts +++ b/src/webhook-router.test.ts @@ -75,6 +75,29 @@ describe("WebhookRouter", () => { expect(ctx.senderId).toBe(65710); }); + test("issues.assigned extracts senderLogin from sender, not issue author", async () => { + // Simulate: external user created the issue, maintainer assigned the bot + const payload = { + ...issuesAssigned, + issue: { + ...issuesAssigned.issue, + user: { login: "external-user", id: 11111 }, + }, + sender: { login: "org-maintainer", id: 22222 }, + }; + const result = await router.handleWebhook( + "issues", + "delivery-001b", + payload, + ); + + expect(result.dispatched).toBe(true); + if (!result.dispatched) throw new Error("expected dispatched"); + const ctx = result.context as CreateTaskContext; + expect(ctx.senderLogin).toBe("org-maintainer"); + expect(ctx.senderId).toBe(22222); + }); + test("issues.assigned with non-matching assignee login → skipped", async () => { const payload = { ...issuesAssigned,