opencode-config/tests/test_commit_tool.ts
Sergey 1db2c800d4
feat(tools): add commit, create-pr, create-issue tools (#38)
* feat(tools): add commit tool with validation

* feat(tools): add create-pr tool with validation

* feat(tools): add create-issue tool with validation

* test(tools): add tests for validation tools

* docs(handoff): add handoff and ADR for validation tools

* docs(handoff): set PR number

* docs: update project map + fix ADR-015 typo

---------

Co-authored-by: opencode-agent <agent@slaid098.dev>
2026-07-24 04:17:44 +03:00

110 lines
No EOL
4.7 KiB
TypeScript

/**
* Tests for .opencode/tools/commit.ts — the commit custom tool.
*
* Mirror of tests/test_pipeline_status_tool.ts / test_memory_setup_tool.ts:
* the tool is a spawnSync wrapper around `git commit` with format validation.
*
* 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_commit_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_commit_tool.py):
* - test_valid_commit — valid message + staged files → "Committed: <msg>"
* - test_no_scope — missing scope → error with rules
* - test_cyrillic — Cyrillic in message → error
* - test_multiline — message with \n → error
* - test_too_long — description >72 chars → error
* - test_no_staged — no staged files → error
* - test_wrong_type — type "wip" → error
*/
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", "commit.ts")
function ctx() {
return {
sessionID: "t", messageID: "t", agent: "t",
directory: ".", worktree: ".",
abort: new AbortController().signal,
metadata() {}, async ask() {},
}
}
describe("commit tool", () => {
test("test_valid_commit — valid message + staged files succeeds", async () => {
// commit.ts makes 2 spawnSync calls: git diff --cached, git commit.
// First: staged files exist (non-empty stdout). Second: commit success.
let callIdx = 0
mock.module("child_process", () => ({
spawnSync: () => {
callIdx++
if (callIdx === 1) return { status: 0, stdout: "file.txt\n", stderr: "" }
return { status: 0, stdout: "", stderr: "" }
},
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({ message: "feat(tools): add commit tool" }, ctx())
expect(result).toBe("Committed: feat(tools): add commit tool")
})
test("test_no_scope — missing scope → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "file.txt\n", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({ message: "feat: no scope here" }, ctx())
expect(result).toContain("must match format")
})
test("test_cyrillic — Cyrillic in message → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "file.txt\n", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({ message: "feat(тест): привет" }, ctx())
expect(result).toContain("must be in English")
})
test("test_multiline — message with newline → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "file.txt\n", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({ message: "feat(tools): line1\nline2" }, ctx())
expect(result).toContain("must be single-line")
})
test("test_too_long — description >72 chars → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "file.txt\n", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const longDesc = "x".repeat(73)
const result = await mod.default.execute({ message: `feat(tools): ${longDesc}` }, ctx())
expect(result).toContain("must match format")
})
test("test_no_staged — no staged files → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({ message: "feat(tools): valid message" }, ctx())
expect(result).toContain("No staged files")
})
test("test_wrong_type — type 'wip' → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "file.txt\n", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({ message: "wip(tools): not a valid type" }, ctx())
expect(result).toContain("must match format")
})
})