import { afterEach, describe, expect, it, vi } from "vitest"; import * as ssh from "./ssh.js"; import { adapterExecutionTargetUsesManagedHome, resolveAdapterExecutionTargetCwd, runAdapterExecutionTargetShellCommand, } from "./execution-target.js"; describe("runAdapterExecutionTargetShellCommand", () => { afterEach(() => { vi.restoreAllMocks(); }); it("quotes remote shell commands with the shared SSH quoting helper", async () => { const runSshCommandSpy = vi.spyOn(ssh, "runSshCommand").mockResolvedValue({ stdout: "", stderr: "", }); await runAdapterExecutionTargetShellCommand( "run-1", { kind: "remote", transport: "ssh", remoteCwd: "/srv/paperclip/workspace", spec: { host: "ssh.example.test", port: 22, username: "ssh-user", remoteCwd: "/srv/paperclip/workspace", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: null, knownHosts: null, strictHostKeyChecking: true, }, }, `printf '%s\\n' "$HOME" && echo "it's ok"`, { cwd: "/tmp/local", env: {}, }, ); expect(runSshCommandSpy).toHaveBeenCalledWith( expect.objectContaining({ host: "ssh.example.test", username: "ssh-user", }), `sh -lc ${ssh.shellQuote(`printf '%s\\n' "$HOME" && echo "it's ok"`)}`, expect.any(Object), ); }); it("returns a timedOut result when the SSH shell command times out", async () => { vi.spyOn(ssh, "runSshCommand").mockRejectedValue(Object.assign(new Error("timed out"), { code: "ETIMEDOUT", stdout: "partial stdout", stderr: "partial stderr", signal: "SIGTERM", })); const onLog = vi.fn(async () => {}); const result = await runAdapterExecutionTargetShellCommand( "run-2", { kind: "remote", transport: "ssh", remoteCwd: "/srv/paperclip/workspace", spec: { host: "ssh.example.test", port: 22, username: "ssh-user", remoteCwd: "/srv/paperclip/workspace", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: null, knownHosts: null, strictHostKeyChecking: true, }, }, "sleep 10", { cwd: "/tmp/local", env: {}, onLog, }, ); expect(result).toMatchObject({ exitCode: null, signal: "SIGTERM", timedOut: true, stdout: "partial stdout", stderr: "partial stderr", }); expect(onLog).toHaveBeenCalledWith("stdout", "partial stdout"); expect(onLog).toHaveBeenCalledWith("stderr", "partial stderr"); }); it("returns the SSH process exit code for non-zero remote command failures", async () => { vi.spyOn(ssh, "runSshCommand").mockRejectedValue(Object.assign(new Error("non-zero exit"), { code: 17, stdout: "partial stdout", stderr: "partial stderr", signal: null, })); const onLog = vi.fn(async () => {}); const result = await runAdapterExecutionTargetShellCommand( "run-3", { kind: "remote", transport: "ssh", remoteCwd: "/srv/paperclip/workspace", spec: { host: "ssh.example.test", port: 22, username: "ssh-user", remoteCwd: "/srv/paperclip/workspace", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: null, knownHosts: null, strictHostKeyChecking: true, }, }, "false", { cwd: "/tmp/local", env: {}, onLog, }, ); expect(result).toMatchObject({ exitCode: 17, signal: null, timedOut: false, stdout: "partial stdout", stderr: "partial stderr", }); expect(onLog).toHaveBeenCalledWith("stdout", "partial stdout"); expect(onLog).toHaveBeenCalledWith("stderr", "partial stderr"); }); it("keeps managed homes disabled for both local and SSH targets", () => { expect(adapterExecutionTargetUsesManagedHome(null)).toBe(false); expect(adapterExecutionTargetUsesManagedHome({ kind: "remote", transport: "ssh", remoteCwd: "/srv/paperclip/workspace", spec: { host: "ssh.example.test", port: 22, username: "ssh-user", remoteCwd: "/srv/paperclip/workspace", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: null, knownHosts: null, strictHostKeyChecking: true, }, })).toBe(false); }); }); describe("resolveAdapterExecutionTargetCwd", () => { const sshTarget = { kind: "remote" as const, transport: "ssh" as const, remoteCwd: "/srv/paperclip/workspace", spec: { host: "ssh.example.test", port: 22, username: "ssh-user", remoteCwd: "/srv/paperclip/workspace", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: null, knownHosts: null, strictHostKeyChecking: true, }, }; it("falls back to the remote cwd when no adapter cwd is configured", () => { expect(resolveAdapterExecutionTargetCwd(sshTarget, "", "/Users/host/repo/server")).toBe( "/srv/paperclip/workspace", ); expect(resolveAdapterExecutionTargetCwd(sshTarget, " ", "/Users/host/repo/server")).toBe( "/srv/paperclip/workspace", ); expect(resolveAdapterExecutionTargetCwd(sshTarget, null, "/Users/host/repo/server")).toBe( "/srv/paperclip/workspace", ); }); it("preserves an explicit adapter cwd when one is configured", () => { expect( resolveAdapterExecutionTargetCwd( sshTarget, "/srv/paperclip/custom-agent-dir", "/Users/host/repo/server", ), ).toBe("/srv/paperclip/custom-agent-dir"); }); it("keeps the local fallback cwd for local targets", () => { expect(resolveAdapterExecutionTargetCwd(null, "", "/Users/host/repo/server")).toBe( "/Users/host/repo/server", ); }); });