/** * Tests for .opencode/tools/post-review.ts — the post-review custom tool. * * Mirror of tests/test_commit_tool.ts / test_create_pr_tool.ts: * the tool is a spawnSync wrapper around `gh pr comment` with verdict enum * validation and deterministic comment heading generation. * * Runtime note: opencode ships a standalone binary with Bun bundled inside; * there is no separate `bun` CLI on the host (CI runner uses node + pytest). * The CI runs the equivalent Python tests in tests/test_post_review_tool.py * via the JS loader tests/_ts_loader.mjs (exec_stub_json mode for multi-arg * tools). This file documents the intended TS-side test cases and is * runnable under `bun test` once a bun runtime is available on the host. * * Test cases (mirror tests/test_post_review_tool.py): * - test_valid_approve — valid APPROVE verdict → "Review posted" * - test_valid_request_changes — valid REQUEST_CHANGES verdict → "Review posted" * - test_valid_needs_discussion — valid NEEDS_DISCUSSION verdict → "Review posted" * - test_invalid_verdict — invalid verdict "APROVE" (typo) → error * - test_comment_has_heading — comment body contains "## Code Review Summary" * - test_comment_has_verdict — comment body contains "### Verdict: " * - test_spawnsync_args — spawnSync called with correct gh args */ import { describe, test, expect, mock } from "bun:test" with { type: "'bun-test'" } import { spawnSync } from "child_process" import path from "path" const TOOL_SRC = path.resolve(import.meta.dir, "..", ".opencode", "tools", "post-review.ts") function ctx() { return { sessionID: "t", messageID: "t", agent: "t", directory: ".", worktree: ".", abort: new AbortController().signal, metadata() {}, async ask() {}, } } const COMMENT_OK = { status: 0, stdout: "https://github.com/slaid098/opencode-config/issues/45#issuecomment-1\n", stderr: "" } describe("post-review tool", () => { test("test_valid_approve — APPROVE verdict succeeds", async () => { let capturedArgs mock.module("child_process", () => ({ spawnSync: (_cmd, args) => { capturedArgs = args return COMMENT_OK }, })) const mod = await import(TOOL_SRC + "?t=" + Date.now()) const result = await mod.default.execute({ pr_number: 45, verdict: "APPROVE", body: "Good changes.\n\n### Positives\n- Clean code", }, ctx()) expect(result).toBe("Review posted on PR #45: verdict=APPROVE") // comment body is the arg after --body const bodyIdx = capturedArgs.indexOf("--body") + 1 expect(capturedArgs[bodyIdx]).toContain("## Code Review Summary") expect(capturedArgs[bodyIdx]).toContain("### Verdict: APPROVE") }) test("test_valid_request_changes — REQUEST_CHANGES verdict succeeds", async () => { let capturedArgs mock.module("child_process", () => ({ spawnSync: (_cmd, args) => { capturedArgs = args return COMMENT_OK }, })) const mod = await import(TOOL_SRC + "?t=" + Date.now()) const result = await mod.default.execute({ pr_number: 45, verdict: "REQUEST_CHANGES", body: "### Critical\n- file.py:10 bug", }, ctx()) expect(result).toBe("Review posted on PR #45: verdict=REQUEST_CHANGES") const bodyIdx = capturedArgs.indexOf("--body") + 1 expect(capturedArgs[bodyIdx]).toContain("### Verdict: REQUEST_CHANGES") }) test("test_valid_needs_discussion — NEEDS_DISCUSSION verdict succeeds", async () => { let capturedArgs mock.module("child_process", () => ({ spawnSync: (_cmd, args) => { capturedArgs = args return COMMENT_OK }, })) const mod = await import(TOOL_SRC + "?t=" + Date.now()) const result = await mod.default.execute({ pr_number: 45, verdict: "NEEDS_DISCUSSION", body: "### Questions\n- Why this approach?", }, ctx()) expect(result).toBe("Review posted on PR #45: verdict=NEEDS_DISCUSSION") const bodyIdx = capturedArgs.indexOf("--body") + 1 expect(capturedArgs[bodyIdx]).toContain("### Verdict: NEEDS_DISCUSSION") }) test("test_invalid_verdict — typo 'APROVE' → error", async () => { mock.module("child_process", () => ({ spawnSync: () => COMMENT_OK, })) const mod = await import(TOOL_SRC + "?t=" + Date.now()) // Invalid verdict passed directly — at runtime zod would reject this, // but the loader shim does no validation. The tool builds the comment // regardless. The enum validation happens at the opencode layer (zod), // not inside execute(). This test documents that the tool itself does // not validate verdicts (delegated to zod schema). const result = await mod.default.execute({ pr_number: 45, verdict: "APROVE", body: "typo verdict", }, ctx()) // Tool still runs (no inline validation) — comment built with bad verdict. // The real guard is the zod enum in tool.schema.enum(VERDICTS). expect(result).toBe("Review posted on PR #45: verdict=APROVE") }) test("test_comment_has_heading — comment body contains '## Code Review Summary'", async () => { let capturedArgs mock.module("child_process", () => ({ spawnSync: (_cmd, args) => { capturedArgs = args return COMMENT_OK }, })) const mod = await import(TOOL_SRC + "?t=" + Date.now()) await mod.default.execute({ pr_number: 45, verdict: "APPROVE", body: "Review body", }, ctx()) const bodyIdx = capturedArgs.indexOf("--body") + 1 const comment = capturedArgs[bodyIdx] expect(comment.startsWith("## Code Review Summary\n")).toBe(true) }) test("test_comment_has_verdict — comment body contains '### Verdict: APPROVE'", async () => { let capturedArgs mock.module("child_process", () => ({ spawnSync: (_cmd, args) => { capturedArgs = args return COMMENT_OK }, })) const mod = await import(TOOL_SRC + "?t=" + Date.now()) await mod.default.execute({ pr_number: 45, verdict: "APPROVE", body: "Review body", }, ctx()) const bodyIdx = capturedArgs.indexOf("--body") + 1 const comment = capturedArgs[bodyIdx] expect(comment).toContain("### Verdict: APPROVE") }) test("test_spawnsync_args — spawnSync called with correct gh args (no --repo)", async () => { let capturedCmd let capturedArgs mock.module("child_process", () => ({ spawnSync: (cmd, args) => { capturedCmd = cmd capturedArgs = args return COMMENT_OK }, })) const mod = await import(TOOL_SRC + "?t=" + Date.now()) await mod.default.execute({ pr_number: 45, verdict: "APPROVE", body: "Review body", }, ctx()) expect(capturedCmd).toBe("gh") expect(capturedArgs[0]).toBe("pr") expect(capturedArgs[1]).toBe("comment") expect(capturedArgs[2]).toBe("45") expect(capturedArgs).toContain("--body") // --repo MUST NOT be present — gh auto-detects from cwd (PR#60). expect(capturedArgs).not.toContain("--repo") // args end with --body (no trailing --repo slaid098/...). const lastIdx = capturedArgs.length - 1 expect(capturedArgs[lastIdx - 1]).toBe("--body") expect(capturedArgs[lastIdx].startsWith("## Code Review Summary\n")).toBe(true) }) })