* feat(tools): add post-review and post-docs-review tools * refactor(agents): use post-review/post-docs-review tools in reviewer and docs-reviewer * feat(permissions): add post_review and post_docs_review to agent.tools map * test(tools): add tests for post-review and post-docs-review tools * docs(handoff): add handoff ADR and project-map for review posting tools * docs(handoff): set PR number * docs: fix PR#45→PR#46 refs in project map --------- Co-authored-by: opencode-agent <agent@slaid098.dev>
182 lines
No EOL
6.8 KiB
TypeScript
182 lines
No EOL
6.8 KiB
TypeScript
/**
|
|
* 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: <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")
|
|
})
|
|
}) |