Files
ECC/tests/opencode-tools.test.js
T
591ab5cbd3 fix(opencode): don't crash the whole session when plugins/lib is missing (#2538)
* fix(opencode): don't crash the whole session when plugins/lib is missing (#2530)

* fix: address review feedback (trailing newlines, symptom wording)

* fix(opencode): also lazy-load the store in ecc-hooks.ts plugin entrypoint

* test(opencode): add regression coverage for missing/present plugins/lib in ecc-hooks

* fix(opencode): guard diagnostic log() against unhandled rejection; tighten test assertion

* fix(opencode): only publish store after init succeeds; scrub loader errors from warning; catch sync log throws

* fix(opencode): scrub raw loader error from changed-files tool; add tool-level regression tests

* fix(ci): make OpenCode checks cross-platform

---------

Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
2026-07-28 21:37:41 -04:00

330 lines
10 KiB
JavaScript

/**
* 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: normalizes Windows backslash paths to forward slashes",
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(parsed.command.includes("src/index.ts"), `expected forward slashes in command: ${parsed.command}`)
assert.ok(!parsed.command.includes("src\\index.ts"), `unexpected backslashes in command: ${parsed.command}`)
}
),
])
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"))
}
),
])
}
// Test changed-files tool
if (tools.changedfiles) {
tests.push([
"changed-files: reports an actionable, scrubbed error when plugins/lib is missing",
async () => withTempProject([], async (projectDir) => {
const repoRoot = path.join(__dirname, "..")
const libDir = path.join(repoRoot, ".opencode", "dist", "plugins", "lib")
const backupDir = path.join(
repoRoot,
".opencode",
"dist",
"plugins",
"lib.missing-store-test-backup"
)
fs.renameSync(libDir, backupDir)
try {
const context = createMockContext(projectDir)
await assert.rejects(
() => tools.changedfiles.execute({}, context),
(error) => {
assert.ok(error instanceof Error)
assert.ok(
error.message.includes("ecc repair --target opencode"),
"Expected the error to point at the repair command"
)
assert.ok(
!error.message.includes(repoRoot),
"Error message must not leak the local filesystem path"
)
assert.ok(
!error.message.includes("Original error"),
"Error message must not include the raw underlying loader error"
)
return true
}
)
} finally {
fs.renameSync(backupDir, libDir)
}
}),
])
tests.push([
"changed-files: renders tracked changes once plugins/lib is present",
async () => withTempProject([], async (projectDir) => {
const repoRoot = path.join(__dirname, "..")
const storeUrl = pathToFileURL(
path.join(repoRoot, ".opencode", "dist", "plugins", "lib", "changed-files-store.js")
).href
const store = await import(storeUrl)
store.initStore(projectDir)
store.clearChanges()
store.recordChange("src/example.ts", "modified")
store.recordChange("src/new-file.ts", "added")
try {
const context = createMockContext(projectDir)
const result = await tools.changedfiles.execute({ format: "json" }, context)
const parsed = JSON.parse(result)
assert.strictEqual(parsed.changed, true)
assert.ok(
parsed.files.some(
(f) =>
f.path === path.normalize("src/example.ts") && f.changeType === "modified"
)
)
assert.ok(
parsed.files.some(
(f) => f.path === path.normalize("src/new-file.ts") && f.changeType === "added"
)
)
} finally {
store.clearChanges()
}
}),
])
}
// 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()