@@ -105,6 +105,19 @@ function mockArchiveResponse(buffer: Uint8Array): void {
|
105 | 105 | }); |
106 | 106 | } |
107 | 107 | |
| 108 | +function createCancelableBody() { |
| 109 | +let canceled = false; |
| 110 | +const stream = new ReadableStream<Uint8Array>({ |
| 111 | +start(controller) { |
| 112 | +controller.enqueue(new Uint8Array([1, 2, 3])); |
| 113 | +}, |
| 114 | +cancel() { |
| 115 | +canceled = true; |
| 116 | +}, |
| 117 | +}); |
| 118 | +return { stream, wasCanceled: () => canceled }; |
| 119 | +} |
| 120 | + |
108 | 121 | function runCommandResult(params?: Partial<Record<"code" | "stdout" | "stderr", string | number>>) { |
109 | 122 | return { |
110 | 123 | code: 0, |
@@ -211,6 +224,37 @@ describe("installDownloadSpec extraction safety", () => {
|
211 | 224 | ).toBe("payload"); |
212 | 225 | }); |
213 | 226 | |
| 227 | +it("cancels failed download response bodies before returning the error", async () => { |
| 228 | +const { stream, wasCanceled } = createCancelableBody(); |
| 229 | +const release = vi.fn(async () => undefined); |
| 230 | +fetchWithSsrFGuardMock.mockResolvedValue({ |
| 231 | +response: { |
| 232 | +ok: false, |
| 233 | +status: 500, |
| 234 | +statusText: "Server Error", |
| 235 | +body: stream, |
| 236 | +}, |
| 237 | + release, |
| 238 | +}); |
| 239 | + |
| 240 | +const result = await installDownloadSpec({ |
| 241 | +entry: buildEntry("failed-download-body"), |
| 242 | +spec: { |
| 243 | +kind: "download", |
| 244 | +id: "dl", |
| 245 | +url: "https://example.invalid/broken.bin", |
| 246 | +extract: false, |
| 247 | +targetDir: "runtime", |
| 248 | +}, |
| 249 | +timeoutMs: 30_000, |
| 250 | +}); |
| 251 | + |
| 252 | +expect(result.ok).toBe(false); |
| 253 | +expect(result.stderr).toContain("Download failed (500 Server Error)"); |
| 254 | +expect(wasCanceled()).toBe(true); |
| 255 | +expect(release).toHaveBeenCalledOnce(); |
| 256 | +}); |
| 257 | + |
214 | 258 | it.runIf(process.platform !== "win32")( |
215 | 259 | "fails closed when the lexical tools root is rebound before the final copy", |
216 | 260 | async () => { |
|