Files
affaan-m--everything-claude…/tests/opencode-plugin-hooks.test.js
T
wehub-resource-sync d48cda4081
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:55:55 +08:00

265 lines
9.1 KiB
JavaScript

/**
* Tests for the published OpenCode hook plugin surface.
*/
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 loadPlugin() {
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 pluginUrl = pathToFileURL(
path.join(repoRoot, ".opencode", "dist", "plugins", "ecc-hooks.js")
).href
return import(pluginUrl)
}
function createClient() {
const logs = []
return {
logs,
app: {
log: ({ body }) => {
logs.push(body)
return Promise.resolve()
},
},
}
}
function createFailingShell() {
const calls = []
const shell = (strings, ...values) => {
calls.push(String.raw({ raw: strings }, ...values))
const error = new Error("OpenCode plugin file probes must not use shell commands")
return {
then: (_resolve, reject) => reject(error),
text: async () => {
throw error
},
}
}
shell.calls = calls
return shell
}
async function withTempProject(files, fn) {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-opencode-plugin-"))
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 plugin hooks ===\n")
const { ECCHooksPlugin } = await loadPlugin()
const tests = [
[
"shell.env detects project markers without shelling out to test -f",
async () => withTempProject(
["pnpm-lock.yaml", "tsconfig.json", "pyproject.toml"],
async (projectDir) => {
const client = createClient()
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
const env = await hooks["shell.env"]()
assert.deepStrictEqual($.calls, [], `Unexpected shell probes: ${$.calls.join(", ")}`)
assert.strictEqual(env.PROJECT_ROOT, projectDir)
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")
}
),
],
[
"session.created checks CLAUDE.md through fs instead of shell test",
async () => withTempProject(["CLAUDE.md"], async (projectDir) => {
const client = createClient()
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
await hooks["session.created"]()
assert.deepStrictEqual($.calls, [], `Unexpected shell probes: ${$.calls.join(", ")}`)
assert.ok(
client.logs.some((entry) => entry.message === "[ECC] Found CLAUDE.md - loading project context"),
"Expected CLAUDE.md detection log"
)
}),
],
[
"session.created ignores directories named CLAUDE.md",
async () => {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-opencode-plugin-"))
try {
fs.mkdirSync(path.join(projectDir, "CLAUDE.md"))
const client = createClient()
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
await hooks["session.created"]()
assert.deepStrictEqual($.calls, [], `Unexpected shell probes: ${$.calls.join(", ")}`)
assert.ok(
!client.logs.some((entry) => entry.message === "[ECC] Found CLAUDE.md - loading project context"),
"Directory named CLAUDE.md should not be treated as project context"
)
} finally {
fs.rmSync(projectDir, { recursive: true, force: true })
}
},
],
[
"shell.env ignores directories named like lockfiles and language markers",
async () => {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-opencode-plugin-"))
try {
fs.mkdirSync(path.join(projectDir, "pnpm-lock.yaml"))
fs.mkdirSync(path.join(projectDir, "tsconfig.json"))
const client = createClient()
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
const env = await hooks["shell.env"]()
assert.deepStrictEqual($.calls, [], `Unexpected shell probes: ${$.calls.join(", ")}`)
assert.ok(!("PACKAGE_MANAGER" in env), "Lockfile directory should not set PACKAGE_MANAGER")
assert.ok(!("DETECTED_LANGUAGES" in env), "Marker directory should not set DETECTED_LANGUAGES")
assert.ok(!("PRIMARY_LANGUAGE" in env), "Marker directory should not set PRIMARY_LANGUAGE")
} finally {
fs.rmSync(projectDir, { recursive: true, force: true })
}
},
],
[
"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
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()