@@ -44,6 +44,27 @@ describe("lmstudio-models", () => {
|
44 | 44 | } |
45 | 45 | return JSON.parse(init.body) as unknown; |
46 | 46 | }; |
| 47 | +const cancelTrackedResponse = ( |
| 48 | +text: string, |
| 49 | +init: ResponseInit, |
| 50 | +): { |
| 51 | +response: Response; |
| 52 | +wasCanceled: () => boolean; |
| 53 | +} => { |
| 54 | +let canceled = false; |
| 55 | +const stream = new ReadableStream<Uint8Array>({ |
| 56 | +start(controller) { |
| 57 | +controller.enqueue(new TextEncoder().encode(text)); |
| 58 | +}, |
| 59 | +cancel() { |
| 60 | +canceled = true; |
| 61 | +}, |
| 62 | +}); |
| 63 | +return { |
| 64 | +response: new Response(stream, init), |
| 65 | +wasCanceled: () => canceled, |
| 66 | +}; |
| 67 | +}; |
47 | 68 | const createModelLoadFetchMock = (params?: { |
48 | 69 | loadedContextLength?: number; |
49 | 70 | maxContextLength?: number; |
@@ -486,6 +507,39 @@ describe("lmstudio-models", () => {
|
486 | 507 | ).rejects.toThrow("LM Studio model load returned malformed JSON"); |
487 | 508 | }); |
488 | 509 | |
| 510 | +it("bounds model load error bodies", async () => { |
| 511 | +const body = `${"lmstudio load unavailable ".repeat(512)}tail`; |
| 512 | +const tracked = cancelTrackedResponse(body, { status: 503 }); |
| 513 | +const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded")); |
| 514 | +const fetchMock = vi.fn(async (url: string | URL) => { |
| 515 | +if (String(url).endsWith("/api/v1/models")) { |
| 516 | +return { |
| 517 | +ok: true, |
| 518 | +json: async () => ({ |
| 519 | +models: [{ type: "llm", key: "qwen3-8b-instruct", loaded_instances: [] }], |
| 520 | +}), |
| 521 | +}; |
| 522 | +} |
| 523 | +if (String(url).endsWith("/api/v1/models/load")) { |
| 524 | +return tracked.response; |
| 525 | +} |
| 526 | +throw new Error(`Unexpected fetch URL: ${String(url)}`); |
| 527 | +}); |
| 528 | +vi.stubGlobal("fetch", asFetch(fetchMock)); |
| 529 | + |
| 530 | +const error = await ensureLmstudioModelLoaded({ |
| 531 | +baseUrl: "http://localhost:1234/v1", |
| 532 | +modelKey: "qwen3-8b-instruct", |
| 533 | +}).catch((caught: unknown) => caught); |
| 534 | +expect(error).toBeInstanceOf(Error); |
| 535 | +expect((error as Error).message).toMatch( |
| 536 | +/LM Studio model load failed \(503\): lmstudio load unavailable/, |
| 537 | +); |
| 538 | +expect((error as Error).message).not.toContain("tail"); |
| 539 | +expect(tracked.wasCanceled()).toBe(true); |
| 540 | +expect(textSpy).not.toHaveBeenCalled(); |
| 541 | +}); |
| 542 | + |
489 | 543 | it("reloads model to the clamped default target when already loaded below the default window", async () => { |
490 | 544 | const fetchMock = createModelLoadFetchMock({ |
491 | 545 | loadedContextLength: 4096, |
|