import assert from "node:assert/strict"; import test from "node:test"; import { createRemoteBashOps } from "../src/remote-bash.ts"; import { changeRemoteCwd, mapLocalPathToRemote, resolveRemoteCwd, SSH_CD_EXECUTION_MODE, } from "../src/remote-cwd.ts"; import { resolveRemoteSearchPath } from "../src/remote-search.ts"; import type { RemoteExecOptions, RemoteTransport } from "../src/ssh2-transport.ts"; class CapturingTransport implements RemoteTransport { calls: Array<{ command: string; cwd: string }> = []; connect(): Promise { return Promise.resolve(); } dispose(): Promise { return Promise.resolve(); } exec(command: string, cwd: string, options: RemoteExecOptions): Promise<{ exitCode: number | null }> { this.calls.push({ command, cwd }); options.onData(Buffer.from("ok\n")); return Promise.resolve({ exitCode: 0 }); } capture(): Promise<{ exitCode: number | null; output: Buffer }> { throw new Error("not used"); } readFile(): Promise { throw new Error("not used"); } ensureReadable(): Promise { throw new Error("not used"); } ensureReadableWritable(): Promise { throw new Error("not used"); } detectImageMimeType(): Promise { throw new Error("not used"); } mkdir(): Promise { throw new Error("not used"); } writeFile(): Promise { throw new Error("not used"); } } test("resolves explicit remote workspace changes", () => { assert.equal(resolveRemoteCwd("services/api", "/srv/project", "/home/build"), "/srv/project/services/api"); assert.equal(resolveRemoteCwd("../shared", "/srv/project", "/home/build"), "/srv/shared"); assert.equal(resolveRemoteCwd("/opt/app/../release", "/srv/project", "/home/build"), "/opt/release"); assert.equal(resolveRemoteCwd("~", "/srv/project", "/home/build"), "/home/build"); assert.equal(resolveRemoteCwd("~/jobs/app", "/srv/project", "/home/build"), "/home/build/jobs/app"); }); test("rejects invalid remote workspace paths", () => { assert.throws(() => resolveRemoteCwd("", "/srv/project", "/home/build"), /non-empty/); assert.throws(() => resolveRemoteCwd("bad\npath", "/srv/project", "/home/build"), /NUL or newline/); assert.throws(() => resolveRemoteCwd("~other/project", "/srv/project", "/home/build"), /only '~' or '~\/'/); }); test("ssh_cd is a sequential workspace transition", () => { assert.equal(SSH_CD_EXECUTION_MODE, "sequential"); }); test("a validated workspace change drives subsequent remote tools", async () => { const connection = { remoteCwd: "/srv/project", remoteHome: "/home/build", localCwd: "/Users/local/project", localHome: "/Users/local", }; const verified: string[] = []; const changed = await changeRemoteCwd(connection, "../release", async (requestedCwd) => { verified.push(requestedCwd); return requestedCwd; }); assert.deepEqual(verified, ["/srv/release"]); assert.deepEqual(changed, { previousCwd: "/srv/project", remoteCwd: "/srv/release" }); assert.equal(connection.remoteCwd, "/srv/release"); assert.equal(mapLocalPathToRemote("/Users/local/project/logs/build.log", connection), "/srv/release/logs/build.log"); assert.equal(resolveRemoteSearchPath(undefined, connection.remoteCwd, connection.remoteHome), "/srv/release"); const transport = new CapturingTransport(); const operations = createRemoteBashOps(connection, transport); await operations.exec("npm test", "/Users/local/project", { onData() {} }); assert.deepEqual(transport.calls, [{ command: "npm test", cwd: "/srv/release" }]); }); test("a failed workspace validation leaves the previous cwd active", async () => { const connection = { remoteCwd: "/srv/project", remoteHome: "/home/build" }; await assert.rejects( changeRemoteCwd(connection, "missing", async () => { throw new Error("not a directory"); }), /not a directory/, ); assert.equal(connection.remoteCwd, "/srv/project"); }); test("successive workspace changes resolve from the latest confirmed cwd", async () => { const connection = { remoteCwd: "/srv/project", remoteHome: "/home/build" }; const verify = async (requestedCwd: string) => requestedCwd; await changeRemoteCwd(connection, "services/api", verify); await changeRemoteCwd(connection, "../worker", verify); assert.equal(connection.remoteCwd, "/srv/project/services/worker"); });