mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-09-14 19:59:52 +08:00
720492d632
* fix(opencode): support OpenCode 2.x via native tools and commands
The single-file plugin (open-code-review.ts) cannot load on OpenCode 2.x:
the 2.x loader requires a default-exported { id, effect | setup } and has
no custom-tool registration API. It also fails on 1.x for most users
because nothing installs the @opencode-ai/plugin dependency the file
imports.
Ship the same ocr_review / ocr_health features as 2.x-native custom
tools plus /ocr-review /ocr-health commands, and document the
@opencode-ai/plugin dependency step for both versions.
* fix(opencode): dual V1+V2 plugin entrypoint instead of separate files
Replace the tools/+commands/ split with the documented dual plugin form:
default-export { ...Plugin.define({ id, setup }), server }. V2 registers
ocr_review/ocr_health via ctx.tool.transform and /ocr-review//ocr-health
via ctx.command.transform, reusing the same OCR logic as V1. Add
@opencode/plugin beta devDependency and a test for the default export.
* fix(opencode): review feedback: typeless V2 import, strict schemas, tests
- Import @opencode/plugin as types only and export a plain dual object,
so OpenCode 1.x never needs the V2 beta package at runtime (verified
in the built output: only node:*, @opencode-ai/plugin imports remain).
- V2 numeric inputs now require positive integers
({ type: integer, minimum: 1 }), matching the V1 zod schema.
- V2 commands keep the V1 sentence break, skip user-defined names like
the V1 ??= guards, and resolveSessionCwd falls back to the plugin
location when the session lookup fails.
- README: single download block, corrected 30-minute tool timeout,
deduped project section.
- Track package-lock.json (drop the local ignore) so npm ci works.
- Move the V2 stub harness into the test suite (+6 tests, 29 passing).
- Verified live on OpenCode 1.18.30 sandbox: plugin loads with no
errors, single init across sessions, both commands registered once.
695 lines
21 KiB
JavaScript
695 lines
21 KiB
JavaScript
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2026 alibaba/open-code-review Contributors
|
|
|
|
import assert from "node:assert/strict"
|
|
import { access, chmod, copyFile, link, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
import { tmpdir } from "node:os"
|
|
import { delimiter, join } from "node:path"
|
|
import test from "node:test"
|
|
import { OpenCodeReviewPlugin } from "../dist/open-code-review.js"
|
|
|
|
async function loadPlugin(worktree) {
|
|
const logs = []
|
|
const hooks = await OpenCodeReviewPlugin({
|
|
client: {
|
|
app: {
|
|
log: async (entry) => logs.push(entry),
|
|
},
|
|
},
|
|
worktree,
|
|
})
|
|
return { hooks, logs }
|
|
}
|
|
|
|
async function withTemporaryDirectory(callback) {
|
|
const directory = await mkdtemp(join(tmpdir(), "ocr-opencode-test-"))
|
|
try {
|
|
return await callback(directory)
|
|
} finally {
|
|
await rm(directory, {
|
|
recursive: true,
|
|
force: true,
|
|
maxRetries: 20,
|
|
retryDelay: 50,
|
|
})
|
|
}
|
|
}
|
|
|
|
async function withFakeOcr(source, callback) {
|
|
return await withTemporaryDirectory(async (directory) => {
|
|
if (process.platform === "win32") {
|
|
const executable = join(directory, "ocr.exe")
|
|
try {
|
|
await link(process.execPath, executable)
|
|
} catch {
|
|
await copyFile(process.execPath, executable)
|
|
}
|
|
for (const command of ["review", "version", "llm"]) {
|
|
await writeFile(
|
|
join(directory, command),
|
|
`process.argv.splice(2, 0, ${JSON.stringify(command)})\n${source}\n`,
|
|
)
|
|
}
|
|
} else {
|
|
const executable = join(directory, "ocr")
|
|
await writeFile(executable, `#!/usr/bin/env node\n${source}\n`)
|
|
await chmod(executable, 0o755)
|
|
}
|
|
|
|
const previousPath = process.env.PATH
|
|
process.env.PATH = `${directory}${delimiter}${previousPath ?? ""}`
|
|
try {
|
|
return await callback(directory)
|
|
} finally {
|
|
process.env.PATH = previousPath
|
|
}
|
|
})
|
|
}
|
|
|
|
function toolContext(worktree, signal = new AbortController().signal) {
|
|
return {
|
|
worktree,
|
|
directory: worktree,
|
|
abort: signal,
|
|
}
|
|
}
|
|
|
|
async function waitForFile(path, timeoutMs = 1_000) {
|
|
const deadline = Date.now() + timeoutMs
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
await access(path)
|
|
return
|
|
} catch {
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
}
|
|
}
|
|
throw new Error(`Timed out waiting for ${path}`)
|
|
}
|
|
|
|
function isProcessRunning(pid) {
|
|
try {
|
|
process.kill(pid, 0)
|
|
return true
|
|
} catch (error) {
|
|
if (error.code === "ESRCH") return false
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function waitForProcessExit(pid, timeoutMs = 5_000) {
|
|
const deadline = Date.now() + timeoutMs
|
|
while (Date.now() < deadline) {
|
|
if (!isProcessRunning(pid)) return
|
|
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
}
|
|
throw new Error(`Process ${pid} did not exit within ${timeoutMs}ms`)
|
|
}
|
|
|
|
async function withShortOverallTimeout(timeoutMs, callback) {
|
|
const originalSetTimeout = globalThis.setTimeout
|
|
const overallTimeouts = []
|
|
globalThis.setTimeout = (handler, delay, ...args) => {
|
|
if (delay >= 60 * 1000) {
|
|
overallTimeouts.push(delay)
|
|
return originalSetTimeout(handler, timeoutMs, ...args)
|
|
}
|
|
return originalSetTimeout(handler, delay, ...args)
|
|
}
|
|
try {
|
|
await callback()
|
|
return overallTimeouts
|
|
} finally {
|
|
globalThis.setTimeout = originalSetTimeout
|
|
}
|
|
}
|
|
|
|
test("module exposes only one OpenCode plugin entry point", async () => {
|
|
const module = await import("../dist/open-code-review.js")
|
|
assert.deepEqual(Object.keys(module).sort(), ["OpenCodeReviewPlugin", "default"])
|
|
})
|
|
|
|
test("default export serves both plugin APIs", async () => {
|
|
const module = await import("../dist/open-code-review.js")
|
|
assert.equal(module.default.id, "open-code-review")
|
|
assert.equal(typeof module.default.setup, "function")
|
|
assert.equal(module.default.server, module.OpenCodeReviewPlugin)
|
|
})
|
|
|
|
test("plugin registers tools and preserves existing user commands", async () => {
|
|
const { hooks, logs } = await loadPlugin("/tmp/project")
|
|
|
|
assert.deepEqual(Object.keys(hooks.tool).sort(), ["ocr_health", "ocr_review"])
|
|
assert.equal(logs.length, 1)
|
|
|
|
const config = {
|
|
command: {
|
|
"ocr-review": {
|
|
template: "Keep my custom review command.",
|
|
},
|
|
},
|
|
}
|
|
await hooks.config(config)
|
|
assert.equal(config.command["ocr-review"].template, "Keep my custom review command.")
|
|
assert.match(config.command["ocr-health"].template, /ocr_health/)
|
|
})
|
|
|
|
test("plugin still registers tools when the telemetry log call fails", async () => {
|
|
const hooks = await OpenCodeReviewPlugin({
|
|
client: {
|
|
app: {
|
|
log: async () => {
|
|
throw new Error("log service unavailable")
|
|
},
|
|
},
|
|
},
|
|
worktree: "/tmp/project",
|
|
})
|
|
assert.deepEqual(Object.keys(hooks.tool).sort(), ["ocr_health", "ocr_review"])
|
|
})
|
|
|
|
test("fake OCR helper removes its temporary directory", async () => {
|
|
let temporaryDirectory
|
|
await withFakeOcr("", async (directory) => {
|
|
temporaryDirectory = directory
|
|
})
|
|
await assert.rejects(access(temporaryDirectory), { code: "ENOENT" })
|
|
})
|
|
|
|
test("ocr_review creates agent-friendly workspace arguments", async () => {
|
|
await withFakeOcr(
|
|
"console.log(JSON.stringify({status:'success', argv:process.argv.slice(2)}))",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const output = await hooks.tool.ocr_review.execute(
|
|
{ background: "Add rate limiting", timeoutMinutes: 30 },
|
|
toolContext(worktree),
|
|
)
|
|
assert.deepEqual(JSON.parse(output).argv, [
|
|
"review",
|
|
"--audience",
|
|
"agent",
|
|
"--format",
|
|
"json",
|
|
"--repo",
|
|
worktree,
|
|
"--background",
|
|
"Add rate limiting",
|
|
"--timeout",
|
|
"30",
|
|
])
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review passes suspicious-looking refs as one argv value without a shell", async () => {
|
|
await withFakeOcr(
|
|
"console.log(JSON.stringify({status:'success', argv:process.argv.slice(2)}))",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const output = await hooks.tool.ocr_review.execute(
|
|
{
|
|
commit: "main; touch /tmp/unsafe",
|
|
exclude: "**/*.generated.ts,dist/**",
|
|
},
|
|
toolContext(worktree),
|
|
)
|
|
assert.deepEqual(JSON.parse(output).argv, [
|
|
"review",
|
|
"--audience",
|
|
"agent",
|
|
"--format",
|
|
"json",
|
|
"--repo",
|
|
worktree,
|
|
"--commit",
|
|
"main; touch /tmp/unsafe",
|
|
"--exclude",
|
|
"**/*.generated.ts,dist/**",
|
|
])
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review rejects incompatible review targets before starting OCR", async () => {
|
|
await withTemporaryDirectory(async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute(
|
|
{ commit: "abc", from: "main", to: "feature" },
|
|
toolContext(worktree),
|
|
),
|
|
/either 'commit' or a 'from'\/'to' range/,
|
|
)
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute(
|
|
{ from: "main" },
|
|
toolContext(worktree),
|
|
),
|
|
/Both 'from' and 'to'/,
|
|
)
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute(
|
|
{ preview: true, resume: "session-1" },
|
|
toolContext(worktree),
|
|
),
|
|
/cannot be used together/,
|
|
)
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute(
|
|
{ resume: "session-1", commit: "abc" },
|
|
toolContext(worktree),
|
|
),
|
|
/'resume' cannot be combined/,
|
|
)
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute(
|
|
{ resume: "session-1", from: "main", to: "feature" },
|
|
toolContext(worktree),
|
|
),
|
|
/'resume' cannot be combined/,
|
|
)
|
|
})
|
|
})
|
|
|
|
test("preview omits JSON mode and adds --preview", async () => {
|
|
await withFakeOcr(
|
|
"console.log(process.argv.slice(2).join('\\n'))",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const output = await hooks.tool.ocr_review.execute(
|
|
{ preview: true },
|
|
toolContext(worktree),
|
|
)
|
|
assert.deepEqual(output.split("\n"), [
|
|
"review",
|
|
"--audience",
|
|
"agent",
|
|
"--repo",
|
|
worktree,
|
|
"--preview",
|
|
])
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review reports non-zero exits with OCR output", async () => {
|
|
await withFakeOcr(
|
|
"console.error('missing credentials'); process.exit(7)",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute({}, toolContext(worktree)),
|
|
(error) => {
|
|
assert.equal(error.name, "OcrExecutionError")
|
|
assert.equal(error.exitCode, 7)
|
|
assert.match(error.message, /missing credentials/)
|
|
return true
|
|
},
|
|
)
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review explains how to install a missing OCR executable", async () => {
|
|
await withTemporaryDirectory(async (directory) => {
|
|
const previousPath = process.env.PATH
|
|
process.env.PATH = directory
|
|
try {
|
|
const { hooks } = await loadPlugin(directory)
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute({}, toolContext(directory)),
|
|
/npm install -g @alibaba-group\/open-code-review/,
|
|
)
|
|
} finally {
|
|
process.env.PATH = previousPath
|
|
}
|
|
})
|
|
})
|
|
|
|
test("ocr_review terminates when OpenCode cancels the tool", async () => {
|
|
await withFakeOcr(
|
|
"setInterval(() => {}, 1000)",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const controller = new AbortController()
|
|
setTimeout(() => controller.abort(), 20)
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute(
|
|
{},
|
|
toolContext(worktree, controller.signal),
|
|
),
|
|
/cancelled by OpenCode/,
|
|
)
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review force-kills a child that ignores cancellation", async () => {
|
|
await withFakeOcr(
|
|
[
|
|
"require('node:fs').writeFileSync('ocr-child.pid', String(process.pid))",
|
|
"process.on('SIGTERM', () => {})",
|
|
"setInterval(() => {}, 1000)",
|
|
].join("\n"),
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const controller = new AbortController()
|
|
const execution = hooks.tool.ocr_review.execute(
|
|
{},
|
|
toolContext(worktree, controller.signal),
|
|
)
|
|
const pidPath = join(worktree, "ocr-child.pid")
|
|
await waitForFile(pidPath)
|
|
const pid = Number(await readFile(pidPath, "utf8"))
|
|
|
|
controller.abort()
|
|
await assert.rejects(execution, /cancelled by OpenCode/)
|
|
try {
|
|
await waitForProcessExit(pid)
|
|
} finally {
|
|
if (isProcessRunning(pid)) {
|
|
process.kill(pid, "SIGKILL")
|
|
}
|
|
}
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review kills the whole process group on cancellation", { skip: process.platform === "win32" }, async () => {
|
|
const grandchildSource = [
|
|
"require('node:fs').writeFileSync('grandchild.pid', String(process.pid))",
|
|
"setInterval(() => {}, 1000)",
|
|
].join("\n")
|
|
await withFakeOcr(
|
|
[
|
|
"const { spawn } = require('node:child_process')",
|
|
`const g = spawn(process.execPath, ['-e', ${JSON.stringify(grandchildSource)}], { stdio: 'ignore' })`,
|
|
"g.unref()",
|
|
"setInterval(() => {}, 1000)",
|
|
].join("\n"),
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const controller = new AbortController()
|
|
const execution = hooks.tool.ocr_review.execute(
|
|
{},
|
|
toolContext(worktree, controller.signal),
|
|
)
|
|
const pidPath = join(worktree, "grandchild.pid")
|
|
await waitForFile(pidPath)
|
|
const pid = Number(await readFile(pidPath, "utf8"))
|
|
|
|
controller.abort()
|
|
await assert.rejects(execution, /cancelled by OpenCode/)
|
|
try {
|
|
await waitForProcessExit(pid)
|
|
} finally {
|
|
if (isProcessRunning(pid)) {
|
|
process.kill(pid, "SIGKILL")
|
|
}
|
|
}
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review defaults to 30-minute overall timeout", async () => {
|
|
await withFakeOcr(
|
|
"console.log('{\"status\":\"success\",\"findings\":[]}')",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const originalSetTimeout = globalThis.setTimeout
|
|
const overallTimeouts = []
|
|
globalThis.setTimeout = (handler, delay, ...args) => {
|
|
if (delay >= 60 * 1000) {
|
|
overallTimeouts.push(delay)
|
|
}
|
|
return originalSetTimeout(handler, delay, ...args)
|
|
}
|
|
try {
|
|
await hooks.tool.ocr_review.execute({}, toolContext(worktree))
|
|
} finally {
|
|
globalThis.setTimeout = originalSetTimeout
|
|
}
|
|
assert.deepEqual(overallTimeouts, [30 * 60 * 1000])
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review keeps per-file and overall timeouts independent", async () => {
|
|
await withFakeOcr(
|
|
"setInterval(() => {}, 1000)",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const overallTimeouts = await withShortOverallTimeout(20, async () => {
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute(
|
|
{ timeoutMinutes: 30, overallTimeoutMinutes: 45 },
|
|
toolContext(worktree),
|
|
),
|
|
/timed out after 2700 seconds/,
|
|
)
|
|
})
|
|
assert.deepEqual(overallTimeouts, [45 * 60 * 1000])
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review enforces one output limit across stdout and stderr", async () => {
|
|
await withFakeOcr(
|
|
[
|
|
"process.stdout.write('a'.repeat(6 * 1024 * 1024))",
|
|
"process.stderr.write('b'.repeat(6 * 1024 * 1024))",
|
|
].join("\n"),
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute({}, toolContext(worktree)),
|
|
/output exceeded the 10485760-byte safety limit/,
|
|
)
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review rejects invalid JSON output", async () => {
|
|
await withFakeOcr(
|
|
"console.log('not json')",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
await assert.rejects(
|
|
hooks.tool.ocr_review.execute({}, toolContext(worktree)),
|
|
/invalid JSON/,
|
|
)
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_review reports no output instead of invalid JSON when OCR prints nothing", async () => {
|
|
await withFakeOcr("", async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const output = await hooks.tool.ocr_review.execute({}, toolContext(worktree))
|
|
assert.match(output, /No changes detected/)
|
|
})
|
|
})
|
|
|
|
test("ocr_review preserves valid JSON after validation", async () => {
|
|
await withFakeOcr(
|
|
"console.log('{\"status\":\"success\",\"findings\":[]}')",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const output = await hooks.tool.ocr_review.execute(
|
|
{},
|
|
toolContext(worktree),
|
|
)
|
|
assert.equal(output, "{\"status\":\"success\",\"findings\":[]}")
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_health reports both version success and LLM failure", async () => {
|
|
await withFakeOcr(
|
|
[
|
|
"if (process.argv[2] === 'version') {",
|
|
" console.log('OpenCodeReview 1.2.3')",
|
|
"} else {",
|
|
" console.error('missing LLM credentials')",
|
|
" process.exitCode = 7",
|
|
"}",
|
|
].join("\n"),
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const output = await hooks.tool.ocr_health.execute(
|
|
{},
|
|
toolContext(worktree),
|
|
)
|
|
assert.match(output, /OpenCodeReview 1\.2\.3/)
|
|
assert.match(output, /LLM connection check failed: missing LLM credentials/)
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_health works when no abort signal is provided", async () => {
|
|
await withFakeOcr(
|
|
[
|
|
"if (process.argv[2] === 'version') {",
|
|
" console.log('OpenCodeReview 1.2.3')",
|
|
"} else {",
|
|
" console.error('missing LLM credentials')",
|
|
" process.exitCode = 7",
|
|
"}",
|
|
].join("\n"),
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const output = await hooks.tool.ocr_health.execute(
|
|
{},
|
|
{ worktree, directory: worktree },
|
|
)
|
|
assert.match(output, /OpenCodeReview 1\.2\.3/)
|
|
assert.match(output, /LLM connection check failed: missing LLM credentials/)
|
|
},
|
|
)
|
|
})
|
|
|
|
test("ocr_health preserves OpenCode cancellation", async () => {
|
|
await withFakeOcr(
|
|
"setInterval(() => {}, 1000)",
|
|
async (worktree) => {
|
|
const { hooks } = await loadPlugin(worktree)
|
|
const controller = new AbortController()
|
|
const execution = hooks.tool.ocr_health.execute(
|
|
{},
|
|
toolContext(worktree, controller.signal),
|
|
)
|
|
setTimeout(() => controller.abort(), 20)
|
|
await assert.rejects(execution, /cancelled by OpenCode/)
|
|
},
|
|
)
|
|
})
|
|
|
|
function stubV2Context({ directory, commands = [], failSessionLookup = false } = {}) {
|
|
const tools = []
|
|
const addedCommands = []
|
|
const prompts = []
|
|
return {
|
|
ctx: {
|
|
tool: {
|
|
transform: async (callback) => callback({
|
|
add: (definition) => tools.push(definition),
|
|
list: () => [],
|
|
get: () => undefined,
|
|
update: () => {},
|
|
remove: () => {},
|
|
namespace: () => {},
|
|
}),
|
|
reload: async () => {},
|
|
},
|
|
command: {
|
|
list: async () => ({ data: commands.map((name) => ({ name })) }),
|
|
transform: async (callback) => callback({
|
|
add: (definition) => addedCommands.push(definition),
|
|
}),
|
|
reload: async () => {},
|
|
},
|
|
session: {
|
|
get: async () => {
|
|
if (failSessionLookup) throw new Error("session gone")
|
|
return { location: { directory } }
|
|
},
|
|
prompt: async (input) => {
|
|
prompts.push(input)
|
|
return {}
|
|
},
|
|
},
|
|
location: { directory },
|
|
},
|
|
tools,
|
|
addedCommands,
|
|
prompts,
|
|
}
|
|
}
|
|
|
|
function v2ToolContext() {
|
|
return {
|
|
sessionID: "session-test",
|
|
agent: "build",
|
|
messageID: "message-test",
|
|
id: "call-test",
|
|
progress: async () => {},
|
|
}
|
|
}
|
|
|
|
test("v2 setup registers both tools and both commands", async () => {
|
|
const module = await import("../dist/open-code-review.js")
|
|
const { ctx, tools, addedCommands } = stubV2Context({ directory: "/tmp/project" })
|
|
await module.default.setup(ctx)
|
|
assert.deepEqual(tools.map((definition) => definition.name).sort(), ["ocr_health", "ocr_review"])
|
|
assert.deepEqual(addedCommands.map((definition) => definition.name).sort(), ["ocr-health", "ocr-review"])
|
|
})
|
|
|
|
test("v2 numeric inputs require positive integers", async () => {
|
|
const module = await import("../dist/open-code-review.js")
|
|
const { ctx, tools } = stubV2Context({ directory: "/tmp/project" })
|
|
await module.default.setup(ctx)
|
|
const review = tools.find((definition) => definition.name === "ocr_review")
|
|
for (const field of ["concurrency", "timeoutMinutes", "overallTimeoutMinutes", "maxTools", "maxGitProcesses"]) {
|
|
assert.deepEqual(
|
|
{ type: review.input.properties[field].type, minimum: review.input.properties[field].minimum },
|
|
{ type: "integer", minimum: 1 },
|
|
)
|
|
}
|
|
})
|
|
|
|
test("v2 preserves user-defined commands", async () => {
|
|
const module = await import("../dist/open-code-review.js")
|
|
const { ctx, addedCommands } = stubV2Context({ directory: "/tmp/project", commands: ["ocr-review"] })
|
|
await module.default.setup(ctx)
|
|
assert.deepEqual(addedCommands.map((definition) => definition.name), ["ocr-health"])
|
|
})
|
|
|
|
test("v2 ocr_review preview resolves cwd from the session location", async () => {
|
|
await withFakeOcr(
|
|
"console.log(process.argv.slice(2).join('\\n'))",
|
|
async (worktree) => {
|
|
const module = await import("../dist/open-code-review.js")
|
|
const { ctx, tools } = stubV2Context({ directory: worktree })
|
|
await module.default.setup(ctx)
|
|
const review = tools.find((definition) => definition.name === "ocr_review")
|
|
const output = await review.execute({ preview: true }, v2ToolContext())
|
|
assert.deepEqual(output.content.split("\n"), [
|
|
"review",
|
|
"--audience",
|
|
"agent",
|
|
"--repo",
|
|
worktree,
|
|
"--preview",
|
|
])
|
|
},
|
|
)
|
|
})
|
|
|
|
test("v2 falls back to the plugin location when session lookup fails", async () => {
|
|
await withFakeOcr(
|
|
"console.log(process.argv.slice(2).join('\\n'))",
|
|
async (worktree) => {
|
|
const module = await import("../dist/open-code-review.js")
|
|
const { ctx, tools } = stubV2Context({ directory: worktree, failSessionLookup: true })
|
|
await module.default.setup(ctx)
|
|
const review = tools.find((definition) => definition.name === "ocr_review")
|
|
const output = await review.execute({ preview: true }, v2ToolContext())
|
|
assert.match(output.content, new RegExp(`--repo\n${worktree}\n--preview`))
|
|
},
|
|
)
|
|
})
|
|
|
|
test("v2 ocr-review command renders review intent with sentence break", async () => {
|
|
const module = await import("../dist/open-code-review.js")
|
|
const { ctx, addedCommands, prompts } = stubV2Context({ directory: "/tmp/project" })
|
|
await module.default.setup(ctx)
|
|
const command = addedCommands.find((definition) => definition.name === "ocr-review")
|
|
await command.execute({ sessionID: "session-test", prompt: { text: "my staged changes" }, delivery: "steer" })
|
|
assert.match(prompts[0].text, /business context:my staged changes\. If no target is specified/)
|
|
await command.execute({ sessionID: "session-test", prompt: {}, delivery: "steer" })
|
|
assert.match(prompts[1].text, /business context:\. If no target is specified/)
|
|
assert.doesNotMatch(prompts[1].text, / /)
|
|
})
|