Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
808
extensions/acpx/src/codex-auth-bridge.test.ts
Normal file
808
extensions/acpx/src/codex-auth-bridge.test.ts
Normal file
@@ -0,0 +1,808 @@
|
||||
// ACPX tests cover codex auth bridge plugin behavior.
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { prepareAcpxCodexAuthConfig } from "./codex-auth-bridge.js";
|
||||
import { resolveAcpxPluginConfig } from "./config.js";
|
||||
import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const tempDirs: string[] = [];
|
||||
const previousEnv = {
|
||||
CODEX_HOME: process.env.CODEX_HOME,
|
||||
OPENCLAW_AGENT_DIR: process.env.OPENCLAW_AGENT_DIR,
|
||||
};
|
||||
|
||||
async function makeTempDir(): Promise<string> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-acpx-codex-auth-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function quoteArg(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function restoreEnv(name: keyof typeof previousEnv): void {
|
||||
const value = previousEnv[name];
|
||||
if (value === undefined) {
|
||||
delete process.env[name];
|
||||
} else {
|
||||
process.env[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function generatedCodexPaths(stateDir: string): {
|
||||
configPath: string;
|
||||
wrapperPath: string;
|
||||
} {
|
||||
const baseDir = path.join(stateDir, "acpx");
|
||||
const codexHome = path.join(baseDir, "codex-home");
|
||||
return {
|
||||
configPath: path.join(codexHome, "config.toml"),
|
||||
wrapperPath: path.join(baseDir, "codex-acp-wrapper.mjs"),
|
||||
};
|
||||
}
|
||||
|
||||
function generatedClaudePaths(stateDir: string): {
|
||||
wrapperPath: string;
|
||||
} {
|
||||
const baseDir = path.join(stateDir, "acpx");
|
||||
return {
|
||||
wrapperPath: path.join(baseDir, "claude-agent-acp-wrapper.mjs"),
|
||||
};
|
||||
}
|
||||
|
||||
function expectCodexWrapperCommand(command: string | undefined, wrapperPath: string): void {
|
||||
expect(command).toContain(quoteArg(process.execPath));
|
||||
expect(command).toContain(quoteArg(wrapperPath));
|
||||
}
|
||||
|
||||
function expectClaudeWrapperCommand(command: string | undefined, wrapperPath: string): void {
|
||||
expect(command).toContain(quoteArg(process.execPath));
|
||||
expect(command).toContain(quoteArg(wrapperPath));
|
||||
}
|
||||
|
||||
function expectWrapperToContainPathSuffix(wrapper: string, pathSuffix: string[]): void {
|
||||
const nativeSuffix = pathSuffix.join(path.sep);
|
||||
const escapedNativeSuffix = JSON.stringify(nativeSuffix).slice(1, -1);
|
||||
const posixSuffix = pathSuffix.join("/");
|
||||
if (wrapper.includes(escapedNativeSuffix)) {
|
||||
expect(wrapper).toContain(escapedNativeSuffix);
|
||||
} else {
|
||||
expect(wrapper).toContain(posixSuffix);
|
||||
}
|
||||
}
|
||||
|
||||
async function expectPathMissing(targetPath: string): Promise<void> {
|
||||
let error: unknown;
|
||||
try {
|
||||
await fs.access(targetPath);
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as NodeJS.ErrnoException).code).toBe("ENOENT");
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
restoreEnv("CODEX_HOME");
|
||||
restoreEnv("OPENCLAW_AGENT_DIR");
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("prepareAcpxCodexAuthConfig", () => {
|
||||
it("installs an isolated Codex ACP wrapper without synthesizing auth from canonical OpenClaw OAuth", async () => {
|
||||
const root = await makeTempDir();
|
||||
const agentDir = path.join(root, "agent");
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const generatedClaude = generatedClaudePaths(stateDir);
|
||||
const installedBinPath = path.join(
|
||||
root,
|
||||
"node_modules",
|
||||
"@zed-industries",
|
||||
"codex-acp",
|
||||
"bin",
|
||||
"codex-acp.js",
|
||||
);
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
const resolved = await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => installedBinPath,
|
||||
});
|
||||
|
||||
expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath);
|
||||
expectClaudeWrapperCommand(resolved.agents.claude, generatedClaude.wrapperPath);
|
||||
await expect(fs.access(generated.wrapperPath)).resolves.toBeUndefined();
|
||||
await expect(fs.access(generatedClaude.wrapperPath)).resolves.toBeUndefined();
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain(JSON.stringify(installedBinPath));
|
||||
expect(wrapper).toContain("defaultArgs = [installedBinPath]");
|
||||
await expectPathMissing(path.join(agentDir, "acp-auth", "codex", "auth.json"));
|
||||
});
|
||||
|
||||
it("keeps generated wrappers usable when chmod is rejected by the state filesystem", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generatedCodex = generatedCodexPaths(stateDir);
|
||||
const generatedClaude = generatedClaudePaths(stateDir);
|
||||
const chmodError = Object.assign(new Error("operation not permitted"), { code: "EPERM" });
|
||||
const chmodSpy = vi.spyOn(fs, "chmod").mockRejectedValue(chmodError);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
const resolved = await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
});
|
||||
|
||||
expect(chmodSpy).toHaveBeenCalledWith(generatedCodex.wrapperPath, 0o755);
|
||||
expect(chmodSpy).toHaveBeenCalledWith(generatedClaude.wrapperPath, 0o755);
|
||||
expectCodexWrapperCommand(resolved.agents.codex, generatedCodex.wrapperPath);
|
||||
expectClaudeWrapperCommand(resolved.agents.claude, generatedClaude.wrapperPath);
|
||||
await expect(fs.access(generatedCodex.wrapperPath)).resolves.toBeUndefined();
|
||||
await expect(fs.access(generatedClaude.wrapperPath)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back to the current Codex ACP package range when the local adapter is unavailable", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => undefined,
|
||||
});
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain('"@zed-industries/codex-acp@0.16.0"');
|
||||
expect(wrapper).toContain('"--", "codex-acp"');
|
||||
expect(wrapper).not.toContain("@zed-industries/codex-acp@^0.11.1");
|
||||
});
|
||||
|
||||
it("falls back to the patched Claude ACP package when the local adapter is unavailable", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedClaudePaths(stateDir);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledClaudeAcpBinPath: async () => undefined,
|
||||
});
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.55.0"');
|
||||
expect(wrapper).toContain('"--", "claude-agent-acp"');
|
||||
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@^0.31.0");
|
||||
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.0");
|
||||
});
|
||||
|
||||
it("uses the bundled Codex ACP dependency by default when it is installed", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
});
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain("@zed-industries/codex-acp");
|
||||
expectWrapperToContainPathSuffix(wrapper, ["bin", "codex-acp.js"]);
|
||||
expect(wrapper).toContain("defaultArgs = [installedBinPath]");
|
||||
});
|
||||
|
||||
it("keeps the orphaned wrapper alive long enough to force-kill the child process group", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
});
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain('killChildTree("SIGTERM")');
|
||||
expect(wrapper).toContain('killChildTree("SIGKILL", { force: true })');
|
||||
expect(wrapper).toMatch(
|
||||
/forceKillTimer = setTimeout\(\(\) => \{\s*killChildTree\("SIGKILL", \{ force: true \}\);\s*childExitCode = 1;/s,
|
||||
);
|
||||
expect(wrapper).toMatch(
|
||||
/child\.on\("exit", \(code, signal\) => \{\s*if \(parentWatcher\) \{\s*clearInterval\(parentWatcher\);\s*\}\s*if \(orphanCleanupStarted\) \{\s*return;\s*\}/s,
|
||||
);
|
||||
expect(wrapper).toMatch(
|
||||
/child\.on\("close", \(\) => \{\s*finishStderrLog\(\);\s*process\.exit\(childExitCode\);/s,
|
||||
);
|
||||
expect(wrapper).not.toMatch(
|
||||
/forceKillTimer = setTimeout\(\(\) => killChildTree\("SIGKILL"\), 1_500\);\s*forceKillTimer\.unref\?\.\(\);\s*process\.exit\(1\);/s,
|
||||
);
|
||||
// Orphan detection must trigger on any PPID change, not only when the new
|
||||
// PPID is init (1). Systemd user services and container init reparent
|
||||
// orphaned processes to a session manager or container init (PID != 1),
|
||||
// and the older `process.ppid !== 1` guard would silently leak the codex
|
||||
// adapter tree there.
|
||||
expect(wrapper).not.toContain("process.ppid !== 1");
|
||||
expect(wrapper).toMatch(
|
||||
/setInterval\(\(\) => \{[\s\S]*?if \(process\.ppid === originalParentPid\) \{\s*return;\s*\}/,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the bundled Claude ACP dependency by default when it is installed", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedClaudePaths(stateDir);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
});
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain("@agentclientprotocol/claude-agent-acp");
|
||||
expectWrapperToContainPathSuffix(wrapper, ["dist", "index.js"]);
|
||||
expect(wrapper).toContain("defaultArgs = [installedBinPath]");
|
||||
});
|
||||
|
||||
it("launches the locally installed Codex ACP bin with isolated CODEX_HOME", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const installedBinPath = path.join(root, "codex-acp-bin.js");
|
||||
await fs.writeFile(
|
||||
installedBinPath,
|
||||
"console.log(JSON.stringify({ argv: process.argv.slice(2), codexHome: process.env.CODEX_HOME }));\n",
|
||||
"utf8",
|
||||
);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => installedBinPath,
|
||||
});
|
||||
|
||||
const { stdout } = await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
generated.wrapperPath,
|
||||
"--openclaw-acpx-lease-id",
|
||||
"lease-1",
|
||||
"--openclaw-gateway-instance-id",
|
||||
"gateway-1",
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
);
|
||||
const launched = JSON.parse(stdout.trim()) as { argv?: unknown; codexHome?: unknown };
|
||||
expect(launched.argv).toStrictEqual([]);
|
||||
const expectedCodexHome = await fs.realpath(path.join(stateDir, "acpx", "codex-home"));
|
||||
expect(path.resolve(String(launched.codexHome))).toBe(expectedCodexHome);
|
||||
});
|
||||
|
||||
it("writes API-key auth into the isolated Codex ACP home when env auth is present", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const installedBinPath = path.join(root, "codex-acp-bin.js");
|
||||
await fs.writeFile(
|
||||
installedBinPath,
|
||||
"console.log(JSON.stringify({ codexHome: process.env.CODEX_HOME }));\n",
|
||||
"utf8",
|
||||
);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => installedBinPath,
|
||||
});
|
||||
|
||||
await execFileAsync(process.execPath, [generated.wrapperPath], {
|
||||
cwd: root,
|
||||
env: { ...process.env, CODEX_API_KEY: "", OPENAI_API_KEY: "sk-test-api-key" },
|
||||
});
|
||||
|
||||
const authPath = path.join(stateDir, "acpx", "codex-home", "auth.json");
|
||||
const auth = JSON.parse(await fs.readFile(authPath, "utf8")) as {
|
||||
auth_mode?: unknown;
|
||||
OPENAI_API_KEY?: unknown;
|
||||
};
|
||||
expect(auth).toMatchObject({
|
||||
OPENAI_API_KEY: "sk-test-api-key",
|
||||
});
|
||||
expect(auth).not.toHaveProperty("auth_mode");
|
||||
if (process.platform !== "win32") {
|
||||
const mode = (await fs.stat(authPath)).mode & 0o777;
|
||||
expect(mode).toBe(0o600);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves existing isolated Codex auth when env auth is present", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const installedBinPath = path.join(root, "codex-acp-bin.js");
|
||||
await fs.writeFile(installedBinPath, "console.log('ok');\n", "utf8");
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => installedBinPath,
|
||||
});
|
||||
|
||||
const authPath = path.join(stateDir, "acpx", "codex-home", "auth.json");
|
||||
const existingAuth = {
|
||||
auth_mode: "chatgpt",
|
||||
tokens: { access_token: "existing-token" },
|
||||
last_refresh: null,
|
||||
};
|
||||
await fs.writeFile(authPath, `${JSON.stringify(existingAuth)}\n`, { mode: 0o600 });
|
||||
|
||||
await execFileAsync(process.execPath, [generated.wrapperPath], {
|
||||
cwd: root,
|
||||
env: { ...process.env, OPENAI_API_KEY: "sk-test-api-key" },
|
||||
});
|
||||
|
||||
expect(JSON.parse(await fs.readFile(authPath, "utf8"))).toEqual(existingAuth);
|
||||
});
|
||||
|
||||
it("updates existing isolated Codex API-key auth when env auth changes", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const installedBinPath = path.join(root, "codex-acp-bin.js");
|
||||
await fs.writeFile(installedBinPath, "console.log('ok');\n", "utf8");
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => installedBinPath,
|
||||
});
|
||||
|
||||
const authPath = path.join(stateDir, "acpx", "codex-home", "auth.json");
|
||||
await fs.writeFile(
|
||||
authPath,
|
||||
`${JSON.stringify({
|
||||
OPENAI_API_KEY: "sk-old-api-key",
|
||||
tokens: null,
|
||||
last_refresh: null,
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
|
||||
await execFileAsync(process.execPath, [generated.wrapperPath], {
|
||||
cwd: root,
|
||||
env: { ...process.env, CODEX_API_KEY: "sk-new-api-key", OPENAI_API_KEY: "sk-other-key" },
|
||||
});
|
||||
|
||||
expect(JSON.parse(await fs.readFile(authPath, "utf8"))).toMatchObject({
|
||||
OPENAI_API_KEY: "sk-new-api-key",
|
||||
tokens: null,
|
||||
last_refresh: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("launches the locally installed Claude ACP bin without going through npm", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedClaudePaths(stateDir);
|
||||
const installedBinPath = path.join(root, "claude-agent-acp-bin.js");
|
||||
await fs.writeFile(
|
||||
installedBinPath,
|
||||
"console.log(JSON.stringify({ argv: process.argv.slice(2), codexHome: process.env.CODEX_HOME ?? null }));\n",
|
||||
"utf8",
|
||||
);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledClaudeAcpBinPath: async () => installedBinPath,
|
||||
});
|
||||
|
||||
const { stdout } = await execFileAsync(
|
||||
process.execPath,
|
||||
[generated.wrapperPath, "--permission-mode", "bypass"],
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
);
|
||||
const launched = JSON.parse(stdout.trim()) as { argv?: unknown; codexHome?: unknown };
|
||||
expect(launched.argv).toEqual(["--permission-mode", "bypass"]);
|
||||
expect(launched.codexHome).toBeNull();
|
||||
});
|
||||
|
||||
it("does not copy source Codex auth", async () => {
|
||||
const root = await makeTempDir();
|
||||
const sourceCodexHome = path.join(root, "source-codex");
|
||||
const agentDir = path.join(root, "agent");
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
await fs.mkdir(sourceCodexHome, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(sourceCodexHome, "auth.json"),
|
||||
`${JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: "test-api-key" }, null, 2)}\n`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceCodexHome, "config.toml"),
|
||||
[
|
||||
'model = "gpt-5.5-1"',
|
||||
'model_provider = "azure_foundry"',
|
||||
'model_reasoning_effort = "high"',
|
||||
'sandbox_mode = "workspace-write"',
|
||||
'notify = ["SkyComputerUseClient", "turn-ended"]',
|
||||
"",
|
||||
"[model_providers.azure_foundry]",
|
||||
'name = "Azure Foundry"',
|
||||
'base_url = "https://example.azure.com/openai/v1"',
|
||||
'wire_api = "responses"',
|
||||
'env_key = "AZURE_OPENAI_API_KEY"',
|
||||
'http_headers = { "api-key" = "inline-secret-key" }',
|
||||
'query_params = { "api-version" = "2026-01-01", "secret" = "inline-secret-param" }',
|
||||
'experimental_bearer_token = "inline-secret-bearer"',
|
||||
"",
|
||||
"[model_providers.azure_foundry.auth]",
|
||||
'command = "bash"',
|
||||
'args = ["-lc", "printf %s test-key"]',
|
||||
"",
|
||||
"[model_providers.secret_only]",
|
||||
'experimental_bearer_token = "secret-only-token"',
|
||||
"",
|
||||
`[projects.${JSON.stringify(path.join(root, "project-with-model-key"))}]`,
|
||||
'model = "nested-project-model"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
process.env.CODEX_HOME = sourceCodexHome;
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
const resolved = await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => undefined,
|
||||
});
|
||||
|
||||
expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath);
|
||||
const isolatedConfig = await fs.readFile(generated.configPath, "utf8");
|
||||
expect(isolatedConfig).toContain('model = "gpt-5.5-1"');
|
||||
expect(isolatedConfig).toContain('model_provider = "azure_foundry"');
|
||||
expect(isolatedConfig).toContain('model_reasoning_effort = "high"');
|
||||
expect(isolatedConfig).toContain('sandbox_mode = "workspace-write"');
|
||||
expect(isolatedConfig).toContain("[model_providers.azure_foundry]");
|
||||
expect(isolatedConfig).toContain('base_url = "https://example.azure.com/openai/v1"');
|
||||
expect(isolatedConfig).toContain('env_key = "AZURE_OPENAI_API_KEY"');
|
||||
expect(isolatedConfig).not.toContain("http_headers");
|
||||
expect(isolatedConfig).not.toContain("query_params");
|
||||
expect(isolatedConfig).not.toContain("experimental_bearer_token");
|
||||
expect(isolatedConfig).not.toContain("[model_providers.azure_foundry.auth]");
|
||||
expect(isolatedConfig).not.toContain("[model_providers.secret_only]");
|
||||
expect(isolatedConfig).not.toContain("nested-project-model");
|
||||
expect(isolatedConfig).not.toContain("inline-secret");
|
||||
expect(isolatedConfig).not.toContain('args = ["-lc", "printf %s test-key"]');
|
||||
expect(isolatedConfig).not.toContain("notify");
|
||||
expect(isolatedConfig).not.toContain("SkyComputerUseClient");
|
||||
expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(root))}]`);
|
||||
expect(isolatedConfig).toContain('trust_level = "trusted"');
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain("CODEX_HOME: codexHome");
|
||||
expect(wrapper).not.toContain(sourceCodexHome);
|
||||
await expectPathMissing(path.join(agentDir, "acp-auth", "codex-source", "auth.json"));
|
||||
await expectPathMissing(path.join(agentDir, "acp-auth", "codex", "auth.json"));
|
||||
});
|
||||
|
||||
it("copies only trusted Codex project declarations into the isolated Codex home", async () => {
|
||||
const root = await makeTempDir();
|
||||
const sourceCodexHome = path.join(root, "source-codex");
|
||||
const stateDir = path.join(root, "state");
|
||||
const explicitProject = path.join(root, "explicit project");
|
||||
const inlineProject = path.join(root, "inline-project");
|
||||
const mapProject = path.join(root, "map-project");
|
||||
const untrustedProject = path.join(root, "untrusted-project");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
await fs.mkdir(sourceCodexHome, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(sourceCodexHome, "config.toml"),
|
||||
[
|
||||
'notify = ["SkyComputerUseClient", "turn-ended"]',
|
||||
`projects = { ${JSON.stringify(mapProject)} = { trust_level = "trusted" }, ${JSON.stringify(untrustedProject)} = { trust_level = "untrusted" } }`,
|
||||
"[projects]",
|
||||
`${JSON.stringify(inlineProject)} = { trust_level = "trusted" }`,
|
||||
`[projects.${JSON.stringify(explicitProject)}]`,
|
||||
'trust_level = "trusted"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
process.env.CODEX_HOME = sourceCodexHome;
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => undefined,
|
||||
});
|
||||
|
||||
const isolatedConfig = await fs.readFile(generated.configPath, "utf8");
|
||||
expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(root))}]`);
|
||||
expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(explicitProject))}]`);
|
||||
expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(inlineProject))}]`);
|
||||
expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(mapProject))}]`);
|
||||
expect(isolatedConfig).not.toContain(untrustedProject);
|
||||
expect(isolatedConfig).not.toContain("notify");
|
||||
expect(isolatedConfig).not.toContain("SkyComputerUseClient");
|
||||
});
|
||||
|
||||
it("normalizes an explicitly configured Codex ACP command to the local wrapper", async () => {
|
||||
const root = await makeTempDir();
|
||||
const sourceCodexHome = path.join(root, "source-codex");
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
await fs.mkdir(sourceCodexHome, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(sourceCodexHome, "config.toml"),
|
||||
'notify = ["SkyComputerUseClient", "turn-ended"]\n',
|
||||
);
|
||||
process.env.CODEX_HOME = sourceCodexHome;
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
agents: {
|
||||
codex: {
|
||||
command: "npx @zed-industries/codex-acp@0.12.0 -c 'model=\"gpt-5.4\"'",
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
const resolved = await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => path.join(root, "codex-acp.js"),
|
||||
});
|
||||
|
||||
expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath);
|
||||
expect(resolved.agents.codex).not.toContain("npx @zed-industries/codex-acp@0.12.0");
|
||||
expect(resolved.agents.codex).toContain(quoteArg("-c"));
|
||||
expect(resolved.agents.codex).toContain(quoteArg('model="gpt-5.4"'));
|
||||
const isolatedConfig = await fs.readFile(generated.configPath, "utf8");
|
||||
expect(isolatedConfig).not.toContain("notify");
|
||||
expect(isolatedConfig).not.toContain("SkyComputerUseClient");
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain("process.argv.slice(2)");
|
||||
expect(wrapper).toContain("CODEX_HOME: codexHome");
|
||||
expect(wrapper).not.toContain(sourceCodexHome);
|
||||
});
|
||||
|
||||
it("normalizes an explicitly configured Claude ACP npx command to the local wrapper", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedClaudePaths(stateDir);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
agents: {
|
||||
claude: {
|
||||
command: "npx -y @agentclientprotocol/claude-agent-acp@0.31.4 --permission-mode bypass",
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
const resolved = await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"),
|
||||
});
|
||||
|
||||
expectClaudeWrapperCommand(resolved.agents.claude, generated.wrapperPath);
|
||||
expect(resolved.agents.claude).not.toContain("npx -y @agentclientprotocol/claude-agent-acp");
|
||||
expect(resolved.agents.claude).toContain("--permission-mode");
|
||||
expect(resolved.agents.claude).toContain("bypass");
|
||||
});
|
||||
|
||||
it("captures Codex wrapper stderr in a stream-aware redacted per-lease log", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const generated = generatedCodexPaths(stateDir);
|
||||
const stderrScript = path.join(root, "emit-stderr.mjs");
|
||||
await fs.writeFile(
|
||||
stderrScript,
|
||||
`const chunks = [
|
||||
"token=sk-test",
|
||||
"secret1234567890\\n",
|
||||
"Authorization: Bearer bearer-secret",
|
||||
"-token-1234567890\\n",
|
||||
'{"client_secret":"json-secret-1234567890","api_key":"json-api-key-1234567890"}\\n',
|
||||
"client-secret: kebab-secret-1234567890\\n",
|
||||
"standalone sk-live-secret",
|
||||
"1234567890\\n",
|
||||
"url=https://example.test/callback?token=query-secret",
|
||||
"-1234567890\\n",
|
||||
"github_pat_1234567890",
|
||||
"abcdefghijklmnopqrstuvwxyz\\n",
|
||||
"-----BEGIN PRIVATE KEY-----\\nprivate-secret-body\\n",
|
||||
"-----END PRIVATE KEY-----\\n",
|
||||
"tail-token=tail-secret-1234567890",
|
||||
"\\n-----BEGIN PRIVATE KEY-----\\ntruncated-private-secret",
|
||||
];
|
||||
let index = 0;
|
||||
function writeNext() {
|
||||
if (index >= chunks.length) {
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
process.stderr.write(chunks[index]);
|
||||
index += 1;
|
||||
setTimeout(writeNext, 5);
|
||||
}
|
||||
writeNext();`,
|
||||
"utf8",
|
||||
);
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
agents: {
|
||||
codex: {
|
||||
command: `${process.execPath} ${stderrScript}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledCodexAcpBinPath: async () => path.join(root, "codex-acp.js"),
|
||||
});
|
||||
|
||||
await expect(
|
||||
execFileAsync(process.execPath, [
|
||||
generated.wrapperPath,
|
||||
"--openclaw-run-configured",
|
||||
process.execPath,
|
||||
stderrScript,
|
||||
OPENCLAW_ACPX_LEASE_ID_ARG,
|
||||
"lease-secret",
|
||||
OPENCLAW_GATEWAY_INSTANCE_ID_ARG,
|
||||
"gateway-test",
|
||||
]),
|
||||
).rejects.toMatchObject({ code: 1 });
|
||||
|
||||
const log = await fs.readFile(
|
||||
path.join(stateDir, "acpx", "codex-acp-wrapper.stderr.lease-secret.log"),
|
||||
"utf8",
|
||||
);
|
||||
expect(log).toContain("token=[REDACTED]");
|
||||
expect(log).toContain("Authorization: Bearer [REDACTED]");
|
||||
expect(log).toContain('"client_secret":"[REDACTED]"');
|
||||
expect(log).toContain('"api_key":"[REDACTED]"');
|
||||
expect(log).toContain("client-secret: [REDACTED]");
|
||||
expect(log).toContain("standalone [REDACTED_OPENAI_KEY]");
|
||||
expect(log).toContain("?token=[REDACTED]");
|
||||
expect(log).toContain("[REDACTED_GITHUB_TOKEN]");
|
||||
expect(log).toContain("[REDACTED_PRIVATE_KEY]");
|
||||
expect(log).toContain("tail-token=[REDACTED]");
|
||||
expect(log).not.toContain("sk-testsecret1234567890");
|
||||
expect(log).not.toContain("bearer-secret-token-1234567890");
|
||||
expect(log).not.toContain("json-secret-1234567890");
|
||||
expect(log).not.toContain("json-api-key-1234567890");
|
||||
expect(log).not.toContain("kebab-secret-1234567890");
|
||||
expect(log).not.toContain("query-secret-1234567890");
|
||||
expect(log).not.toContain("github_pat_1234567890abcdefghijklmnopqrstuvwxyz");
|
||||
expect(log).not.toContain("private-secret-body");
|
||||
expect(log).not.toContain("truncated-private-secret");
|
||||
expect(log).not.toContain("tail-secret-1234567890");
|
||||
await expectPathMissing(path.join(stateDir, "acpx", "codex-acp-wrapper.stderr.log"));
|
||||
});
|
||||
|
||||
it("leaves a custom Claude agent command alone", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
agents: {
|
||||
claude: {
|
||||
command: "node ./custom-claude-wrapper.mjs --flag",
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
const resolved = await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"),
|
||||
});
|
||||
|
||||
expect(resolved.agents.claude).toBe("node ./custom-claude-wrapper.mjs --flag");
|
||||
});
|
||||
|
||||
it("does not normalize custom Claude commands that only mention the package name", async () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const command =
|
||||
"node ./custom-claude-wrapper.mjs @agentclientprotocol/claude-agent-acp@0.31.4 --flag";
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
agents: {
|
||||
claude: {
|
||||
command,
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir: root,
|
||||
});
|
||||
|
||||
const resolved = await prepareAcpxCodexAuthConfig({
|
||||
pluginConfig,
|
||||
stateDir,
|
||||
resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"),
|
||||
});
|
||||
|
||||
expect(resolved.agents.claude).toBe(command);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user