feat(opencode): 全面升级OpenCode集成 (#2251)

- 修复ecc-hooks.ts中的硬编码ECC_VERSION(从package.json读取)
- 改进错误处理机制(统一模式、详细错误信息)
- 增强类型安全(添加ToolArgs、ToolInput等类型定义)
- 改进跨平台兼容性(支持macOS、Windows、Linux)
- 添加dependency-analyzer工具(依赖分析)
- 改进format-code工具(错误处理、跨平台支持)
- 改进lint-check工具(错误处理、跨平台支持)
- 更新文档(代理26个、工具8个、命令26个)
- 添加工具测试(6个测试用例)
- 改进现有测试(7个测试用例)

所有测试通过(16/16)

Co-authored-by: Pual-LI-6 <dj2112236494@outlook.com>
This commit is contained in:
cogiwimute367-create
2026-06-16 02:01:34 +08:00
committed by GitHub
parent e53b4d9e39
commit 3a08b0c7a8
8 changed files with 807 additions and 82 deletions
+81
View File
@@ -100,6 +100,9 @@ async function main() {
assert.strictEqual(env.PACKAGE_MANAGER, "pnpm")
assert.strictEqual(env.DETECTED_LANGUAGES, "typescript,python")
assert.strictEqual(env.PRIMARY_LANGUAGE, "typescript")
// Verify ECC_VERSION is not hardcoded
assert.ok(env.ECC_VERSION !== "1.8.0", "ECC_VERSION should not be hardcoded to 1.8.0")
assert.ok(env.ECC_VERSION.match(/^\d+\.\d+\.\d+$/), "ECC_VERSION should be a valid semver version")
}
),
],
@@ -165,6 +168,84 @@ async function main() {
}
},
],
[
"permission.ask handles read-only tools correctly",
async () => withTempProject(
[],
async (projectDir) => {
const client = createClient()
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
// Test read-only tools
const readResult = await hooks["permission.ask"]({ tool: "read", args: {} })
assert.strictEqual(readResult.approved, true)
assert.strictEqual(readResult.reason, "Read-only operation")
const globResult = await hooks["permission.ask"]({ tool: "glob", args: {} })
assert.strictEqual(globResult.approved, true)
assert.strictEqual(globResult.reason, "Read-only operation")
const grepResult = await hooks["permission.ask"]({ tool: "grep", args: {} })
assert.strictEqual(grepResult.approved, true)
assert.strictEqual(grepResult.reason, "Read-only operation")
}
),
],
[
"permission.ask handles formatters correctly",
async () => withTempProject(
[],
async (projectDir) => {
const client = createClient()
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
// Test formatter tools - note: args should be the command string, not object
const prettierResult = await hooks["permission.ask"]({
tool: "bash",
args: "npx prettier --write src/index.ts"
})
console.log("prettierResult:", JSON.stringify(prettierResult))
assert.strictEqual(prettierResult.approved, true)
assert.strictEqual(prettierResult.reason, "Formatter execution")
const biomeResult = await hooks["permission.ask"]({
tool: "bash",
args: "npx @biomejs/biome format --write src/index.ts"
})
console.log("biomeResult:", JSON.stringify(biomeResult))
assert.strictEqual(biomeResult.approved, true)
assert.strictEqual(biomeResult.reason, "Formatter execution")
}
),
],
[
"permission.ask handles test execution correctly",
async () => withTempProject(
[],
async (projectDir) => {
const client = createClient()
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
// Test test execution tools
const npmTestResult = await hooks["permission.ask"]({
tool: "bash",
args: { command: "npm test" }
})
assert.strictEqual(npmTestResult.approved, true)
assert.strictEqual(npmTestResult.reason, "Test execution")
const vitestResult = await hooks["permission.ask"]({
tool: "bash",
args: { command: "npx vitest run" }
})
assert.strictEqual(vitestResult.approved, true)
assert.strictEqual(vitestResult.reason, "Test execution")
}
),
],
]
let passed = 0
+233
View File
@@ -0,0 +1,233 @@
/**
* Tests for OpenCode custom tools
*
* Tests the 7 custom tools: run-tests, check-coverage, security-audit,
* format-code, lint-check, git-summary, changed-files
*/
const assert = require("node:assert")
const fs = require("node:fs")
const os = require("node:os")
const path = require("node:path")
const { spawnSync } = require("node:child_process")
const { pathToFileURL } = require("node:url")
function runTest(name, fn) {
return Promise.resolve()
.then(fn)
.then(() => {
console.log(`${name}`)
return { passed: 1, failed: 0 }
})
.catch((error) => {
console.log(`${name}`)
console.error(` ${error.stack || error.message}`)
return { passed: 0, failed: 1 }
})
}
async function loadTools() {
const repoRoot = path.join(__dirname, "..")
const buildResult = spawnSync("node", [path.join(repoRoot, "scripts", "build-opencode.js")], {
cwd: repoRoot,
encoding: "utf8",
})
assert.strictEqual(buildResult.status, 0, buildResult.stderr || buildResult.stdout)
const toolsDir = path.join(repoRoot, ".opencode", "dist", "tools")
const tools = {}
// Load each tool
const toolFiles = [
"format-code.js",
"lint-check.js",
"git-summary.js",
"changed-files.js",
"run-tests.js",
"check-coverage.js",
"security-audit.js",
]
for (const toolFile of toolFiles) {
const toolPath = path.join(toolsDir, toolFile)
if (fs.existsSync(toolPath)) {
const toolUrl = pathToFileURL(toolPath).href
const toolModule = await import(toolUrl)
const toolName = toolFile.replace(".js", "").replace("-", "")
tools[toolName] = toolModule.default || toolModule
}
}
return tools
}
function createMockContext(projectDir) {
return {
worktree: projectDir,
directory: projectDir,
}
}
async function withTempProject(files, fn) {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-opencode-tools-"))
try {
for (const file of files) {
const filePath = path.join(projectDir, file)
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, "")
}
return await fn(projectDir)
} finally {
fs.rmSync(projectDir, { recursive: true, force: true })
}
}
async function main() {
console.log("\n=== Testing OpenCode custom tools ===\n")
const tools = await loadTools()
const tests = []
// Test format-code tool
if (tools.formatcode) {
tests.push([
"format-code: detects TypeScript formatter",
async () => withTempProject(
["tsconfig.json", "src/index.ts"],
async (projectDir) => {
const context = createMockContext(projectDir)
const result = await tools.formatcode.execute(
{ filePath: "src/index.ts" },
context
)
const parsed = JSON.parse(result)
assert.strictEqual(parsed.success, true)
assert.ok(["biome", "prettier"].includes(parsed.formatter))
assert.ok(parsed.command.includes("src/index.ts"))
}
),
])
tests.push([
"format-code: detects Python formatter",
async () => withTempProject(
["pyproject.toml", "src/main.py"],
async (projectDir) => {
const context = createMockContext(projectDir)
const result = await tools.formatcode.execute(
{ filePath: "src/main.py" },
context
)
const parsed = JSON.parse(result)
assert.strictEqual(parsed.success, true)
assert.strictEqual(parsed.formatter, "black")
assert.ok(parsed.command.includes("src/main.py"))
}
),
])
tests.push([
"format-code: handles unsupported file types",
async () => withTempProject(
["README.md"],
async (projectDir) => {
const context = createMockContext(projectDir)
const result = await tools.formatcode.execute(
{ filePath: "README.md" },
context
)
const parsed = JSON.parse(result)
// .md files are supported by prettier, so this should succeed
assert.strictEqual(parsed.success, true)
assert.strictEqual(parsed.formatter, "prettier")
assert.ok(parsed.command.includes("README.md"))
}
),
])
}
// Test lint-check tool
if (tools.lintcheck) {
tests.push([
"lint-check: detects ESLint",
async () => withTempProject(
[".eslintrc.json", "src/index.ts"],
async (projectDir) => {
const context = createMockContext(projectDir)
const result = await tools.lintcheck.execute(
{ target: "src" },
context
)
const parsed = JSON.parse(result)
assert.strictEqual(parsed.success, true)
assert.strictEqual(parsed.linter, "eslint")
assert.ok(parsed.command.includes("src"))
}
),
])
tests.push([
"lint-check: detects Biome",
async () => withTempProject(
["biome.json", "src/index.ts"],
async (projectDir) => {
const context = createMockContext(projectDir)
const result = await tools.lintcheck.execute(
{ target: "src" },
context
)
const parsed = JSON.parse(result)
assert.strictEqual(parsed.success, true)
assert.strictEqual(parsed.linter, "biome")
assert.ok(parsed.command.includes("src"))
}
),
])
}
// Test git-summary tool
if (tools.gitsummary) {
tests.push([
"git-summary: returns git information",
async () => withTempProject(
[],
async (projectDir) => {
// Initialize git repo
spawnSync("git", ["init"], { cwd: projectDir })
spawnSync("git", ["config", "user.email", "test@test.com"], { cwd: projectDir })
spawnSync("git", ["config", "user.name", "Test"], { cwd: projectDir })
// Create a file and commit
fs.writeFileSync(path.join(projectDir, "test.txt"), "test")
spawnSync("git", ["add", "test.txt"], { cwd: projectDir })
spawnSync("git", ["commit", "-m", "test commit"], { cwd: projectDir })
const context = createMockContext(projectDir)
const result = await tools.gitsummary.execute(
{ depth: 1, includeDiff: false },
context
)
const parsed = JSON.parse(result)
assert.ok(parsed.branch)
assert.ok(parsed.log)
assert.ok(parsed.log.includes("test commit"))
}
),
])
}
// Run all tests
let passed = 0
let failed = 0
for (const [name, fn] of tests) {
const result = await runTest(name, fn)
passed += result.passed
failed += result.failed
}
console.log(`\nPassed: ${passed}`)
console.log(`Failed: ${failed}`)
process.exit(failed > 0 ? 1 : 0)
}
main()