feat(omp): list ssh:// directories via read

- `read ssh://host/dir` lists a remote directory one level deep; `ssh://host/` lists the remote root
- add statRemotePath + listRemoteDir; resolve reads first and classifies on error (directory -> one-level listing, dirs-first, dotfiles included)
- directory resources carry isDirectory + immutable and expose no sourcePath
- search refuses a virtual (no-sourcePath) directory resource instead of grepping the listing text
- writeRemoteFile refuses a directory destination and cleans up its temp on that path
This commit is contained in:
Tommaso Fontana
2026-06-26 20:41:35 +02:00
parent f9ece90853
commit e4ceebb258
10 changed files with 557 additions and 44 deletions
@@ -0,0 +1,29 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import type { SSHConnectionTarget } from "../connection-manager";
import * as connectionManager from "../connection-manager";
import { readRemoteFile, writeRemoteFile } from "../file-transfer";
describe("ssh file-transfer POSIX guard", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("rejects a confirmed Windows remote before running any POSIX command", async () => {
// Stub BOTH the connection and the host-info probe so the guard is reached
// without opening a real SSH connection and before any command is spawned.
const ensureConnectionSpy = vi.spyOn(connectionManager, "ensureConnection").mockResolvedValue(undefined);
const ensureHostInfoSpy = vi.spyOn(connectionManager, "ensureHostInfo").mockResolvedValue({
version: 2,
os: "windows",
shell: "powershell",
compatEnabled: false,
});
const target: SSHConnectionTarget = { name: "winbox", host: "winbox" };
await expect(readRemoteFile(target, "C:/x.txt", { maxBytes: 1024 })).rejects.toThrow(/Windows host/);
await expect(writeRemoteFile(target, "C:/x.txt", new Uint8Array([1]), {})).rejects.toThrow(/Windows host/);
// Prove the guard ran through the stubbed transport rather than failing early
// for an unrelated reason (e.g. a future import refactor bypassing the mocks).
expect(ensureConnectionSpy).toHaveBeenCalled();
expect(ensureHostInfoSpy).toHaveBeenCalled();
});
});