/** * Tests for .opencode/tools/post-docs-review.ts — the post-docs-review custom tool. * * Mirror of tests/test_post_review_tool.ts / test_commit_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_docs_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_docs_review_tool.py): * - test_valid_approve — valid APPROVE verdict → "Docs review posted" * - test_valid_fixed — valid FIXED verdict → "Docs review posted" * - test_valid_no_changes — valid NO_CHANGES verdict → "Docs review posted" * - test_invalid_verdict — invalid verdict "APROVE" (typo) → error * - test_comment_has_heading — comment body contains "## Docs 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-docs-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-docs-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: "- Project map: no structural changes\n- Handoff: valid", }, ctx()) expect(result).toBe("Docs review posted on PR #45: verdict=APPROVE") const bodyIdx = capturedArgs.indexOf("--body") + 1 expect(capturedArgs[bodyIdx]).toContain("## Docs Review Summary") expect(capturedArgs[bodyIdx]).toContain("### Verdict: APPROVE") }) test("test_valid_fixed — FIXED 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: "FIXED", body: "- Handoff: fixed: added missing section", }, ctx()) expect(result).toBe("Docs review posted on PR #45: verdict=FIXED") const bodyIdx = capturedArgs.indexOf("--body") + 1 expect(capturedArgs[bodyIdx]).toContain("### Verdict: FIXED") }) test("test_valid_no_changes — NO_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: "NO_CHANGES", body: "- Project map: no structural changes\n- Handoff: valid", }, ctx()) expect(result).toBe("Docs review posted on PR #45: verdict=NO_CHANGES") const bodyIdx = capturedArgs.indexOf("--body") + 1 expect(capturedArgs[bodyIdx]).toContain("### Verdict: NO_CHANGES") }) 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()) expect(result).toBe("Docs review posted on PR #45: verdict=APROVE") }) test("test_comment_has_heading — comment body contains '## Docs 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: "Docs body", }, ctx()) const bodyIdx = capturedArgs.indexOf("--body") + 1 const comment = capturedArgs[bodyIdx] expect(comment.startsWith("## Docs 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: "Docs 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", 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: "Docs 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") expect(capturedArgs).toContain("--repo") const repoIdx = capturedArgs.indexOf("--repo") + 1 expect(capturedArgs[repoIdx]).toBe("slaid098/opencode-config") }) })