|
1 | 1 | import type { ChildProcess } from "node:child_process"; |
2 | 2 | import { EventEmitter } from "node:events"; |
3 | 3 | import { beforeEach, describe, expect, it, vi } from "vitest"; |
4 | | -import { testing } from "./stage-sandbox-media.js"; |
| 4 | +import { SCP_STDERR_TAIL_CHARS, testing } from "./stage-sandbox-media.js"; |
| 5 | + |
| 6 | +const hasUnpairedUtf16Surrogate = (text: string): boolean => |
| 7 | + /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(text); |
5 | 8 |
|
6 | 9 | const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() })); |
7 | 10 |
|
@@ -48,6 +51,31 @@ describe("scpFile", () => { |
48 | 51 | await expect(resultPromise).rejects.toThrow("scp failed (1): stderr EPIPE"); |
49 | 52 | }); |
50 | 53 |
|
| 54 | + it("surfaces UTF-16 safe scp stderr when transfer fails with emoji at tail boundary", async () => { |
| 55 | + const { child, stderr } = createChild(); |
| 56 | + // Place the retained tail window on the emoji's low surrogate so raw slicing |
| 57 | + // would keep a lone surrogate half before the thrown error is built. |
| 58 | + const lowSurrogateTailStart = 100; |
| 59 | + const padding = "n".repeat(lowSurrogateTailStart - 1); |
| 60 | + const recent = "🤖" + "n".repeat(SCP_STDERR_TAIL_CHARS - 5) + "fail"; |
| 61 | + |
| 62 | + const resultPromise = testing.scpFile("host", "/remote/path", "/local/path"); |
| 63 | + stderr.emit("data", padding); |
| 64 | + stderr.emit("data", recent); |
| 65 | + child.emit("close", 1); |
| 66 | + |
| 67 | + let message = ""; |
| 68 | + try { |
| 69 | + await resultPromise; |
| 70 | + } catch (error) { |
| 71 | + message = error instanceof Error ? error.message : String(error); |
| 72 | + } |
| 73 | + expect(message).toMatch(/^scp failed \(1\):/); |
| 74 | + expect(message).toContain("fail"); |
| 75 | + expect(message).not.toContain("🤖"); |
| 76 | + expect(hasUnpairedUtf16Surrogate(message)).toBe(false); |
| 77 | + }); |
| 78 | + |
51 | 79 | it("does not terminate scp again when spawning fails", async () => { |
52 | 80 | const { child, kill } = createChild(); |
53 | 81 |
|
|
0 commit comments