From a129a15f619cb55a2a9ab76fc48cb84d30eaeeba Mon Sep 17 00:00:00 2001 From: Vlad Mocanu Date: Wed, 25 Feb 2026 21:43:37 +0000 Subject: [PATCH] feat(prd-6): verify workflow triggers and add assignee_trigger (#13) --- .forgejo/workflows/claude.yml | 19 ++++++++++++- prds/6-e2e-test-forgejo-triggers.md | 2 +- src/github/context.ts | 16 ++++++++++- src/github/utils/image-downloader.ts | 13 +++++++-- test/image-downloader.test.ts | 40 ++++++++++++++++++---------- 5 files changed, 71 insertions(+), 19 deletions(-) diff --git a/.forgejo/workflows/claude.yml b/.forgejo/workflows/claude.yml index 848d7cd..2646de3 100644 --- a/.forgejo/workflows/claude.yml +++ b/.forgejo/workflows/claude.yml @@ -14,10 +14,26 @@ concurrency: group: claude-${{ github.event.issue.number || github.event.pull_request.number }}-${{ github.run_id }} jobs: + # Temporary debug job to inspect Forgejo event payloads (PRD #6 E2E testing) + debug-event: + runs-on: grunt + steps: + - name: Dump event context + run: | + echo "=== Event Context ===" + echo "github.event_name: ${{ github.event_name }}" + echo "github.event.action: ${{ github.event.action }}" + echo "github.event.comment.body: ${{ github.event.comment.body }}" + echo "github.event.review.body: ${{ github.event.review.body }}" + echo "GITHUB_EVENT_NAME env: $GITHUB_EVENT_NAME" + echo "" + echo "=== Event Payload (first 200 lines) ===" + cat "$GITHUB_EVENT_PATH" | head -200 + claude: if: | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + ((github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_comment') && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && ( github.event.action == 'labeled' || @@ -42,6 +58,7 @@ jobs: bot_name: "claude" bot_email: "claude@wxs.ro" platform: "forgejo" + assignee_trigger: "claude" env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/prds/6-e2e-test-forgejo-triggers.md b/prds/6-e2e-test-forgejo-triggers.md index 060a765..834d0ac 100644 --- a/prds/6-e2e-test-forgejo-triggers.md +++ b/prds/6-e2e-test-forgejo-triggers.md @@ -24,7 +24,7 @@ Systematically exercise each untested scenario on the live Forgejo instance (git ## Milestones -- [ ] **M1: Ensure workflow covers all trigger types** +- [x] **M1: Ensure workflow covers all trigger types** - Review `.forgejo/workflows/claude.yml` to confirm all 5 trigger types are listed - Add any missing triggers to the workflow file diff --git a/src/github/context.ts b/src/github/context.ts index aca526b..07b0df0 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -60,6 +60,18 @@ const ENTITY_EVENT_NAMES = [ "pull_request_review_comment", ] as const; +/** + * Forgejo uses different event names for some webhook events. + * Normalize to GitHub-canonical names so all downstream code works unchanged. + */ +const FORGEJO_EVENT_NAME_MAP: Record = { + pull_request_comment: "pull_request_review_comment", +}; + +function normalizeEventName(eventName: string): string { + return FORGEJO_EVENT_NAME_MAP[eventName] ?? eventName; +} + const AUTOMATION_EVENT_NAMES = [ "workflow_dispatch", "repository_dispatch", @@ -132,6 +144,8 @@ export type GitHubContext = ParsedGitHubContext | AutomationContext; export function parseGitHubContext(): GitHubContext { const context = github.context; + // Normalize Forgejo event names to GitHub-canonical equivalents + const eventName = normalizeEventName(context.eventName); const commonFields = { runId: process.env.GITHUB_RUN_ID!, @@ -165,7 +179,7 @@ export function parseGitHubContext(): GitHubContext { }, }; - switch (context.eventName) { + switch (eventName) { case "issues": { const payload = context.payload as IssuesEvent; return { diff --git a/src/github/utils/image-downloader.ts b/src/github/utils/image-downloader.ts index 0b2c0c7..63922f9 100644 --- a/src/github/utils/image-downloader.ts +++ b/src/github/utils/image-downloader.ts @@ -2,6 +2,15 @@ import fs from "fs/promises"; import path from "path"; import type { Octokits } from "../api/client"; +// Mutable fs reference for cross-platform test compatibility. +// On Linux, Bun gives test and source files different fs/promises objects, +// so spyOn(fs, "mkdir") in a test file won't intercept calls here. +// Tests override these methods directly instead. +export const _fsForTest = { + mkdir: fs.mkdir.bind(fs), + writeFile: fs.writeFile.bind(fs), +}; + // Build regexes at call time, reading GITHUB_SERVER_URL from process.env // directly. The config.ts export freezes the value at module load time, // which can be wrong when the Forgejo CI runner injects its own @@ -67,7 +76,7 @@ export async function downloadCommentImages( const urlToPathMap = new Map(); const downloadsDir = "/tmp/github-images"; - await fs.mkdir(downloadsDir, { recursive: true }); + await _fsForTest.mkdir(downloadsDir, { recursive: true }); const { imageRegex, htmlImgRegex } = buildImageRegexes(); @@ -217,7 +226,7 @@ export async function downloadCommentImages( const arrayBuffer = await imageResponse.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); - await fs.writeFile(localPath, buffer); + await _fsForTest.writeFile(localPath, buffer); console.log(`✓ Saved: ${localPath}`); urlToPathMap.set(originalUrl, localPath); diff --git a/test/image-downloader.test.ts b/test/image-downloader.test.ts index 124413a..d68b63f 100644 --- a/test/image-downloader.test.ts +++ b/test/image-downloader.test.ts @@ -8,20 +8,28 @@ import { jest, setSystemTime, } from "bun:test"; -import fs from "fs/promises"; -import { downloadCommentImages } from "../src/github/utils/image-downloader"; -import type { CommentWithImages } from "../src/github/utils/image-downloader"; +import { + downloadCommentImages, + _fsForTest, + type CommentWithImages, +} from "../src/github/utils/image-downloader"; import type { Octokits } from "../src/github/api/client"; describe("downloadCommentImages", () => { let consoleLogSpy: any; let consoleWarnSpy: any; let consoleErrorSpy: any; - let fsMkdirSpy: any; - let fsWriteFileSpy: any; let fetchSpy: any; let savedServerUrl: string | undefined; + // Store originals for restoration + const origMkdir = _fsForTest.mkdir; + const origWriteFile = _fsForTest.writeFile; + + // Mock functions for fs operations + const mockMkdir = jest.fn().mockResolvedValue(undefined); + const mockWriteFile = jest.fn().mockResolvedValue(undefined); + beforeEach(() => { // Pin GITHUB_SERVER_URL so tests pass regardless of runner environment. // The image-downloader reads process.env directly at call time. @@ -33,9 +41,11 @@ describe("downloadCommentImages", () => { consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {}); consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {}); - // Spy on fs methods - fsMkdirSpy = spyOn(fs, "mkdir").mockResolvedValue(undefined); - fsWriteFileSpy = spyOn(fs, "writeFile").mockResolvedValue(undefined); + // Override _fsForTest methods directly — works cross-platform + mockMkdir.mockClear(); + mockWriteFile.mockClear(); + _fsForTest.mkdir = mockMkdir as any; + _fsForTest.writeFile = mockWriteFile as any; // Set fake system time for consistent filenames setSystemTime(new Date("2024-01-01T00:00:00.000Z")); // 1704067200000 @@ -49,11 +59,13 @@ describe("downloadCommentImages", () => { delete process.env.GITHUB_SERVER_URL; } + // Restore original fs methods + _fsForTest.mkdir = origMkdir; + _fsForTest.writeFile = origWriteFile; + consoleLogSpy.mockRestore(); consoleWarnSpy.mockRestore(); consoleErrorSpy.mockRestore(); - fsMkdirSpy.mockRestore(); - fsWriteFileSpy.mockRestore(); if (fetchSpy) fetchSpy.mockRestore(); setSystemTime(); // Reset to real time }); @@ -80,7 +92,7 @@ describe("downloadCommentImages", () => { await downloadCommentImages(mockOctokit, "owner", "repo", comments); - expect(fsMkdirSpy).toHaveBeenCalledWith("/tmp/github-images", { + expect(mockMkdir).toHaveBeenCalledWith("/tmp/github-images", { recursive: true, }); }); @@ -153,7 +165,7 @@ describe("downloadCommentImages", () => { }); expect(fetchSpy).toHaveBeenCalledWith(signedUrl); - expect(fsWriteFileSpy).toHaveBeenCalledWith( + expect(mockWriteFile).toHaveBeenCalledWith( "/tmp/github-images/image-1704067200000-0.png", Buffer.from(mockArrayBuffer), ); @@ -628,7 +640,7 @@ describe("downloadCommentImages", () => { ); // Reset for next iteration - fsWriteFileSpy.mockClear(); + mockWriteFile.mockClear(); callIndex++; } }); @@ -721,7 +733,7 @@ describe("downloadCommentImages", () => { }); expect(fetchSpy).toHaveBeenCalledWith(signedUrl); - expect(fsWriteFileSpy).toHaveBeenCalledWith( + expect(mockWriteFile).toHaveBeenCalledWith( "/tmp/github-images/image-1704067200000-0.png", Buffer.from(mockArrayBuffer), );