


























@@ -21,6 +21,10 @@ async function makeTempDir(): Promise<string> {
2121return dir;
2222}
232324+function quoteArg(value: string): string {
25+return JSON.stringify(value);
26+}
27+2428function restoreEnv(name: keyof typeof previousEnv): void {
2529const value = previousEnv[name];
2630if (value === undefined) {
@@ -42,11 +46,25 @@ function generatedCodexPaths(stateDir: string): {
4246};
4347}
444849+function generatedClaudePaths(stateDir: string): {
50+wrapperPath: string;
51+} {
52+const baseDir = path.join(stateDir, "acpx");
53+return {
54+wrapperPath: path.join(baseDir, "claude-agent-acp-wrapper.mjs"),
55+};
56+}
57+4558function expectCodexWrapperCommand(command: string | undefined, wrapperPath: string): void {
4659expect(command).toContain(process.execPath);
4760expect(command).toContain(wrapperPath);
4861}
496263+function expectClaudeWrapperCommand(command: string | undefined, wrapperPath: string): void {
64+expect(command).toContain(process.execPath);
65+expect(command).toContain(wrapperPath);
66+}
67+5068afterEach(async () => {
5169restoreEnv("CODEX_HOME");
5270restoreEnv("OPENCLAW_AGENT_DIR");
@@ -62,6 +80,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
6280const agentDir = path.join(root, "agent");
6381const stateDir = path.join(root, "state");
6482const generated = generatedCodexPaths(stateDir);
83+const generatedClaude = generatedClaudePaths(stateDir);
6584const installedBinPath = path.join(
6685root,
6786"node_modules",
@@ -84,7 +103,9 @@ describe("prepareAcpxCodexAuthConfig", () => {
84103});
8510486105expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath);
106+expectClaudeWrapperCommand(resolved.agents.claude, generatedClaude.wrapperPath);
87107await expect(fs.access(generated.wrapperPath)).resolves.toBeUndefined();
108+await expect(fs.access(generatedClaude.wrapperPath)).resolves.toBeUndefined();
88109const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
89110expect(wrapper).toContain(JSON.stringify(installedBinPath));
90111expect(wrapper).toContain("defaultArgs = [installedBinPath]");
@@ -114,6 +135,28 @@ describe("prepareAcpxCodexAuthConfig", () => {
114135expect(wrapper).not.toContain("@zed-industries/codex-acp@^0.11.1");
115136});
116137138+it("falls back to the patched Claude ACP package when the local adapter is unavailable", async () => {
139+const root = await makeTempDir();
140+const stateDir = path.join(root, "state");
141+const generated = generatedClaudePaths(stateDir);
142+const pluginConfig = resolveAcpxPluginConfig({
143+rawConfig: {},
144+workspaceDir: root,
145+});
146+147+await prepareAcpxCodexAuthConfig({
148+ pluginConfig,
149+ stateDir,
150+resolveInstalledClaudeAcpBinPath: async () => undefined,
151+});
152+153+const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
154+expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.31.0"');
155+expect(wrapper).toContain('"--", "claude-agent-acp"');
156+expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@^0.31.0");
157+expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.1");
158+});
159+117160it("uses the bundled Codex ACP dependency by default when it is installed", async () => {
118161const root = await makeTempDir();
119162const stateDir = path.join(root, "state");
@@ -134,6 +177,26 @@ describe("prepareAcpxCodexAuthConfig", () => {
134177expect(wrapper).toContain("defaultArgs = [installedBinPath]");
135178});
136179180+it("uses the bundled Claude ACP dependency by default when it is installed", async () => {
181+const root = await makeTempDir();
182+const stateDir = path.join(root, "state");
183+const generated = generatedClaudePaths(stateDir);
184+const pluginConfig = resolveAcpxPluginConfig({
185+rawConfig: {},
186+workspaceDir: root,
187+});
188+189+await prepareAcpxCodexAuthConfig({
190+ pluginConfig,
191+ stateDir,
192+});
193+194+const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
195+expect(wrapper).toContain("@agentclientprotocol/claude-agent-acp");
196+expect(wrapper).toContain("dist/index.js");
197+expect(wrapper).toContain("defaultArgs = [installedBinPath]");
198+});
199+137200it("launches the locally installed Codex ACP bin with isolated CODEX_HOME", async () => {
138201const root = await makeTempDir();
139202const stateDir = path.join(root, "state");
@@ -164,6 +227,39 @@ describe("prepareAcpxCodexAuthConfig", () => {
164227expect(path.resolve(String(launched.codexHome))).toBe(expectedCodexHome);
165228});
166229230+it("launches the locally installed Claude ACP bin without going through npm", async () => {
231+const root = await makeTempDir();
232+const stateDir = path.join(root, "state");
233+const generated = generatedClaudePaths(stateDir);
234+const installedBinPath = path.join(root, "claude-agent-acp-bin.js");
235+await fs.writeFile(
236+installedBinPath,
237+"console.log(JSON.stringify({ argv: process.argv.slice(2), codexHome: process.env.CODEX_HOME ?? null }));\n",
238+"utf8",
239+);
240+const pluginConfig = resolveAcpxPluginConfig({
241+rawConfig: {},
242+workspaceDir: root,
243+});
244+245+await prepareAcpxCodexAuthConfig({
246+ pluginConfig,
247+ stateDir,
248+resolveInstalledClaudeAcpBinPath: async () => installedBinPath,
249+});
250+251+const { stdout } = await execFileAsync(
252+process.execPath,
253+[generated.wrapperPath, "--permission-mode", "bypass"],
254+{
255+cwd: root,
256+},
257+);
258+const launched = JSON.parse(stdout.trim()) as { argv?: unknown; codexHome?: unknown };
259+expect(launched.argv).toEqual(["--permission-mode", "bypass"]);
260+expect(launched.codexHome).toBeNull();
261+});
262+167263it("does not copy source Codex auth", async () => {
168264const root = await makeTempDir();
169265const sourceCodexHome = path.join(root, "source-codex");
@@ -208,7 +304,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
208304).rejects.toMatchObject({ code: "ENOENT" });
209305});
210306211-it("wraps an explicitly configured Codex agent command with isolated CODEX_HOME", async () => {
307+it("normalizes an explicitly configured Codex ACP command to the local wrapper", async () => {
212308const root = await makeTempDir();
213309const sourceCodexHome = path.join(root, "source-codex");
214310const stateDir = path.join(root, "state");
@@ -237,8 +333,9 @@ describe("prepareAcpxCodexAuthConfig", () => {
237333});
238334239335expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath);
240-expect(resolved.agents.codex).toContain("npx @zed-industries/codex-acp@0.12.0");
241-expect(resolved.agents.codex).toContain("-c 'model=\"gpt-5.4\"'");
336+expect(resolved.agents.codex).not.toContain("npx @zed-industries/codex-acp@0.12.0");
337+expect(resolved.agents.codex).toContain(quoteArg("-c"));
338+expect(resolved.agents.codex).toContain(quoteArg('model="gpt-5.4"'));
242339const isolatedConfig = await fs.readFile(generated.configPath, "utf8");
243340expect(isolatedConfig).not.toContain("notify");
244341expect(isolatedConfig).not.toContain("SkyComputerUseClient");
@@ -247,4 +344,79 @@ describe("prepareAcpxCodexAuthConfig", () => {
247344expect(wrapper).toContain("CODEX_HOME: codexHome");
248345expect(wrapper).not.toContain(sourceCodexHome);
249346});
347+348+it("normalizes an explicitly configured Claude ACP npx command to the local wrapper", async () => {
349+const root = await makeTempDir();
350+const stateDir = path.join(root, "state");
351+const generated = generatedClaudePaths(stateDir);
352+const pluginConfig = resolveAcpxPluginConfig({
353+rawConfig: {
354+agents: {
355+claude: {
356+command: "npx -y @agentclientprotocol/claude-agent-acp@0.31.0 --permission-mode bypass",
357+},
358+},
359+},
360+workspaceDir: root,
361+});
362+363+const resolved = await prepareAcpxCodexAuthConfig({
364+ pluginConfig,
365+ stateDir,
366+resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"),
367+});
368+369+expectClaudeWrapperCommand(resolved.agents.claude, generated.wrapperPath);
370+expect(resolved.agents.claude).not.toContain("npx -y @agentclientprotocol/claude-agent-acp");
371+expect(resolved.agents.claude).toContain("--permission-mode");
372+expect(resolved.agents.claude).toContain("bypass");
373+});
374+375+it("leaves a custom Claude agent command alone", async () => {
376+const root = await makeTempDir();
377+const stateDir = path.join(root, "state");
378+const pluginConfig = resolveAcpxPluginConfig({
379+rawConfig: {
380+agents: {
381+claude: {
382+command: "node ./custom-claude-wrapper.mjs --flag",
383+},
384+},
385+},
386+workspaceDir: root,
387+});
388+389+const resolved = await prepareAcpxCodexAuthConfig({
390+ pluginConfig,
391+ stateDir,
392+resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"),
393+});
394+395+expect(resolved.agents.claude).toBe("node ./custom-claude-wrapper.mjs --flag");
396+});
397+398+it("does not normalize custom Claude commands that only mention the package name", async () => {
399+const root = await makeTempDir();
400+const stateDir = path.join(root, "state");
401+const command =
402+"node ./custom-claude-wrapper.mjs @agentclientprotocol/claude-agent-acp@0.31.0 --flag";
403+const pluginConfig = resolveAcpxPluginConfig({
404+rawConfig: {
405+agents: {
406+claude: {
407+ command,
408+},
409+},
410+},
411+workspaceDir: root,
412+});
413+414+const resolved = await prepareAcpxCodexAuthConfig({
415+ pluginConfig,
416+ stateDir,
417+resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"),
418+});
419+420+expect(resolved.agents.claude).toBe(command);
421+});
250422});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。