diff --git a/README.md b/README.md index b3cfb4d..be17208 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ This GitHub Action powers the Factory **Droid** app. It watches your pull requests for the two supported commands and runs a full Droid Exec session to help you ship faster: -* `@droid fill` — turns a bare pull request into a polished description that matches your template or our opinionated fallback. -* `@droid review` — performs an automated code review, surfaces potential bugs, and leaves inline comments directly on the diff. +- `@droid fill` — turns a bare pull request into a polished description that matches your template or our opinionated fallback. +- `@droid review` — performs an automated code review, surfaces potential bugs, and leaves inline comments directly on the diff. Everything runs inside GitHub Actions using your Factory API key, so the bot never leaves your repository and operates with the permissions you grant. @@ -18,11 +18,11 @@ Everything runs inside GitHub Actions using your Factory API key, so the bot nev ## Installation 1. **Install the Droid GitHub App** - * Install from the Factory dashboard and grant it access to the repositories where you want Droid to operate. + - Install from the Factory dashboard and grant it access to the repositories where you want Droid to operate. 2. **Create a Factory API Key** - * Generate a token at [https://app.factory.ai/settings/api-keys](https://app.factory.ai/settings/api-keys) and save it as `FACTORY_API_KEY` in your repository or organization secrets. + - Generate a token at [https://app.factory.ai/settings/api-keys](https://app.factory.ai/settings/api-keys) and save it as `FACTORY_API_KEY` in your repository or organization secrets. 3. **Add the Action Workflows** - * Create two workflow files under `.github/workflows/` to separate on-demand tagging from automatic PR reviews. + - Create two workflow files under `.github/workflows/` to separate on-demand tagging from automatic PR reviews. `droid.yml` (responds to explicit `@droid` mentions): @@ -105,26 +105,28 @@ Once committed, tagging `@droid fill` or `@droid review` on an open PR will trig ## Using the Commands ### `@droid fill` -* Place the command in the PR description or in a top-level comment. -* Droid searches for common PR template locations (`.github/pull_request_template.md`, etc.). When a template exists, it fills the sections; otherwise it writes a structured summary (overview, changes, testing, rollout). -* The original request is replaced with the generated description so reviewers can merge immediately. + +- Place the command in the PR description or in a top-level comment. +- Droid searches for common PR template locations (`.github/pull_request_template.md`, etc.). When a template exists, it fills the sections; otherwise it writes a structured summary (overview, changes, testing, rollout). +- The original request is replaced with the generated description so reviewers can merge immediately. ### `@droid review` -* Mention `@droid review` in a PR comment. -* Droid inspects the diff, prioritizes potential bugs or high-impact issues, and leaves inline comments directly on the changed lines. -* A short summary comment is posted in the original thread highlighting the findings and linking to any inline feedback. + +- Mention `@droid review` in a PR comment. +- Droid inspects the diff, prioritizes potential bugs or high-impact issues, and leaves inline comments directly on the changed lines. +- A short summary comment is posted in the original thread highlighting the findings and linking to any inline feedback. ## Configuration Essentials -| Input | Purpose | -| --- | --- | -| `factory_api_key` | **Required.** Grants Droid Exec permission to run via Factory. | -| `github_token` | Optional override if you prefer a custom GitHub App/token. By default the installed app token is used. | -| `review_model` | Optional. Override the model used for code review (e.g., `claude-sonnet-4-5-20250929`, `gpt-5.1-codex`). Only applies to review flows. | -| `fill_model` | Optional. Override the model used for PR description fill (e.g., `claude-sonnet-4-5-20250929`, `gpt-5.1-codex`). Only applies to fill flows. | +| Input | Purpose | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `factory_api_key` | **Required.** Grants Droid Exec permission to run via Factory. | +| `github_token` | Optional override if you prefer a custom GitHub App/token. By default the installed app token is used. | +| `review_model` | Optional. Override the model used for code review (e.g., `claude-sonnet-4-5-20250929`, `gpt-5.1-codex`). Only applies to review flows. | +| `fill_model` | Optional. Override the model used for PR description fill (e.g., `claude-sonnet-4-5-20250929`, `gpt-5.1-codex`). Only applies to fill flows. | ## Troubleshooting & Support -* Check the workflow run linked from the Droid tracking comment for execution logs. -* Verify that the workflow file and repository allow the GitHub App to run (branch protections can block bots). -* Need more detail? Start with the [Setup Guide](./docs/setup.md) or [FAQ](./docs/faq.md). +- Check the workflow run linked from the Droid tracking comment for execution logs. +- Verify that the workflow file and repository allow the GitHub App to run (branch protections can block bots). +- Need more detail? Start with the [Setup Guide](./docs/setup.md) or [FAQ](./docs/faq.md). diff --git a/base-action/src/index.ts b/base-action/src/index.ts index d854ab3..3661021 100644 --- a/base-action/src/index.ts +++ b/base-action/src/index.ts @@ -28,8 +28,7 @@ async function run() { mcpTools: process.env.INPUT_MCP_TOOLS, systemPrompt: process.env.INPUT_SYSTEM_PROMPT, appendSystemPrompt: process.env.INPUT_APPEND_SYSTEM_PROMPT, - pathToDroidExecutable: - process.env.INPUT_PATH_TO_DROID_EXECUTABLE, + pathToDroidExecutable: process.env.INPUT_PATH_TO_DROID_EXECUTABLE, showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT, }); } catch (error) { diff --git a/base-action/src/run-droid.ts b/base-action/src/run-droid.ts index d7cb999..724fd0c 100644 --- a/base-action/src/run-droid.ts +++ b/base-action/src/run-droid.ts @@ -121,10 +121,12 @@ export async function runDroid(promptPath: string, options: DroidOptions) { const cfg = JSON.parse(options.mcpTools); const servers = cfg?.mcpServers || {}; const serverNames = Object.keys(servers); - + if (serverNames.length > 0) { - console.log(`Registering ${serverNames.length} MCP servers: ${serverNames.join(", ")}`); - + console.log( + `Registering ${serverNames.length} MCP servers: ${serverNames.join(", ")}`, + ); + for (const [name, def] of Object.entries(servers)) { const cmd = [def.command, ...(def.args || [])] .filter(Boolean) @@ -143,12 +145,15 @@ export async function runDroid(promptPath: string, options: DroidOptions) { .join(" "); const addCmd = `droid mcp add ${name} "${cmd}" ${envFlags}`.trim(); - + try { await execAsync(addCmd, { env: { ...process.env } }); console.log(` ✓ Registered MCP server: ${name}`); } catch (e: any) { - console.error(` ✗ Failed to register MCP server ${name}:`, e.message); + console.error( + ` ✗ Failed to register MCP server ${name}:`, + e.message, + ); throw e; } } @@ -184,15 +189,19 @@ export async function runDroid(promptPath: string, options: DroidOptions) { // Log custom arguments if any if (options.droidArgs && options.droidArgs.trim() !== "") { console.log(`Custom Droid arguments: ${options.droidArgs}`); - + // Check for deprecated MCP tool naming - const enabledToolsMatch = options.droidArgs.match(/--enabled-tools\s+["\']?([^"\']+)["\']?/); + const enabledToolsMatch = options.droidArgs.match( + /--enabled-tools\s+["\']?([^"\']+)["\']?/, + ); if (enabledToolsMatch && enabledToolsMatch[1]) { - const tools = enabledToolsMatch[1].split(",").map(t => t.trim()); - const oldStyleTools = tools.filter(t => t.startsWith("mcp__")); - + const tools = enabledToolsMatch[1].split(",").map((t) => t.trim()); + const oldStyleTools = tools.filter((t) => t.startsWith("mcp__")); + if (oldStyleTools.length > 0) { - console.warn(`Warning: Found ${oldStyleTools.length} tools with deprecated mcp__ prefix. Update to new pattern (e.g., github_comment___update_droid_comment)`); + console.warn( + `Warning: Found ${oldStyleTools.length} tools with deprecated mcp__ prefix. Update to new pattern (e.g., github_comment___update_droid_comment)`, + ); } } } @@ -247,7 +256,10 @@ export async function runDroid(promptPath: string, options: DroidOptions) { const parsed = JSON.parse(line); if (!sessionId && typeof parsed === "object" && parsed !== null) { const detectedSessionId = parsed.session_id; - if (typeof detectedSessionId === "string" && detectedSessionId.trim()) { + if ( + typeof detectedSessionId === "string" && + detectedSessionId.trim() + ) { sessionId = detectedSessionId; console.log(`Detected Droid session: ${sessionId}`); } @@ -272,7 +284,6 @@ export async function runDroid(promptPath: string, options: DroidOptions) { // In non-full-output mode, suppress non-JSON output } }); - }); // Handle stdout errors diff --git a/base-action/test/parse-shell-args.test.ts b/base-action/test/parse-shell-args.test.ts index b6f0dc4..297d5e3 100644 --- a/base-action/test/parse-shell-args.test.ts +++ b/base-action/test/parse-shell-args.test.ts @@ -8,14 +8,8 @@ describe("shell-quote parseShellArgs", () => { }); test("should parse simple arguments", () => { - expect(parseShellArgs("--auto medium")).toEqual([ - "--auto", - "medium", - ]); - expect(parseShellArgs("-s session-123")).toEqual([ - "-s", - "session-123", - ]); + expect(parseShellArgs("--auto medium")).toEqual(["--auto", "medium"]); + expect(parseShellArgs("-s session-123")).toEqual(["-s", "session-123"]); }); test("should handle double quotes", () => { @@ -27,10 +21,11 @@ describe("shell-quote parseShellArgs", () => { }); test("should handle single quotes", () => { - expect(parseShellArgs("--file '/tmp/prompt.md'")) - .toEqual(["--file", "/tmp/prompt.md"]); - expect(parseShellArgs("'arg with spaces'")) - .toEqual(["arg with spaces"]); + expect(parseShellArgs("--file '/tmp/prompt.md'")).toEqual([ + "--file", + "/tmp/prompt.md", + ]); + expect(parseShellArgs("'arg with spaces'")).toEqual(["arg with spaces"]); }); test("should handle escaped characters", () => { expect(parseShellArgs("arg\\ with\\ spaces")).toEqual(["arg with spaces"]); diff --git a/base-action/test/run-droid-mcp.test.ts b/base-action/test/run-droid-mcp.test.ts index ea652a2..98cde42 100644 --- a/base-action/test/run-droid-mcp.test.ts +++ b/base-action/test/run-droid-mcp.test.ts @@ -74,11 +74,15 @@ const mockSpawn = mock( mock.module("child_process", () => ({ exec: ( command: string, - options?: Record | ((err: Error | null, result?: any) => void), + options?: + | Record + | ((err: Error | null, result?: any) => void), maybeCallback?: (err: Error | null, result?: any) => void, ) => { const callback = - typeof options === "function" ? options : maybeCallback ?? (() => undefined); + typeof options === "function" + ? options + : (maybeCallback ?? (() => undefined)); setImmediate(async () => { try { @@ -98,7 +102,7 @@ let runDroid: RunDroidModule["runDroid"]; beforeAll(async () => { const module = (await import( - `../src/run-droid?mcp-test=${Math.random().toString(36).slice(2)}`, + `../src/run-droid?mcp-test=${Math.random().toString(36).slice(2)}` )) as RunDroidModule; prepareRunConfig = module.prepareRunConfig; runDroid = module.runDroid; @@ -139,23 +143,23 @@ describe("MCP Server Registration", () => { env: { GITHUB_TOKEN: "test-token", REPO_OWNER: "owner", - REPO_NAME: "repo" - } + REPO_NAME: "repo", + }, }, github_ci: { command: "bun", args: ["run", "/path/to/github-actions-server.ts"], env: { GITHUB_TOKEN: "test-token", - PR_NUMBER: "123" - } - } - } + PR_NUMBER: "123", + }, + }, + }, }); const options: DroidOptions = { mcpTools, - pathToDroidExecutable: "droid" + pathToDroidExecutable: "droid", }; const promptPath = await createPromptFile(); const tempDir = process.env.RUNNER_TEMP!; @@ -180,14 +184,14 @@ describe("MCP Server Registration", () => { mcpTools: "", }; const prepared = prepareRunConfig("/tmp/test-prompt.txt", options); - + expect(prepared.droidArgs).not.toContain("--mcp-config"); }); test("should handle invalid JSON in MCP config", () => { const options: DroidOptions = { mcpTools: "{ invalid json }", - pathToDroidExecutable: "droid" + pathToDroidExecutable: "droid", }; // prepareRunConfig doesn't parse MCP config, so it won't throw @@ -205,7 +209,8 @@ describe("MCP Server Registration", () => { console.warn = warnSpy as unknown as typeof console.warn; const options: DroidOptions = { - droidArgs: '--enabled-tools "mcp__github_comment__update_droid_comment,Execute"' + droidArgs: + '--enabled-tools "mcp__github_comment__update_droid_comment,Execute"', }; const promptPath = await createPromptFile(); @@ -216,8 +221,10 @@ describe("MCP Server Registration", () => { const warningMessages = warnSpy.mock.calls.map((args) => args[0]); expect( - warningMessages.some((msg) => - typeof msg === "string" && msg.includes("deprecated mcp__ prefix"), + warningMessages.some( + (msg) => + typeof msg === "string" && + msg.includes("deprecated mcp__ prefix"), ), ).toBe(true); } finally { @@ -232,7 +239,8 @@ describe("MCP Server Registration", () => { console.warn = warnSpy as unknown as typeof console.warn; const options: DroidOptions = { - droidArgs: '--enabled-tools "github_comment___update_droid_comment,Execute"' + droidArgs: + '--enabled-tools "github_comment___update_droid_comment,Execute"', }; const promptPath = await createPromptFile(); @@ -249,14 +257,17 @@ describe("MCP Server Registration", () => { test("should detect MCP tools with triple underscore pattern", () => { const options: DroidOptions = { - droidArgs: '--enabled-tools "github_ci___get_ci_status,github_comment___update_droid_comment"' + droidArgs: + '--enabled-tools "github_ci___get_ci_status,github_comment___update_droid_comment"', }; const prepared = prepareRunConfig("/tmp/test-prompt.txt", options); - + // The args should be passed through correctly expect(prepared.droidArgs).toContain("--enabled-tools"); - expect(prepared.droidArgs).toContain("github_ci___get_ci_status,github_comment___update_droid_comment"); + expect(prepared.droidArgs).toContain( + "github_ci___get_ci_status,github_comment___update_droid_comment", + ); }); }); @@ -267,14 +278,14 @@ describe("MCP Server Registration", () => { failing_server: { command: "nonexistent", args: ["command"], - env: {} - } - } + env: {}, + }, + }, }); const options: DroidOptions = { mcpTools, - pathToDroidExecutable: "droid" + pathToDroidExecutable: "droid", }; const promptPath = await createPromptFile(); const tempDir = process.env.RUNNER_TEMP!; @@ -299,18 +310,18 @@ describe("MCP Server Registration", () => { describe("Environment Variables", () => { test("should include GITHUB_ACTION_INPUTS when present", () => { process.env.INPUT_ACTION_INPUTS_PRESENT = "true"; - + const options: DroidOptions = {}; const prepared = prepareRunConfig("/tmp/test-prompt.txt", options); expect(prepared.env.GITHUB_ACTION_INPUTS).toBe("true"); - + delete process.env.INPUT_ACTION_INPUTS_PRESENT; }); test("should not include GITHUB_ACTION_INPUTS when not present", () => { delete process.env.INPUT_ACTION_INPUTS_PRESENT; - + const options: DroidOptions = {}; const prepared = prepareRunConfig("/tmp/test-prompt.txt", options); diff --git a/base-action/test/run-droid.test.ts b/base-action/test/run-droid.test.ts index 4bb95d2..36f7cb6 100644 --- a/base-action/test/run-droid.test.ts +++ b/base-action/test/run-droid.test.ts @@ -43,7 +43,7 @@ describe("prepareRunConfig", () => { "exec", "--output-format", "stream-json", -"--skip-permissions-unsafe", + "--skip-permissions-unsafe", "--max-turns", "10", "--model", @@ -63,7 +63,7 @@ describe("prepareRunConfig", () => { "exec", "--output-format", "stream-json", - "--skip-permissions-unsafe", + "--skip-permissions-unsafe", "-f", "/tmp/test-prompt.txt", ]); @@ -79,7 +79,7 @@ describe("prepareRunConfig", () => { "exec", "--output-format", "stream-json", - "--skip-permissions-unsafe", + "--skip-permissions-unsafe", "--system-prompt", "You are a helpful assistant", "-f", diff --git a/base-action/test/setup-droid-settings.test.ts b/base-action/test/setup-droid-settings.test.ts index 00dc661..0ae22a4 100644 --- a/base-action/test/setup-droid-settings.test.ts +++ b/base-action/test/setup-droid-settings.test.ts @@ -99,7 +99,9 @@ describe("setupDroidSettings", () => { }); test("should throw error for non-existent file path", async () => { - expect(() => setupDroidSettings("/non/existent/file.json", testHomeDir)).toThrow(); + expect(() => + setupDroidSettings("/non/existent/file.json", testHomeDir), + ).toThrow(); }); test("should handle empty string input", async () => { diff --git a/base-action/test/validate-env.test.ts b/base-action/test/validate-env.test.ts index 67b5a28..0989d29 100644 --- a/base-action/test/validate-env.test.ts +++ b/base-action/test/validate-env.test.ts @@ -16,13 +16,13 @@ describe("validateEnvironmentVariables", () => { }); test("passes when FACTORY_API_KEY is set", () => { - process.env.FACTORY_API_KEY = 'test-factory-key'; + process.env.FACTORY_API_KEY = "test-factory-key"; expect(() => validateEnvironmentVariables()).not.toThrow(); }); test("throws when FACTORY_API_KEY is missing", () => { expect(() => validateEnvironmentVariables()).toThrow( - 'FACTORY_API_KEY is required to run Droid Exec. Please provide the factory_api_key input.' + "FACTORY_API_KEY is required to run Droid Exec. Please provide the factory_api_key input.", ); }); }); diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 511fd34..7c2ba96 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -108,15 +108,12 @@ export function prepareContext( commonFields.droidBranch = droidBranch; } - const eventData = buildEventData( - context, - { - commentId, - commentBody, - baseBranch, - droidBranch, - }, - ); + const eventData = buildEventData(context, { + commentId, + commentBody, + baseBranch, + droidBranch, + }); const result: PreparedContext = { ...commonFields, @@ -282,9 +279,7 @@ function buildEventData( } } -export type PromptGenerator = ( - context: PreparedContext, -) => string; +export type PromptGenerator = (context: PreparedContext) => string; export type PromptCreationOptions = { githubContext: ParsedGitHubContext; diff --git a/src/create-prompt/templates/fill-prompt.ts b/src/create-prompt/templates/fill-prompt.ts index cc105d3..86bad58 100644 --- a/src/create-prompt/templates/fill-prompt.ts +++ b/src/create-prompt/templates/fill-prompt.ts @@ -1,8 +1,6 @@ import type { PreparedContext } from "../types"; -export function generateFillPrompt( - context: PreparedContext, -): string { +export function generateFillPrompt(context: PreparedContext): string { const prNumber = context.eventData.isPR ? context.eventData.prNumber : context.githubContext && "entityNumber" in context.githubContext diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index cb09303..5763f35 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -73,7 +73,6 @@ async function run() { if (result?.mcpTools) { core.setOutput("mcp_tools", result.mcpTools); } - } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(`Prepare step failed with error: ${errorMessage}`); diff --git a/src/github/token.ts b/src/github/token.ts index 986f0b6..471f8ba 100644 --- a/src/github/token.ts +++ b/src/github/token.ts @@ -41,45 +41,57 @@ async function exchangeForAppToken(oidcToken: string): Promise { // Handle the simplified flat error response format const errorCode = responseJson.error || `http_${response.status}`; - const errorMessage = responseJson.message || responseJson.detail || responseJson.error || "Unknown error"; + const errorMessage = + responseJson.message || + responseJson.detail || + responseJson.error || + "Unknown error"; const specificErrorCode = responseJson.error_code; const repository = responseJson.repository; // Check for specific error codes that should skip the action - if (errorCode === "workflow_validation_failed" || - specificErrorCode === "workflow_not_found_on_default_branch") { - core.warning(`Skipping action due to workflow validation: ${errorMessage}`); + if ( + errorCode === "workflow_validation_failed" || + specificErrorCode === "workflow_not_found_on_default_branch" + ) { + core.warning( + `Skipping action due to workflow validation: ${errorMessage}`, + ); console.log( "Action skipped due to workflow validation error. This is expected when adding Droid workflows to new repositories or on PRs with workflow changes. If you're seeing this, your workflow will begin working once you merge your PR.", ); core.setOutput("skipped_due_to_workflow_validation_mismatch", "true"); process.exit(0); } - + // Handle GitHub App not installed error with helpful message if (errorCode === "app_not_installed") { const repo = repository || "this repository"; console.error( `The Factory GitHub App is not installed for ${repo}. ` + - `Please install it at: https://github.com/apps/factory-ai` + `Please install it at: https://github.com/apps/factory-ai`, ); throw new Error(errorMessage); } - + // Handle rate limiting with retry suggestion if (errorCode === "rate_limited") { console.error( - `GitHub API rate limit exceeded. Please wait a few minutes and try again.` + `GitHub API rate limit exceeded. Please wait a few minutes and try again.`, ); throw new Error(errorMessage); } - + // Handle OIDC verification errors if (errorCode === "oidc_verification_failed") { if (specificErrorCode === "token_expired") { - console.error("OIDC token has expired. The workflow may be taking too long."); + console.error( + "OIDC token has expired. The workflow may be taking too long.", + ); } else if (specificErrorCode === "audience_mismatch") { - console.error("OIDC token audience mismatch. This is likely a configuration issue."); + console.error( + "OIDC token audience mismatch. This is likely a configuration issue.", + ); } else if (specificErrorCode === "invalid_signature") { console.error("OIDC token signature verification failed."); } @@ -89,7 +101,7 @@ async function exchangeForAppToken(oidcToken: string): Promise { console.error( `App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`, errorCode !== errorMessage ? `(Code: ${errorCode})` : "", - specificErrorCode ? `(Specific: ${specificErrorCode})` : "" + specificErrorCode ? `(Specific: ${specificErrorCode})` : "", ); throw new Error(errorMessage); } @@ -99,7 +111,7 @@ async function exchangeForAppToken(oidcToken: string): Promise { token?: string; expires_at?: string; }; - + if (!appTokenData.token) { throw new Error("App token not found in response"); } diff --git a/src/github/utils/command-parser.test.ts b/src/github/utils/command-parser.test.ts index b0be23f..7fcfb6c 100644 --- a/src/github/utils/command-parser.test.ts +++ b/src/github/utils/command-parser.test.ts @@ -125,17 +125,14 @@ describe("Command Parser", () => { describe("extractCommandFromContext", () => { it("should extract from PR body", () => { - const context = createContext( - "pull_request", - { - action: "opened", - pull_request: { - body: "PR description\n\n@droid fill", - number: 1, - title: "PR", - }, - } as unknown as PullRequestEvent, - ); + const context = createContext("pull_request", { + action: "opened", + pull_request: { + body: "PR description\n\n@droid fill", + number: 1, + title: "PR", + }, + } as unknown as PullRequestEvent); const result = extractCommandFromContext(context); expect(result?.command).toBe("fill"); expect(result?.location).toBe("body"); @@ -160,20 +157,17 @@ describe("Command Parser", () => { }); it("should extract from issue comment", () => { - const context = createContext( - "issue_comment", - { - action: "created", - comment: { - body: "@droid fill please", - created_at: "2024-01-01T00:00:00Z", - }, - issue: { - number: 1, - pull_request: { url: "" }, - }, - } as unknown as IssueCommentEvent, - ); + const context = createContext("issue_comment", { + action: "created", + comment: { + body: "@droid fill please", + created_at: "2024-01-01T00:00:00Z", + }, + issue: { + number: 1, + pull_request: { url: "" }, + }, + } as unknown as IssueCommentEvent); const result = extractCommandFromContext(context); expect(result?.command).toBe("fill"); expect(result?.location).toBe("comment"); @@ -181,19 +175,16 @@ describe("Command Parser", () => { }); it("should extract from PR review comment", () => { - const context = createContext( - "pull_request_review_comment", - { - action: "created", - comment: { - body: "Can you @droid review this section?", - created_at: "2024-01-01T00:00:00Z", - }, - pull_request: { - number: 1, - }, - } as unknown as PullRequestReviewCommentEvent, - ); + const context = createContext("pull_request_review_comment", { + action: "created", + comment: { + body: "Can you @droid review this section?", + created_at: "2024-01-01T00:00:00Z", + }, + pull_request: { + number: 1, + }, + } as unknown as PullRequestReviewCommentEvent); const result = extractCommandFromContext(context); expect(result?.command).toBe("review"); expect(result?.location).toBe("comment"); @@ -201,19 +192,16 @@ describe("Command Parser", () => { }); it("should extract from PR review body", () => { - const context = createContext( - "pull_request_review", - { - action: "submitted", - review: { - body: "LGTM but @droid fill the description", - submitted_at: "2024-01-01T00:00:00Z", - }, - pull_request: { - number: 1, - }, - } as unknown as PullRequestReviewEvent, - ); + const context = createContext("pull_request_review", { + action: "submitted", + review: { + body: "LGTM but @droid fill the description", + submitted_at: "2024-01-01T00:00:00Z", + }, + pull_request: { + number: 1, + }, + } as unknown as PullRequestReviewEvent); const result = extractCommandFromContext(context); expect(result?.command).toBe("fill"); expect(result?.location).toBe("comment"); @@ -221,17 +209,14 @@ describe("Command Parser", () => { }); it("should return null for events without commands", () => { - const context = createContext( - "pull_request", - { - action: "opened", - pull_request: { - body: "Regular PR description", - number: 1, - title: "PR", - }, - } as unknown as PullRequestEvent, - ); + const context = createContext("pull_request", { + action: "opened", + pull_request: { + body: "Regular PR description", + number: 1, + title: "PR", + }, + } as unknown as PullRequestEvent); const result = extractCommandFromContext(context); expect(result).toBeNull(); }); @@ -247,17 +232,14 @@ describe("Command Parser", () => { }); it("should handle missing body gracefully", () => { - const context = createContext( - "pull_request", - { - action: "opened", - pull_request: { - body: null, - number: 1, - title: "PR", - }, - } as unknown as PullRequestEvent, - ); + const context = createContext("pull_request", { + action: "opened", + pull_request: { + body: null, + number: 1, + title: "PR", + }, + } as unknown as PullRequestEvent); const result = extractCommandFromContext(context); expect(result).toBeNull(); }); @@ -273,20 +255,17 @@ describe("Command Parser", () => { }); it("should extract default command when no specific command", () => { - const context = createContext( - "issue_comment", - { - action: "created", - comment: { - body: "@droid can you help with this?", - created_at: "2024-01-01T00:00:00Z", - }, - issue: { - number: 1, - pull_request: { url: "" }, - }, - } as unknown as IssueCommentEvent, - ); + const context = createContext("issue_comment", { + action: "created", + comment: { + body: "@droid can you help with this?", + created_at: "2024-01-01T00:00:00Z", + }, + issue: { + number: 1, + pull_request: { url: "" }, + }, + } as unknown as IssueCommentEvent); const result = extractCommandFromContext(context); expect(result?.command).toBe("default"); expect(result?.location).toBe("comment"); diff --git a/src/github/utils/command-parser.ts b/src/github/utils/command-parser.ts index a1250ca..bf9f941 100644 --- a/src/github/utils/command-parser.ts +++ b/src/github/utils/command-parser.ts @@ -4,12 +4,12 @@ import type { GitHubContext } from "../context"; -export type DroidCommand = 'fill' | 'review' | 'default'; +export type DroidCommand = "fill" | "review" | "default"; export interface ParsedCommand { command: DroidCommand; raw: string; - location: 'body' | 'comment'; + location: "body" | "comment"; timestamp?: string | null; } @@ -27,9 +27,9 @@ export function parseDroidCommand(text: string): ParsedCommand | null { const fillMatch = text.match(/@droid\s+fill/i); if (fillMatch) { return { - command: 'fill', + command: "fill", raw: fillMatch[0], - location: 'body', // Will be set by caller + location: "body", // Will be set by caller }; } @@ -37,9 +37,9 @@ export function parseDroidCommand(text: string): ParsedCommand | null { const reviewMatch = text.match(/@droid\s+review/i); if (reviewMatch) { return { - command: 'review', + command: "review", raw: reviewMatch[0], - location: 'body', // Will be set by caller + location: "body", // Will be set by caller }; } @@ -47,9 +47,9 @@ export function parseDroidCommand(text: string): ParsedCommand | null { const droidMatch = text.match(/@droid/i); if (droidMatch) { return { - command: 'default', + command: "default", raw: droidMatch[0], - location: 'body', // Will be set by caller + location: "body", // Will be set by caller }; } @@ -61,43 +61,48 @@ export function parseDroidCommand(text: string): ParsedCommand | null { * @param context The GitHub context from the event * @returns ParsedCommand with location info, or null if no command found */ -export function extractCommandFromContext(context: GitHubContext): ParsedCommand | null { +export function extractCommandFromContext( + context: GitHubContext, +): ParsedCommand | null { // Handle missing payload if (!context.payload) { return null; } // Check PR body for commands (pull_request events) - if (context.eventName === 'pull_request' && 'pull_request' in context.payload) { + if ( + context.eventName === "pull_request" && + "pull_request" in context.payload + ) { const body = context.payload.pull_request.body; if (body) { const command = parseDroidCommand(body); if (command) { - return { ...command, location: 'body' }; + return { ...command, location: "body" }; } } } // Check issue body for commands (issues events) - if (context.eventName === 'issues' && 'issue' in context.payload) { + if (context.eventName === "issues" && "issue" in context.payload) { const body = context.payload.issue.body; if (body) { const command = parseDroidCommand(body); if (command) { - return { ...command, location: 'body' }; + return { ...command, location: "body" }; } } } // Check comment body for commands (issue_comment events) - if (context.eventName === 'issue_comment' && 'comment' in context.payload) { + if (context.eventName === "issue_comment" && "comment" in context.payload) { const comment = context.payload.comment; if (comment.body) { const command = parseDroidCommand(comment.body); if (command) { return { ...command, - location: 'comment', + location: "comment", timestamp: comment.created_at, }; } @@ -105,14 +110,17 @@ export function extractCommandFromContext(context: GitHubContext): ParsedCommand } // Check review comment body (pull_request_review_comment events) - if (context.eventName === 'pull_request_review_comment' && 'comment' in context.payload) { + if ( + context.eventName === "pull_request_review_comment" && + "comment" in context.payload + ) { const comment = context.payload.comment; if (comment.body) { const command = parseDroidCommand(comment.body); if (command) { return { ...command, - location: 'comment', + location: "comment", timestamp: comment.created_at, }; } @@ -120,14 +128,17 @@ export function extractCommandFromContext(context: GitHubContext): ParsedCommand } // Check review body (pull_request_review events) - if (context.eventName === 'pull_request_review' && 'review' in context.payload) { + if ( + context.eventName === "pull_request_review" && + "review" in context.payload + ) { const review = context.payload.review; if (review.body) { const command = parseDroidCommand(review.body); if (command) { return { ...command, - location: 'comment', + location: "comment", timestamp: review.submitted_at, }; } diff --git a/src/github/validation/trigger-commands.test.ts b/src/github/validation/trigger-commands.test.ts index 1d44a46..57240c2 100644 --- a/src/github/validation/trigger-commands.test.ts +++ b/src/github/validation/trigger-commands.test.ts @@ -9,8 +9,9 @@ import type { PullRequestReviewEvent, } from "@octokit/webhooks-types"; -type ContextOverrides = - Partial> & { payload?: unknown }; +type ContextOverrides = Partial> & { + payload?: unknown; +}; const defaultPayload = { action: "created", @@ -27,7 +28,9 @@ const defaultPayload = { } as unknown as IssueCommentEvent; // Helper function to create a mock context -function createMockContext(overrides: ContextOverrides = {}): ParsedGitHubContext { +function createMockContext( + overrides: ContextOverrides = {}, +): ParsedGitHubContext { return { runId: "run-1", eventName: "issue_comment", @@ -70,7 +73,7 @@ describe("checkContainsTrigger with commands", () => { }, } as unknown as PullRequestEvent, }); - + expect(checkContainsTrigger(context)).toBe(true); }); @@ -84,7 +87,7 @@ describe("checkContainsTrigger with commands", () => { }, } as unknown as IssueCommentEvent, }); - + expect(checkContainsTrigger(context)).toBe(true); }); @@ -101,7 +104,7 @@ describe("checkContainsTrigger with commands", () => { }, } as unknown as PullRequestReviewCommentEvent, }); - + expect(checkContainsTrigger(context)).toBe(true); }); @@ -118,7 +121,7 @@ describe("checkContainsTrigger with commands", () => { }, } as unknown as PullRequestReviewEvent, }); - + expect(checkContainsTrigger(context)).toBe(true); }); @@ -132,7 +135,7 @@ describe("checkContainsTrigger with commands", () => { }, } as unknown as IssueCommentEvent, }); - + // This should still trigger because of the existing trigger phrase logic // but now it will be handled as default command expect(checkContainsTrigger(context)).toBe(true); @@ -148,7 +151,7 @@ describe("checkContainsTrigger with commands", () => { }, } as unknown as IssueCommentEvent, }); - + expect(checkContainsTrigger(context)).toBe(true); }); @@ -162,7 +165,7 @@ describe("checkContainsTrigger with commands", () => { }, } as unknown as IssueCommentEvent, }); - + expect(checkContainsTrigger(context)).toBe(false); }); @@ -179,7 +182,7 @@ describe("checkContainsTrigger with commands", () => { }, } as unknown as IssuesEvent, }); - + expect(checkContainsTrigger(context)).toBe(true); }); }); diff --git a/src/github/validation/trigger.ts b/src/github/validation/trigger.ts index 51e8d25..c01b8c6 100644 --- a/src/github/validation/trigger.ts +++ b/src/github/validation/trigger.ts @@ -18,8 +18,10 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { // Check for specific @droid commands (fill, review) const command = extractCommandFromContext(context); - if (command && ['fill', 'review'].includes(command.command)) { - console.log(`Detected @droid ${command.command} command, triggering action`); + if (command && ["fill", "review"].includes(command.command)) { + console.log( + `Detected @droid ${command.command} command, triggering action`, + ); return true; } diff --git a/src/mcp/github-inline-comment-server.ts b/src/mcp/github-inline-comment-server.ts index 3b9ae31..2a51185 100644 --- a/src/mcp/github-inline-comment-server.ts +++ b/src/mcp/github-inline-comment-server.ts @@ -59,9 +59,9 @@ server.tool( .optional() .default("RIGHT") .describe( - "Side of the diff to comment on: LEFT (old code) or RIGHT (new code). " + - "IMPORTANT: Use RIGHT for comments on new/modified code. " + - "Only use LEFT when specifically referencing code being removed." + "Side of the diff to comment on: LEFT (old code) or RIGHT (new code). " + + "IMPORTANT: Use RIGHT for comments on new/modified code. " + + "Only use LEFT when specifically referencing code being removed.", ), commit_id: z .string() diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index 44b5c46..0714523 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -93,7 +93,6 @@ export async function prepareMcpTools( }, }; - // Include inline comment server for PRs when requested via allowed tools if ( isEntityContext(context) && @@ -118,9 +117,7 @@ export async function prepareMcpTools( const hasWorkflowToken = !!process.env.DEFAULT_WORKFLOW_TOKEN; const shouldIncludeCIServer = - isEntityContext(context) && - context.isPR && - hasWorkflowToken; + isEntityContext(context) && context.isPR && hasWorkflowToken; if (shouldIncludeCIServer) { // Verify the token actually has actions:read permission diff --git a/src/tag/commands/fill.ts b/src/tag/commands/fill.ts index 67a5e19..1cc7e31 100644 --- a/src/tag/commands/fill.ts +++ b/src/tag/commands/fill.ts @@ -33,7 +33,7 @@ export async function prepareFillMode({ const commentId = trackingCommentId ?? (await createInitialComment(octokit.rest, context)).id; - + const prData = await fetchPRBranchData({ octokits: octokit, repository: context.repository, diff --git a/src/utils/parse-tools.ts b/src/utils/parse-tools.ts index 9f6e065..9020401 100644 --- a/src/utils/parse-tools.ts +++ b/src/utils/parse-tools.ts @@ -22,7 +22,10 @@ export function parseAllowedTools(args: string): string[] { return []; } - return value.split(",").map((tool) => tool.trim()).filter(Boolean); + return value + .split(",") + .map((tool) => tool.trim()) + .filter(Boolean); } export function normalizeDroidArgs(args: string): string { @@ -30,14 +33,16 @@ export function normalizeDroidArgs(args: string): string { return ""; } - return args - .replace(/--allowedTools/g, "--enabled-tools") - .replace(/--allowed-tools/g, "--enabled-tools") - .replace(/--enabledTools/g, "--enabled-tools") - .replace(/--disallowedTools/g, "--disabled-tools") - .replace(/--disabled-tools/g, "--disabled-tools") - .replace(/--disallowed-tools/g, "--disabled-tools") - // Strip unsupported MCP inline config flags to avoid CLI errors - .replace(/--mcp-config\s+(?:"[^"]*"|'[^']*'|[^\s]+)/g, "") - .trim(); + return ( + args + .replace(/--allowedTools/g, "--enabled-tools") + .replace(/--allowed-tools/g, "--enabled-tools") + .replace(/--enabledTools/g, "--enabled-tools") + .replace(/--disallowedTools/g, "--disabled-tools") + .replace(/--disabled-tools/g, "--disabled-tools") + .replace(/--disallowed-tools/g, "--disabled-tools") + // Strip unsupported MCP inline config flags to avoid CLI errors + .replace(/--mcp-config\s+(?:"[^"]*"|'[^']*'|[^\s]+)/g, "") + .trim() + ); } diff --git a/test/comment-logic.test.ts b/test/comment-logic.test.ts index 33bb2e6..b068949 100644 --- a/test/comment-logic.test.ts +++ b/test/comment-logic.test.ts @@ -279,9 +279,7 @@ describe("updateCommentBody", () => { }; const result = updateCommentBody(input); - expect(result).toContain( - "**Droid finished @testuser's task in 1m 15s**", - ); + expect(result).toContain("**Droid finished @testuser's task in 1m 15s**"); }); it("includes duration in error header", () => { diff --git a/test/create-prompt/templates/fill-prompt.test.ts b/test/create-prompt/templates/fill-prompt.test.ts index 5d42bf4..baff84d 100644 --- a/test/create-prompt/templates/fill-prompt.test.ts +++ b/test/create-prompt/templates/fill-prompt.test.ts @@ -2,7 +2,9 @@ import { describe, expect, it } from "bun:test"; import { generateFillPrompt } from "../../../src/create-prompt/templates/fill-prompt"; import type { PreparedContext } from "../../../src/create-prompt/types"; -function createBaseContext(overrides: Partial = {}): PreparedContext { +function createBaseContext( + overrides: Partial = {}, +): PreparedContext { return { repository: "test-owner/test-repo", droidCommentId: "123", @@ -26,7 +28,9 @@ describe("generateFillPrompt", () => { const prompt = generateFillPrompt(context); expect(prompt).toContain("Procedure:"); - expect(prompt).toContain("gh pr view 42 --repo test-owner/test-repo --json title,body"); + expect(prompt).toContain( + "gh pr view 42 --repo test-owner/test-repo --json title,body", + ); expect(prompt).toContain("gh pr diff 42 --repo test-owner/test-repo"); expect(prompt).toContain("github_pr___update_pr_description"); expect(prompt).toContain("Do not proceed if required commands fail"); diff --git a/test/create-prompt/templates/review-prompt.test.ts b/test/create-prompt/templates/review-prompt.test.ts index 3ba4e97..446b1b5 100644 --- a/test/create-prompt/templates/review-prompt.test.ts +++ b/test/create-prompt/templates/review-prompt.test.ts @@ -30,32 +30,46 @@ describe("generateReviewPrompt", () => { expect(prompt).toContain("Objectives:"); expect(prompt).toContain("Re-check existing review comments"); expect(prompt).toContain("gh pr diff 42 --repo test-owner/test-repo"); - expect(prompt).toContain("gh api repos/test-owner/test-repo/pulls/42/files"); + expect(prompt).toContain( + "gh api repos/test-owner/test-repo/pulls/42/files", + ); expect(prompt).toContain("github_inline_comment___create_inline_comment"); expect(prompt).toContain("github_pr___resolve_review_thread"); - expect(prompt).toContain("every substantive comment must be inline on the changed line"); + expect(prompt).toContain( + "every substantive comment must be inline on the changed line", + ); }); it("emphasizes accuracy gates and bug detection guidelines", () => { const prompt = generateReviewPrompt(createBaseContext()); expect(prompt).toContain("How Many Findings to Return:"); - expect(prompt).toContain("Output all findings that the original author would fix"); + expect(prompt).toContain( + "Output all findings that the original author would fix", + ); expect(prompt).toContain("Key Guidelines for Bug Detection:"); expect(prompt).toContain("Priority Levels:"); expect(prompt).toContain("[P0]"); expect(prompt).toContain("Never raise purely stylistic"); - expect(prompt).toContain("Never repeat or re-raise an issue previously highlighted"); + expect(prompt).toContain( + "Never repeat or re-raise an issue previously highlighted", + ); }); it("describes submission guidance", () => { const prompt = generateReviewPrompt(createBaseContext()); - expect(prompt).toContain("Prefer github_inline_comment___create_inline_comment"); - expect(prompt).toContain("gh api repos/test-owner/test-repo/pulls/42/reviews"); + expect(prompt).toContain( + "Prefer github_inline_comment___create_inline_comment", + ); + expect(prompt).toContain( + "gh api repos/test-owner/test-repo/pulls/42/reviews", + ); expect(prompt).toContain("Do not approve or request changes"); expect(prompt).toContain("github_pr___submit_review"); expect(prompt).toContain("github_pr___resolve_review_thread"); - expect(prompt).toContain("skip submitting another comment to avoid redundancy"); + expect(prompt).toContain( + "skip submitting another comment to avoid redundancy", + ); }); }); diff --git a/test/github/token.test.ts b/test/github/token.test.ts index d5983a6..a4d8a5a 100644 --- a/test/github/token.test.ts +++ b/test/github/token.test.ts @@ -34,15 +34,14 @@ describe("setupGitHubToken", () => { process.env.OVERRIDE_GITHUB_TOKEN = "override-token"; const setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {}); - const getIdTokenSpy = spyOn(core, "getIDToken").mockResolvedValue("oidc-token"); + const getIdTokenSpy = spyOn(core, "getIDToken").mockResolvedValue( + "oidc-token", + ); const result = await setupGitHubToken(); expect(result).toBe("override-token"); - expect(setOutputSpy).toHaveBeenCalledWith( - "GITHUB_TOKEN", - "override-token", - ); + expect(setOutputSpy).toHaveBeenCalledWith("GITHUB_TOKEN", "override-token"); expect(getIdTokenSpy).not.toHaveBeenCalled(); setOutputSpy.mockRestore(); @@ -59,7 +58,9 @@ describe("setupGitHubToken", () => { global.fetch = fetchMock as unknown as typeof fetch; const setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {}); - const getIdTokenSpy = spyOn(core, "getIDToken").mockResolvedValue("oidc-token"); + const getIdTokenSpy = spyOn(core, "getIDToken").mockResolvedValue( + "oidc-token", + ); const retrySpy = spyOn(retryModule, "retryWithBackoff").mockImplementation( (operation: () => Promise) => operation(), ); diff --git a/test/integration/fill-flow.test.ts b/test/integration/fill-flow.test.ts index 21ea50c..ed41bc5 100644 --- a/test/integration/fill-flow.test.ts +++ b/test/integration/fill-flow.test.ts @@ -1,11 +1,4 @@ -import { - afterEach, - beforeEach, - describe, - expect, - it, - spyOn, -} from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; import path from "node:path"; import os from "node:os"; import { mkdtemp, readFile, rm } from "node:fs/promises"; @@ -93,17 +86,18 @@ describe("fill command integration", () => { } as any, }); - const octokit = { - rest: {}, - graphql: () => Promise.resolve({ - repository: { - pullRequest: { - baseRefName: "main", - headRefName: "feature/fill", - headRefOid: "abc123", - } - } - }) + const octokit = { + rest: {}, + graphql: () => + Promise.resolve({ + repository: { + pullRequest: { + baseRefName: "main", + headRefName: "feature/fill", + headRefOid: "abc123", + }, + }, + }), } as any; graphqlSpy = spyOn(octokit, "graphql").mockResolvedValue({ @@ -112,8 +106,8 @@ describe("fill command integration", () => { baseRefName: "main", headRefName: "feature/fill", headRefOid: "abc123", - } - } + }, + }, }); const result = await prepareTagExecution({ diff --git a/test/integration/review-flow.test.ts b/test/integration/review-flow.test.ts index 61bbf6e..16b7dbc 100644 --- a/test/integration/review-flow.test.ts +++ b/test/integration/review-flow.test.ts @@ -1,11 +1,4 @@ -import { - afterEach, - beforeEach, - describe, - expect, - it, - spyOn, -} from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; import path from "node:path"; import os from "node:os"; import { mkdtemp, readFile, rm } from "node:fs/promises"; @@ -32,8 +25,6 @@ describe("review command integration", () => { process.env.RUNNER_TEMP = tmpDir; process.env.DROID_ARGS = ""; - - createCommentSpy = spyOn( createInitial, "createInitialComment", @@ -95,27 +86,28 @@ describe("review command integration", () => { } as any, }); - const octokit = { - rest: {}, - graphql: () => Promise.resolve({ - repository: { - pullRequest: { - baseRefName: "main", - headRefName: "feature/review", - headRefOid: "def456", - } - } - }) + const octokit = { + rest: {}, + graphql: () => + Promise.resolve({ + repository: { + pullRequest: { + baseRefName: "main", + headRefName: "feature/review", + headRefOid: "def456", + }, + }, + }), } as any; graphqlSpy = spyOn(octokit, "graphql").mockResolvedValue({ repository: { pullRequest: { baseRefName: "main", - headRefName: "feature/review", + headRefName: "feature/review", headRefOid: "def456", - } - } + }, + }, }); const result = await prepareTagExecution({ @@ -147,20 +139,24 @@ describe("review command integration", () => { expect(prompt).toContain("You are performing an automated code review"); expect(prompt).toContain("github_inline_comment___create_inline_comment"); expect(prompt).toContain("How Many Findings to Return:"); - expect(prompt).toContain("Output all findings that the original author would fix"); + expect(prompt).toContain( + "Output all findings that the original author would fix", + ); expect(prompt).toContain("Key Guidelines for Bug Detection:"); expect(prompt).toContain("Priority Levels:"); - expect(prompt).toContain("gh pr view 7 --repo test-owner/test-repo --json comments,reviews"); - expect(prompt).toContain("every substantive comment must be inline on the changed line"); + expect(prompt).toContain( + "gh pr view 7 --repo test-owner/test-repo --json comments,reviews", + ); + expect(prompt).toContain( + "every substantive comment must be inline on the changed line", + ); expect(prompt).toContain("github_pr___resolve_review_thread"); const droidArgsCall = setOutputSpy.mock.calls.find( (call: unknown[]) => call[0] === "droid_args", ) as [string, string] | undefined; - expect(droidArgsCall?.[1]).toContain( - "github_pr___list_review_comments", - ); + expect(droidArgsCall?.[1]).toContain("github_pr___list_review_comments"); expect(droidArgsCall?.[1]).toContain("github_pr___submit_review"); expect(droidArgsCall?.[1]).toContain( "github_inline_comment___create_inline_comment", diff --git a/test/modes/tag/fill-command.test.ts b/test/modes/tag/fill-command.test.ts index 46b6cdf..ad3f061 100644 --- a/test/modes/tag/fill-command.test.ts +++ b/test/modes/tag/fill-command.test.ts @@ -1,11 +1,4 @@ -import { - afterEach, - beforeEach, - describe, - expect, - it, - spyOn, -} from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; import * as core from "@actions/core"; import { prepareFillMode } from "../../../src/tag/commands/fill"; import { createMockContext } from "../../mockContext"; diff --git a/test/modes/tag/review-command.test.ts b/test/modes/tag/review-command.test.ts index a135ae6..3c20e0b 100644 --- a/test/modes/tag/review-command.test.ts +++ b/test/modes/tag/review-command.test.ts @@ -260,7 +260,9 @@ describe("prepareReviewMode", () => { const droidArgsCall = setOutputSpy.mock.calls.find( (call: unknown[]) => call[0] === "droid_args", ) as [string, string] | undefined; - expect(droidArgsCall?.[1]).toContain('--model "claude-sonnet-4-5-20250929"'); + expect(droidArgsCall?.[1]).toContain( + '--model "claude-sonnet-4-5-20250929"', + ); }); it("does not add --model flag when REVIEW_MODEL is empty", async () => { diff --git a/test/prepare-context.test.ts b/test/prepare-context.test.ts index e228822..d391956 100644 --- a/test/prepare-context.test.ts +++ b/test/prepare-context.test.ts @@ -72,7 +72,9 @@ describe("parseEnvVarsWithContext", () => { test("should throw error when DROID_BRANCH is missing", () => { expect(() => prepareContext(mockIssueCommentContext, "12345", "main"), - ).toThrow("issue_comment on issues requires droidBranch and baseBranch"); + ).toThrow( + "issue_comment on issues requires droidBranch and baseBranch", + ); }); test("should throw error when BASE_BRANCH is missing", () => { @@ -83,7 +85,9 @@ describe("parseEnvVarsWithContext", () => { undefined, "droid/issue-67890-20240101-1200", ), - ).toThrow("issue_comment on issues requires droidBranch and baseBranch"); + ).toThrow( + "issue_comment on issues requires droidBranch and baseBranch", + ); }); }); diff --git a/test/trigger-validation.test.ts b/test/trigger-validation.test.ts index 3ca63f0..46bdf8f 100644 --- a/test/trigger-validation.test.ts +++ b/test/trigger-validation.test.ts @@ -21,7 +21,6 @@ import type { import type { ParsedGitHubContext } from "../src/github/context"; describe("checkContainsTrigger", () => { - // TODO: Assignee and label triggers are disabled until instruct mode is implemented. // These tests verify the triggers are no-ops for now. describe("assignee trigger (disabled)", () => { diff --git a/test/utils/retry.test.ts b/test/utils/retry.test.ts index 36a58f0..84a584d 100644 --- a/test/utils/retry.test.ts +++ b/test/utils/retry.test.ts @@ -5,14 +5,14 @@ describe("retryWithBackoff", () => { let timeoutSpy: ReturnType; beforeEach(() => { - timeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation( - ((handler: Parameters[0]) => { - if (typeof handler === "function") { - handler(); - } - return 0 as unknown as ReturnType; - }) as unknown as typeof setTimeout, - ); + timeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation((( + handler: Parameters[0], + ) => { + if (typeof handler === "function") { + handler(); + } + return 0 as unknown as ReturnType; + }) as unknown as typeof setTimeout); }); afterEach(() => { @@ -64,7 +64,9 @@ describe("retryWithBackoff", () => { expect(attempts).toBe(2); expect(timeoutSpy).toHaveBeenCalledTimes(1); - const firstCall = timeoutSpy.mock.calls[0] as Parameters | undefined; + const firstCall = timeoutSpy.mock.calls[0] as + | Parameters + | undefined; expect(firstCall?.[1]).toBe(5); }); });