import { describe, expect, test } from "bun:test"; import { createSshStdoutForwarder, formatSshStdoutTruncationHint, parseSshInvocation, sshStdoutStreamMaxBytes, sshStdoutTruncationHint, windowsPowerShellScriptPrelude, } from "./ssh"; describe("ssh windows PowerShell safety prelude", () => { test("shadows ConvertTo-Json and strips filesystem ETS metadata", () => { const prelude = windowsPowerShellScriptPrelude("C:\\test"); expect(prelude).toContain("function ConvertTo-UniDeskPlainJsonValue"); expect(prelude).toContain("function ConvertTo-Json"); expect(prelude).toContain("'PSPath','PSParentPath','PSChildName','PSDrive','PSProvider','ReadCount'"); expect(prelude).toContain("Microsoft.PowerShell.Utility\\ConvertTo-Json"); expect(prelude).toContain("Set-Location -LiteralPath 'C:\\test'"); }); }); describe("ssh stdout bounded streaming", () => { test("uses bounded defaults and clamps env override", () => { expect(sshStdoutStreamMaxBytes({} as NodeJS.ProcessEnv)).toBe(256 * 1024); expect(sshStdoutStreamMaxBytes({ UNIDESK_SSH_STDOUT_STREAM_MAX_BYTES: "8192" } as NodeJS.ProcessEnv)).toBe(8192); expect(sshStdoutStreamMaxBytes({ UNIDESK_SSH_STDOUT_STREAM_MAX_BYTES: "1" } as NodeJS.ProcessEnv)).toBe(4096); expect(sshStdoutStreamMaxBytes({ UNIDESK_SSH_STDOUT_STREAM_MAX_BYTES: String(64 * 1024 * 1024) } as NodeJS.ProcessEnv)).toBe(16 * 1024 * 1024); }); test("formats truncation hint without echoing remote command", () => { const invocation = parseSshInvocation("D601:win", ["ps"]); expect(invocation.parsed.remoteCommand).not.toBeNull(); const hint = sshStdoutTruncationHint({ invocation, transport: "backend-core-broker", thresholdBytes: 4096, observedBytesAtTruncation: 5000, dumpPath: "/tmp/unidesk-cli-output/sample.stdout.bin", }); const formatted = formatSshStdoutTruncationHint(hint); expect(formatted.startsWith("UNIDESK_SSH_STDOUT_TRUNCATED ")).toBe(true); const payload = JSON.parse(formatted.slice("UNIDESK_SSH_STDOUT_TRUNCATED ".length)) as Record; expect(payload.code).toBe("ssh-stdout-truncated"); expect(payload.providerId).toBe("D601"); expect(payload.route).toBe("D601:win"); expect(payload.thresholdBytes).toBe(4096); expect(formatted).not.toContain("Get-Content"); }); test("forwarder bounds stdout and emits one truncation hint", () => { const invocation = parseSshInvocation("D601:win", ["ps"]); const forwarded: Buffer[] = []; const stdout = { write(chunk: string | Buffer): boolean { forwarded.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); return true; }, } as NodeJS.WritableStream; const forwarder = createSshStdoutForwarder({ invocation, transport: "frontend-websocket", maxBytes: 5, stdout, }); expect(forwarder.write(Buffer.from("abc"))).toBeNull(); const hint = forwarder.write(Buffer.from("defgh")); expect(hint).not.toBeNull(); expect(forwarder.write(Buffer.from("ijkl"))).toBeNull(); expect(Buffer.concat(forwarded).toString("utf8")).toBe("abcde"); expect(hint).toContain("UNIDESK_SSH_STDOUT_TRUNCATED"); expect(hint).toContain("\"transport\":\"frontend-websocket\""); }); }); describe("ssh removed shell aliases", () => { test("reports route-aware replacement examples for trans script", () => { const previousEntrypoint = process.env.UNIDESK_SSH_ENTRYPOINT; process.env.UNIDESK_SSH_ENTRYPOINT = "trans"; try { parseSshInvocation("D601:/tmp", ["script", "--", "pwd"]); throw new Error("expected script alias to fail"); } catch (error) { expect(error).toBeInstanceOf(Error); const payload = error as Error & { code?: string; entrypoint?: string; route?: string; operation?: string; replacementExamples?: { posixSh?: string; bash?: string; }; }; expect(payload.code).toBe("ssh-removed-shell-alias"); expect(payload.entrypoint).toBe("trans"); expect(payload.route).toBe("D601:/tmp"); expect(payload.operation).toBe("script"); expect(payload.replacementExamples?.posixSh).toBe("trans D601:/tmp sh -- 'pwd'"); expect(payload.replacementExamples?.bash).toBe("trans D601:/tmp bash -- 'pwd'"); } finally { if (previousEntrypoint === undefined) { delete process.env.UNIDESK_SSH_ENTRYPOINT; } else { process.env.UNIDESK_SSH_ENTRYPOINT = previousEntrypoint; } } }); });