[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
478
electron/bridges/sshAuthHelper.systemAgent.test.cjs
Normal file
478
electron/bridges/sshAuthHelper.systemAgent.test.cjs
Normal file
@@ -0,0 +1,478 @@
|
||||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const net = require("node:net");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { Duplex } = require("node:stream");
|
||||
|
||||
const {
|
||||
buildAuthHandler,
|
||||
getAvailableAgentSocket,
|
||||
getAvailableForwardingAgentSocket,
|
||||
getNativeOpenSshAgentSocket,
|
||||
cygwinAgentConnectable,
|
||||
isWindowsNamedPipe,
|
||||
socketAgentConnectable,
|
||||
socketAgentIdentityCount,
|
||||
ssh2AgentConnectable,
|
||||
resolveIdentityAgentPath,
|
||||
} = require("./sshAuthHelper.cjs");
|
||||
|
||||
test("macOS forwarding prefers a Bitwarden agent with identities over an empty inherited agent", async () => {
|
||||
const inheritedSocket = "/private/tmp/com.apple.launchd.test/Listeners";
|
||||
const bitwardenSocket = "/Users/alice/.bitwarden-ssh-agent.sock";
|
||||
const checked = [];
|
||||
|
||||
const result = await getAvailableForwardingAgentSocket(undefined, {
|
||||
platform: "darwin",
|
||||
localEnv: { SSH_AUTH_SOCK: inheritedSocket },
|
||||
homedir: "/Users/alice",
|
||||
getAvailableAgentSocketImpl: async (candidate) => {
|
||||
checked.push(candidate);
|
||||
return candidate;
|
||||
},
|
||||
socketAgentIdentityCount: async (candidate) => (
|
||||
candidate === bitwardenSocket ? 1 : 0
|
||||
),
|
||||
});
|
||||
|
||||
assert.equal(result, bitwardenSocket);
|
||||
assert.deepEqual(checked, [inheritedSocket, bitwardenSocket]);
|
||||
});
|
||||
|
||||
test("forwarding keeps an explicitly configured agent authoritative", async () => {
|
||||
let identityChecks = 0;
|
||||
const result = await getAvailableForwardingAgentSocket("/tmp/selected-agent.sock", {
|
||||
platform: "darwin",
|
||||
getAvailableAgentSocketImpl: async (candidate) => candidate,
|
||||
socketAgentIdentityCount: async () => {
|
||||
identityChecks += 1;
|
||||
return 0;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, "/tmp/selected-agent.sock");
|
||||
assert.equal(identityChecks, 0);
|
||||
});
|
||||
|
||||
test("IdentityAgent none disables login but still allows forwarding discovery", async () => {
|
||||
const inheritedSocket = "/private/tmp/com.apple.launchd.test/Listeners";
|
||||
const bitwardenSocket = "/Users/alice/.bitwarden-ssh-agent.sock";
|
||||
const result = await getAvailableForwardingAgentSocket("none", {
|
||||
platform: "darwin",
|
||||
localEnv: { SSH_AUTH_SOCK: inheritedSocket },
|
||||
homedir: "/Users/alice",
|
||||
getAvailableAgentSocketImpl: async (candidate) => candidate,
|
||||
socketAgentIdentityCount: async (candidate) => candidate === bitwardenSocket ? 1 : 0,
|
||||
});
|
||||
|
||||
assert.equal(result, bitwardenSocket);
|
||||
});
|
||||
|
||||
test("macOS forwarding preserves the inherited agent when discovered providers are empty", async () => {
|
||||
const inheritedSocket = "/private/tmp/com.apple.launchd.test/Listeners";
|
||||
const result = await getAvailableForwardingAgentSocket(undefined, {
|
||||
platform: "darwin",
|
||||
localEnv: { SSH_AUTH_SOCK: inheritedSocket },
|
||||
homedir: "/Users/alice",
|
||||
getAvailableAgentSocketImpl: async (candidate) => candidate,
|
||||
socketAgentIdentityCount: async () => 0,
|
||||
});
|
||||
|
||||
assert.equal(result, inheritedSocket);
|
||||
});
|
||||
|
||||
test("forwarding ignores a remote terminal env when reading the local agent", async () => {
|
||||
const inheritedSocket = "/private/tmp/com.apple.launchd.test/Listeners";
|
||||
const checked = [];
|
||||
await getAvailableForwardingAgentSocket(undefined, {
|
||||
platform: "darwin",
|
||||
env: { TERM: "xterm-256color" },
|
||||
localEnv: { SSH_AUTH_SOCK: inheritedSocket },
|
||||
homedir: "/Users/alice",
|
||||
getAvailableAgentSocketImpl: async (candidate) => {
|
||||
checked.push(candidate);
|
||||
return null;
|
||||
},
|
||||
socketAgentIdentityCount: async () => 0,
|
||||
});
|
||||
|
||||
assert.equal(checked[0], inheritedSocket);
|
||||
});
|
||||
|
||||
test("forwarding does not report an empty auto-discovered provider as available", async () => {
|
||||
const result = await getAvailableForwardingAgentSocket(undefined, {
|
||||
platform: "darwin",
|
||||
localEnv: {},
|
||||
homedir: "/Users/alice",
|
||||
getAvailableAgentSocketImpl: async (candidate) => candidate,
|
||||
socketAgentIdentityCount: async () => 0,
|
||||
});
|
||||
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test("non-macOS forwarding keeps the existing agent resolution path", async () => {
|
||||
const calls = [];
|
||||
const result = await getAvailableForwardingAgentSocket(undefined, {
|
||||
platform: "linux",
|
||||
getAvailableAgentSocketImpl: async (candidate) => {
|
||||
calls.push(candidate);
|
||||
return "/tmp/agent.sock";
|
||||
},
|
||||
socketAgentIdentityCount: async () => {
|
||||
throw new Error("must not inspect identities outside macOS discovery");
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, "/tmp/agent.sock");
|
||||
assert.deepEqual(calls, [undefined]);
|
||||
});
|
||||
|
||||
test("explicit agent opt-out suppresses automatic socket fallback", () => {
|
||||
const auth = buildAuthHandler({
|
||||
username: "deploy",
|
||||
allowAgentFallback: false,
|
||||
sshAgentSocketOverride: "/tmp/agent.sock",
|
||||
});
|
||||
|
||||
assert.equal(auth.agent, null);
|
||||
});
|
||||
|
||||
test("IdentityAgent paths expand standard OpenSSH connection tokens", () => {
|
||||
const resolved = resolveIdentityAgentPath(
|
||||
"%d/.ssh/agent-%h-%p-%r-%u-%l-%L-%i-%%-%C.sock",
|
||||
{
|
||||
hostname: "server.example.com",
|
||||
port: 2222,
|
||||
username: "deploy",
|
||||
localHostname: "mac.example.net",
|
||||
localUsername: "alice",
|
||||
uid: 501,
|
||||
},
|
||||
);
|
||||
|
||||
assert.match(
|
||||
resolved,
|
||||
new RegExp(`^${os.homedir().replace(/[\\^$.*+?()[\]{}|]/g, "\\$&")}/\\.ssh/agent-server\\.example\\.com-2222-deploy-alice-mac\\.example\\.net-mac-501-%-[a-f0-9]{40}\\.sock$`),
|
||||
);
|
||||
});
|
||||
|
||||
test("Windows named pipe detection accepts both slash styles", () => {
|
||||
assert.equal(isWindowsNamedPipe("\\\\.\\pipe\\openssh-ssh-agent"), true);
|
||||
assert.equal(isWindowsNamedPipe("//./pipe/openssh-ssh-agent"), true);
|
||||
assert.equal(isWindowsNamedPipe("pageant"), false);
|
||||
assert.equal(isWindowsNamedPipe("C:\\cygwin\\agent.socket"), false);
|
||||
});
|
||||
|
||||
test("Windows Pageant and Cygwin overrides use ssh2 agent validation", async () => {
|
||||
const checked = [];
|
||||
const injected = {
|
||||
platform: "win32",
|
||||
windowsPipeConnectable: async () => {
|
||||
throw new Error("named-pipe validation should not run");
|
||||
},
|
||||
ssh2AgentConnectable: async (agentPath) => {
|
||||
checked.push(agentPath);
|
||||
return true;
|
||||
},
|
||||
cygwinAgentConnectable: async (agentPath) => {
|
||||
checked.push(agentPath);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(await getAvailableAgentSocket("pageant", injected), "pageant");
|
||||
assert.equal(
|
||||
await getAvailableAgentSocket("C:\\cygwin\\agent.socket", injected),
|
||||
"C:\\cygwin\\agent.socket",
|
||||
);
|
||||
assert.deepEqual(checked, ["pageant", "C:\\cygwin\\agent.socket"]);
|
||||
});
|
||||
|
||||
test("Windows named pipe overrides retain the lightweight pipe probe", async () => {
|
||||
const pipePath = "\\\\.\\pipe\\custom-agent";
|
||||
let checkedPath = null;
|
||||
const result = await getAvailableAgentSocket(pipePath, {
|
||||
platform: "win32",
|
||||
windowsPipeConnectable: async (value) => {
|
||||
checkedPath = value;
|
||||
return true;
|
||||
},
|
||||
ssh2AgentConnectable: async () => {
|
||||
throw new Error("ssh2 validation should not run for a named pipe");
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, pipePath);
|
||||
assert.equal(checkedPath, pipePath);
|
||||
});
|
||||
|
||||
test("ssh2 agent validation times out when an agent does not respond", async () => {
|
||||
const start = Date.now();
|
||||
const stream = new Duplex({
|
||||
read() {},
|
||||
write(_chunk, _encoding, callback) { callback(); },
|
||||
});
|
||||
const available = await ssh2AgentConnectable("pageant", {
|
||||
timeoutMs: 20,
|
||||
createAgentImpl: () => ({ getStream(callback) { callback(null, stream); } }),
|
||||
});
|
||||
|
||||
assert.equal(available, false);
|
||||
assert.equal(stream.destroyed, true);
|
||||
assert.ok(Date.now() - start < 500);
|
||||
});
|
||||
|
||||
test("Pageant validation destroys a stream delivered after timeout", async () => {
|
||||
let deliverStream;
|
||||
const stream = new Duplex({
|
||||
read() {},
|
||||
write(_chunk, _encoding, callback) { callback(); },
|
||||
});
|
||||
const available = await ssh2AgentConnectable("pageant", {
|
||||
timeoutMs: 20,
|
||||
createAgentImpl: () => ({
|
||||
getStream(callback) { deliverStream = callback; },
|
||||
}),
|
||||
});
|
||||
assert.equal(available, false);
|
||||
deliverStream(null, stream);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(stream.destroyed, true);
|
||||
});
|
||||
|
||||
test("Cygwin agent validation closes a stalled negotiation socket", async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-cygwin-agent-"));
|
||||
const descriptorPath = path.join(dir, "agent.socket");
|
||||
const server = net.createServer();
|
||||
let acceptedSocket = null;
|
||||
let acceptedSocketClosed;
|
||||
const closed = new Promise((resolve) => { acceptedSocketClosed = resolve; });
|
||||
server.on("connection", (socket) => {
|
||||
acceptedSocket = socket;
|
||||
socket.resume();
|
||||
socket.once("close", acceptedSocketClosed);
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
fs.writeFileSync(
|
||||
descriptorPath,
|
||||
`!<socket >${address.port} s 00000000-00000000-00000000-00000000`,
|
||||
);
|
||||
t.after(() => {
|
||||
server.close();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
assert.equal(await cygwinAgentConnectable(descriptorPath, { timeoutMs: 20 }), false);
|
||||
await Promise.race([
|
||||
closed,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("Cygwin probe connection stayed open")), 2_000)),
|
||||
]);
|
||||
assert.equal(acceptedSocket?.destroyed, true);
|
||||
});
|
||||
|
||||
test("Cygwin validation does not connect when descriptor reading finishes after timeout", async () => {
|
||||
let finishRead;
|
||||
let connectionAttempts = 0;
|
||||
const availablePromise = cygwinAgentConnectable("C:\\cygwin\\agent.socket", {
|
||||
timeoutMs: 20,
|
||||
readFileImpl: () => new Promise((resolve) => { finishRead = resolve; }),
|
||||
createConnectionImpl: () => {
|
||||
connectionAttempts += 1;
|
||||
throw new Error("must not connect after timeout");
|
||||
},
|
||||
});
|
||||
assert.equal(await availablePromise, false);
|
||||
finishRead("!<socket >1234 s 00000000-00000000-00000000-00000000");
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(connectionAttempts, 0);
|
||||
});
|
||||
|
||||
test("Cygwin validation converts POSIX-style descriptor paths", async () => {
|
||||
const readPaths = [];
|
||||
let convertedPath = null;
|
||||
let connectionAttempts = 0;
|
||||
const available = await cygwinAgentConnectable("/tmp/agent.socket", {
|
||||
timeoutMs: 20,
|
||||
readFileImpl: async (value) => {
|
||||
readPaths.push(value);
|
||||
if (value === "/tmp/agent.socket") throw new Error("ENOENT");
|
||||
return "!<socket >1234 s 00000000-00000000-00000000-00000000";
|
||||
},
|
||||
resolveCygwinPathImpl: async (value) => {
|
||||
assert.equal(value, "/tmp/agent.socket");
|
||||
convertedPath = "C:\\cygwin64\\tmp\\agent.socket";
|
||||
return convertedPath;
|
||||
},
|
||||
createConnectionImpl: () => {
|
||||
connectionAttempts += 1;
|
||||
return new Duplex({
|
||||
read() {},
|
||||
write(_chunk, _encoding, callback) { callback(); },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(available, false);
|
||||
assert.deepEqual(readPaths, ["/tmp/agent.socket", convertedPath]);
|
||||
assert.equal(connectionAttempts, 1);
|
||||
});
|
||||
|
||||
test("Cygwin agent validation completes the two-stage handshake", async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-cygwin-agent-ok-"));
|
||||
const descriptorPath = path.join(dir, "agent.socket");
|
||||
const server = net.createServer();
|
||||
let connectionIndex = 0;
|
||||
let retryCredentials = null;
|
||||
server.on("connection", (socket) => {
|
||||
connectionIndex += 1;
|
||||
const currentConnection = connectionIndex;
|
||||
let state = "secret";
|
||||
let buffered = Buffer.alloc(0);
|
||||
socket.on("data", (chunk) => {
|
||||
buffered = Buffer.concat([buffered, chunk]);
|
||||
while (true) {
|
||||
const needed = state === "secret" ? 16 : state === "credentials" ? 12 : 5;
|
||||
if (buffered.length < needed) return;
|
||||
const value = buffered.subarray(0, needed);
|
||||
buffered = buffered.subarray(needed);
|
||||
if (state === "secret") {
|
||||
socket.write(value);
|
||||
state = "credentials";
|
||||
} else if (state === "credentials") {
|
||||
if (currentConnection === 2) retryCredentials = Buffer.from(value);
|
||||
socket.write(currentConnection === 1 ? Buffer.alloc(12, 7) : Buffer.alloc(12, 9));
|
||||
state = currentConnection === 1 ? "done" : "agent";
|
||||
} else if (state === "agent") {
|
||||
socket.write(Buffer.from([0, 0, 0, 5, 12, 0, 0, 0, 0]));
|
||||
state = "done";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
fs.writeFileSync(
|
||||
descriptorPath,
|
||||
`!<socket >${address.port} s 00000000-00000000-00000000-00000000`,
|
||||
);
|
||||
t.after(() => {
|
||||
server.close();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
assert.equal(await cygwinAgentConnectable(descriptorPath, {
|
||||
timeoutMs: 500,
|
||||
processId: 4242,
|
||||
}), true);
|
||||
assert.equal(connectionIndex, 2);
|
||||
assert.equal(retryCredentials.readUInt32LE(0), 4242);
|
||||
assert.deepEqual(retryCredentials.subarray(4), Buffer.alloc(8, 7));
|
||||
});
|
||||
|
||||
test("Unix agent availability requires a working agent protocol response", async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-agent-socket-"));
|
||||
const socketPath = path.join(dir, "agent.sock");
|
||||
const server = net.createServer();
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(socketPath, resolve);
|
||||
});
|
||||
t.after(() => {
|
||||
server.close();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
assert.equal(await getAvailableAgentSocket(socketPath, {
|
||||
platform: "linux",
|
||||
socketAgentConnectable: async () => false,
|
||||
}), null);
|
||||
assert.equal(await getAvailableAgentSocket(socketPath, {
|
||||
platform: "linux",
|
||||
socketAgentConnectable: async () => true,
|
||||
}), socketPath);
|
||||
});
|
||||
|
||||
test("Unix agent protocol probe closes a connection that never responds", async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-agent-hung-"));
|
||||
const socketPath = path.join(dir, "agent.sock");
|
||||
const server = net.createServer();
|
||||
let acceptedSocket = null;
|
||||
let probeSocket = null;
|
||||
let acceptedSocketClosed;
|
||||
const closed = new Promise((resolve) => { acceptedSocketClosed = resolve; });
|
||||
server.on("connection", (socket) => {
|
||||
acceptedSocket = socket;
|
||||
socket.resume();
|
||||
socket.once("close", acceptedSocketClosed);
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(socketPath, resolve);
|
||||
});
|
||||
t.after(() => {
|
||||
server.close();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
assert.equal(await socketAgentConnectable(socketPath, {
|
||||
timeoutMs: 20,
|
||||
createConnectionImpl: (value) => {
|
||||
probeSocket = net.createConnection(value);
|
||||
return probeSocket;
|
||||
},
|
||||
}), false);
|
||||
assert.equal(probeSocket?.destroyed, true);
|
||||
await Promise.race([
|
||||
closed,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("agent probe connection stayed open")), 500)),
|
||||
]);
|
||||
assert.equal(acceptedSocket?.destroyed, true);
|
||||
});
|
||||
|
||||
test("Unix agent identity probe reads the advertised identity count", async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-agent-identities-"));
|
||||
const socketPath = path.join(dir, "agent.sock");
|
||||
const server = net.createServer((socket) => {
|
||||
socket.once("data", () => {
|
||||
socket.write(Buffer.from([0, 0, 0, 5, 12, 0, 0, 0, 2]));
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(socketPath, resolve);
|
||||
});
|
||||
t.after(() => {
|
||||
server.close();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
assert.equal(await socketAgentIdentityCount(socketPath), 2);
|
||||
});
|
||||
|
||||
test("native OpenSSH modes reject Pageant and Cygwin-only adapters clearly", async () => {
|
||||
await assert.rejects(
|
||||
getNativeOpenSshAgentSocket("pageant", {
|
||||
platform: "win32",
|
||||
ssh2AgentConnectable: async () => true,
|
||||
}),
|
||||
(error) => {
|
||||
assert.equal(error.code, "ERR_SSH_AGENT_NATIVE_UNSUPPORTED");
|
||||
assert.match(error.message, /named-pipe agent/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user