[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:
225
electron/bridges/sftpBridge/scpSessionOps.test.cjs
Normal file
225
electron/bridges/sftpBridge/scpSessionOps.test.cjs
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Integration-style tests: SCP-mode clients registered in sftpClients are
|
||||
* reachable through the same list/mkdir/write entry points the UI and AI tools use.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const { describe, it, beforeEach, afterEach } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { EventEmitter } = require("node:events");
|
||||
const { createScpBackend, isScpModeClient } = require("./scpBackend.cjs");
|
||||
const { createFileOpsApi } = require("./fileOps.cjs");
|
||||
const { SCP_OK, buildFileControlLine } = require("./scpProtocol.cjs");
|
||||
|
||||
function createMockStream() {
|
||||
const ee = new EventEmitter();
|
||||
ee.writable = true;
|
||||
ee.readable = true;
|
||||
ee.stderr = new EventEmitter();
|
||||
ee.write = (buf, cb) => {
|
||||
ee.emit("_write", Buffer.from(buf));
|
||||
if (typeof cb === "function") cb();
|
||||
return true;
|
||||
};
|
||||
ee.end = (cb) => { if (typeof cb === "function") cb(); };
|
||||
ee.close = () => ee.emit("close");
|
||||
ee.destroy = () => ee.emit("close");
|
||||
ee.pushFromRemote = (buf) => ee.emit("data", Buffer.from(buf));
|
||||
return ee;
|
||||
}
|
||||
|
||||
describe("SCP-mode session ops via fileOps entry points (AI/UI shared path)", () => {
|
||||
let sftpClients;
|
||||
let api;
|
||||
let commands;
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-scp-session-"));
|
||||
sftpClients = new Map();
|
||||
commands = [];
|
||||
|
||||
const backend = createScpBackend({
|
||||
exec: async (command) => {
|
||||
commands.push(command);
|
||||
if (command.includes("for f in") || command.includes("cd ")) {
|
||||
const name = Buffer.from("agent.txt").toString("base64");
|
||||
return {
|
||||
stdout: `f|-rw-r--r--|4|1700000000|${name}\n`,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
if (command.includes("mkdir")) return { stdout: "", stderr: "", code: 0 };
|
||||
if (command.includes("rm ") || command.includes("rmdir")) return { stdout: "", stderr: "", code: 0 };
|
||||
if (command.includes("mv ")) return { stdout: "", stderr: "", code: 0 };
|
||||
if (command.includes("$HOME") || command.startsWith("printf")) {
|
||||
return { stdout: "/home/agent\n", stderr: "", code: 0 };
|
||||
}
|
||||
if (command.includes("if [ ! -e")) {
|
||||
return {
|
||||
stdout: "f|-rw-r--r--|4|1700000000|/home/agent/agent.txt\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
execStream: async (command) => {
|
||||
commands.push(command);
|
||||
const stream = createMockStream();
|
||||
if (command.includes("scp -t")) {
|
||||
setImmediate(() => stream.pushFromRemote(Buffer.from([SCP_OK])));
|
||||
stream.on("_write", (buf) => {
|
||||
const text = buf.toString("utf8");
|
||||
if (text.startsWith("C") || (buf.length === 1 && buf[0] === 0x00)) {
|
||||
setImmediate(() => stream.pushFromRemote(Buffer.from([SCP_OK])));
|
||||
}
|
||||
});
|
||||
} else if (command.includes("scp -f")) {
|
||||
let ackCount = 0;
|
||||
stream.on("_write", (buf) => {
|
||||
if (!(buf[0] === SCP_OK && buf.length === 1)) return;
|
||||
ackCount += 1;
|
||||
if (ackCount === 1) {
|
||||
setImmediate(() => {
|
||||
stream.pushFromRemote(buildFileControlLine({ mode: 0o644, size: 4, name: "agent.txt" }));
|
||||
});
|
||||
} else if (ackCount === 2) {
|
||||
setImmediate(() => {
|
||||
stream.pushFromRemote(Buffer.concat([Buffer.from("data"), Buffer.from([0x00])]));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return stream;
|
||||
},
|
||||
});
|
||||
|
||||
const client = {
|
||||
client: { exec: () => {} },
|
||||
sftp: null,
|
||||
__netcattyFileProtocol: "scp",
|
||||
__netcattyScpBackend: backend,
|
||||
async end() {},
|
||||
};
|
||||
sftpClients.set("scp-session-1", client);
|
||||
|
||||
// Minimal ctx for createFileOpsApi — only what list/mkdir/rename/delete/write need
|
||||
api = createFileOpsApi({
|
||||
get sftpClients() { return sftpClients; },
|
||||
get electronModule() {
|
||||
return {
|
||||
webContents: { fromId: () => ({ send: () => {} }) },
|
||||
};
|
||||
},
|
||||
fileWatcherBridge: { stopWatchersForSession: () => {} },
|
||||
fs,
|
||||
path,
|
||||
Buffer,
|
||||
console,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
jumpConnectionsMap: new Map(),
|
||||
sftpEncodingState: new Map(),
|
||||
normalizeEncoding: (e) => e || "utf-8",
|
||||
isAsciiString: () => true,
|
||||
requireSftpChannel: async () => { throw new Error("should not use SFTP channel in SCP mode"); },
|
||||
resolveEncodingForRequest: () => "utf-8",
|
||||
updateResolvedEncoding: () => "utf-8",
|
||||
encodePath: (p) => p,
|
||||
decodeName: (n) => n,
|
||||
detectEncodingFromList: () => null,
|
||||
statResultFromAttrs: (a) => a,
|
||||
normalizeRemotePathString: async (_c, p) => p,
|
||||
collectReadable: async () => Buffer.alloc(0),
|
||||
writeToWritable: async () => {},
|
||||
throwIfAborted: () => {},
|
||||
pipeStreams: async () => {},
|
||||
ensureRemoteDirForSession: async () => true,
|
||||
removeRemotePathInternal: async () => {},
|
||||
renameRemotePath: async () => {},
|
||||
realpathAsync: async () => "/",
|
||||
statAsync: async () => ({}),
|
||||
readdirAsync: async () => [],
|
||||
mkdirAsync: async () => {},
|
||||
rmdirAsync: async () => {},
|
||||
unlinkAsync: async () => {},
|
||||
openFileAsync: async () => ({}),
|
||||
writeFileChunkAsync: async () => {},
|
||||
closeFileAsync: async () => {},
|
||||
createAbortError: (s, m) => new Error(m),
|
||||
copySftpEncodingState: () => {},
|
||||
clearSftpEncodingState: () => {},
|
||||
safeSend: () => {},
|
||||
tempDirBridge: { getTempFilePath: (n) => path.join(tmpDir, n) },
|
||||
randomUUID: () => "test-uuid",
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
});
|
||||
|
||||
it("marks clients as SCP-mode", () => {
|
||||
assert.equal(isScpModeClient(sftpClients.get("scp-session-1")), true);
|
||||
});
|
||||
|
||||
it("list works for SCP-mode session id (AI sftp.list path)", async () => {
|
||||
const entries = await api.listSftp(null, { sftpId: "scp-session-1", path: "/home/agent" });
|
||||
assert.equal(entries.length, 1);
|
||||
assert.equal(entries[0].name, "agent.txt");
|
||||
assert.ok(commands.some((c) => c.includes("cd ") || c.includes("for f in")));
|
||||
});
|
||||
|
||||
it("mkdir write-class op works for SCP-mode session (AI sftp.mkdir path)", async () => {
|
||||
const ok = await api.mkdirSftp(null, { sftpId: "scp-session-1", path: "/home/agent/newdir" });
|
||||
assert.equal(ok, true);
|
||||
assert.ok(commands.some((c) => c.includes("mkdir") && c.includes("/home/agent/newdir")));
|
||||
});
|
||||
|
||||
it("rename and delete work for SCP-mode session", async () => {
|
||||
await api.renameSftp(null, {
|
||||
sftpId: "scp-session-1",
|
||||
oldPath: "/home/agent/a",
|
||||
newPath: "/home/agent/b",
|
||||
});
|
||||
await api.deleteSftp(null, { sftpId: "scp-session-1", path: "/home/agent/b" });
|
||||
assert.ok(commands.some((c) => c.includes("mv --")));
|
||||
assert.ok(commands.some((c) => c.includes("rm ")));
|
||||
});
|
||||
|
||||
it("write + homeDir work for SCP-mode session", async () => {
|
||||
await api.writeSftp(null, {
|
||||
sftpId: "scp-session-1",
|
||||
path: "/home/agent/out.txt",
|
||||
content: "hi\n",
|
||||
});
|
||||
assert.ok(commands.some((c) => c.includes("scp -t")));
|
||||
const home = await api.getSftpHomeDir(null, { sftpId: "scp-session-1" });
|
||||
assert.equal(home.success, true);
|
||||
assert.equal(home.homeDir, "/home/agent");
|
||||
});
|
||||
|
||||
it("capability catalog still exposes stable sftp.* CLI verbs", () => {
|
||||
const { SFTP_CAPABILITIES } = require("../../capabilities/catalog/sftp.cjs");
|
||||
const ids = SFTP_CAPABILITIES.map((c) => c.id);
|
||||
for (const id of [
|
||||
"sftp.list",
|
||||
"sftp.mkdir",
|
||||
"sftp.write",
|
||||
"sftp.upload",
|
||||
"sftp.download",
|
||||
"sftp.delete",
|
||||
"sftp.rename",
|
||||
]) {
|
||||
assert.ok(ids.includes(id), `missing capability ${id}`);
|
||||
}
|
||||
const listCap = SFTP_CAPABILITIES.find((c) => c.id === "sftp.list");
|
||||
assert.deepEqual(listCap.surfaces.cli.command, ["sftp", "list"]);
|
||||
assert.match(listCap.description, /SCP-mode/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user