diff --git a/scripts/e2e/oca-codex-telegram-proof.ts b/scripts/e2e/oca-codex-telegram-proof.ts index bc7b5d56..fae838a1 100644 --- a/scripts/e2e/oca-codex-telegram-proof.ts +++ b/scripts/e2e/oca-codex-telegram-proof.ts @@ -2,7 +2,9 @@ import { spawn, spawnSync } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { + accessSync, chmodSync, + constants, copyFileSync, existsSync, mkdirSync, @@ -386,12 +388,27 @@ export function resolveProofOutputDir(outputDir: string): string { return resolved; } -function commandExists(command: string): boolean { - if (command.includes("/") || command.includes("\\")) return existsSync(expandHome(command)); - const result = spawnSync("sh", ["-c", `command -v "$1" >/dev/null 2>&1`, "sh", command], { - stdio: "ignore", +export function commandExists(command: string): boolean { + const pathEntries = process.env.PATH === undefined + ? (process.platform === "win32" ? [] : ["/usr/bin", "/bin"]) + : process.env.PATH.split(path.delimiter).map((directory) => directory || "."); + const candidates = command.includes("/") || command.includes("\\") + ? [expandHome(command)] + : pathEntries.flatMap((directory) => { + const base = path.join(directory, command); + if (process.platform !== "win32") return [base]; + const extensions = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean); + return [base, ...extensions.map((extension) => `${base}${extension.toLowerCase()}`)]; + }); + + return candidates.some((candidate) => { + try { + accessSync(candidate, constants.X_OK); + return statSync(candidate).isFile(); + } catch { + return false; + } }); - return result.status === 0; } function shellQuote(value: string): string { diff --git a/src/config.ts b/src/config.ts index 21eaad33..74f7724b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -372,7 +372,11 @@ export function resolveAgentChannel(workdir: string): string | undefined { const mapping = pluginConfig.agentChannels; if (!mapping) return undefined; - const normalise = (p: string) => p.replace(/\/+$/, ""); + const normalise = (p: string) => { + let end = p.length; + while (end > 0 && p[end - 1] === "/") end -= 1; + return p.slice(0, end); + }; const normWorkdir = normalise(workdir); const entries = Object.entries(mapping).sort((a, b) => b[0].length - a[0].length); diff --git a/tests/config.test.ts b/tests/config.test.ts index 9e798273..d80d3b27 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -47,6 +47,12 @@ describe("resolveAgentChannel", () => { assert.equal(resolveAgentChannel("/home/user"), "telegram|bot1|123"); }); + it("normalizes long runs of trailing slashes in linear time", () => { + const trailingSlashes = "/".repeat(100_000); + setPluginConfig({ agentChannels: { [`/home/user${trailingSlashes}`]: "telegram|bot1|123" } }); + assert.equal(resolveAgentChannel(`/home/user${trailingSlashes}`), "telegram|bot1|123"); + }); + it("returns undefined for non-matching path", () => { setPluginConfig({ agentChannels: { "/home/user/project": "telegram|bot1|123" } }); assert.equal(resolveAgentChannel("/other/path"), undefined); diff --git a/tests/oca-codex-telegram-proof.test.ts b/tests/oca-codex-telegram-proof.test.ts index 82b7568e..54098ddf 100644 --- a/tests/oca-codex-telegram-proof.test.ts +++ b/tests/oca-codex-telegram-proof.test.ts @@ -1,10 +1,11 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { delimiter, join } from "node:path"; import { + commandExists, buildProofPlan, collectDoctorChecks, parseArgs, @@ -102,6 +103,30 @@ describe("OCA Codex Telegram proof runner", () => { } }); + it("checks executable paths without interpreting shell syntax", () => { + assert.equal(commandExists(process.execPath), true); + assert.equal(commandExists("definitely-missing-command; true"), false); + }); + + it("preserves POSIX empty PATH entries as the current directory", { skip: process.platform === "win32" }, () => { + const originalCwd = process.cwd(); + const originalPath = process.env.PATH; + const directory = mkdtempSync(join(tmpdir(), "oca-proof-command-path-")); + const command = "oca-proof-local-command"; + try { + writeFileSync(join(directory, command), "#!/bin/sh\nexit 0\n"); + chmodSync(join(directory, command), 0o700); + process.chdir(directory); + process.env.PATH = `${delimiter}${originalPath ?? ""}`; + assert.equal(commandExists(command), true); + } finally { + process.chdir(originalCwd); + if (originalPath === undefined) delete process.env.PATH; + else process.env.PATH = originalPath; + rmSync(directory, { recursive: true, force: true }); + } + }); + it("prints a redacted proof plan without exposing Convex secret values", () => { const originalSecret = process.env.OPENCLAW_QA_CONVEX_SECRET_CI; const originalSite = process.env.OPENCLAW_QA_CONVEX_SITE_URL;