mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
fix(pi-ssh): harden remote execution and search
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
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<void> { return Promise.resolve(); }
|
||||
dispose(): Promise<void> { 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<Buffer> { throw new Error("not used"); }
|
||||
ensureReadable(): Promise<void> { throw new Error("not used"); }
|
||||
ensureReadableWritable(): Promise<void> { throw new Error("not used"); }
|
||||
detectImageMimeType(): Promise<string | null> { throw new Error("not used"); }
|
||||
mkdir(): Promise<void> { throw new Error("not used"); }
|
||||
writeFile(): Promise<void> { 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");
|
||||
});
|
||||
Reference in New Issue
Block a user