
























@@ -36,6 +36,57 @@ afterAll(() => {
36363737type StreamEvent = { type: string } & Record<string, unknown>;
383839+function requireRecord(value: unknown, label: string): Record<string, unknown> {
40+if (!value || typeof value !== "object" || Array.isArray(value)) {
41+throw new Error(`expected ${label} to be a record`);
42+}
43+return value as Record<string, unknown>;
44+}
45+46+function expectRecordFields(record: Record<string, unknown>, fields: Record<string, unknown>) {
47+for (const [key, value] of Object.entries(fields)) {
48+expect(record[key]).toEqual(value);
49+}
50+}
51+52+function expectSingleDoneEvent(events: StreamEvent[]) {
53+expect(events).toHaveLength(1);
54+expect(events[0]?.type).toBe("done");
55+}
56+57+function requireMockCallArg(mock: { mock: { calls: unknown[][] } }, label: string) {
58+const call = mock.mock.calls[0];
59+if (!call) {
60+throw new Error(`expected ${label} call`);
61+}
62+return call;
63+}
64+65+function expectEnsureLoadedFields(fields: Record<string, unknown>) {
66+const [params] = requireMockCallArg(ensureLmstudioModelLoadedMock, "ensureLmstudioModelLoaded");
67+const record = requireRecord(params, "ensureLmstudioModelLoaded params");
68+for (const [key, value] of Object.entries(fields)) {
69+if (key === "ssrfPolicy") {
70+expectRecordFields(
71+requireRecord(record.ssrfPolicy, "ssrfPolicy"),
72+value as Record<string, unknown>,
73+);
74+} else {
75+expect(record[key]).toEqual(value);
76+}
77+}
78+}
79+80+function expectBaseStreamModelFields(baseStream: StreamFn, fields: Record<string, unknown>) {
81+const call = requireMockCallArg(
82+baseStream as unknown as { mock: { calls: unknown[][] } },
83+"base stream",
84+);
85+expectRecordFields(requireRecord(call[0], "base stream model"), fields);
86+expect(call[1]).toBeDefined();
87+expect(call[2]).toBeUndefined();
88+}
89+3990async function collectEvents(stream: ReturnType<StreamFn>): Promise<StreamEvent[]> {
4091const resolved = stream instanceof Promise ? await stream : stream;
4192const events: StreamEvent[] = [];
@@ -135,17 +186,15 @@ describe("lmstudio stream wrapper", () => {
135186);
136187const events = await collectEvents(stream);
137188138-expect(events).toEqual([expect.objectContaining({ type: "done" })]);
189+expectSingleDoneEvent(events);
139190expect(ensureLmstudioModelLoadedMock).toHaveBeenCalledTimes(1);
140-expect(ensureLmstudioModelLoadedMock).toHaveBeenCalledWith(
141-expect.objectContaining({
142-baseUrl: "http://lmstudio.internal:1234/v1",
143-modelKey: "qwen3-8b-instruct",
144-requestedContextLength: 131072,
145-apiKey: "lmstudio-token",
146-ssrfPolicy: { allowedHostnames: ["lmstudio.internal"] },
147-}),
148-);
191+expectEnsureLoadedFields({
192+baseUrl: "http://lmstudio.internal:1234/v1",
193+modelKey: "qwen3-8b-instruct",
194+requestedContextLength: 131072,
195+apiKey: "lmstudio-token",
196+ssrfPolicy: { allowedHostnames: ["lmstudio.internal"] },
197+});
149198});
150199151200it("prefers model contextTokens over contextWindow for preload requests", async () => {
@@ -160,17 +209,15 @@ describe("lmstudio stream wrapper", () => {
160209);
161210const events = await collectEvents(stream);
162211163-expect(events).toEqual([expect.objectContaining({ type: "done" })]);
212+expectSingleDoneEvent(events);
164213expect(ensureLmstudioModelLoadedMock).toHaveBeenCalledTimes(1);
165-expect(ensureLmstudioModelLoadedMock).toHaveBeenCalledWith(
166-expect.objectContaining({
167-baseUrl: "http://lmstudio.internal:1234/v1",
168-modelKey: "qwen3-8b-instruct",
169-requestedContextLength: 64000,
170-apiKey: "lmstudio-token",
171-ssrfPolicy: { allowedHostnames: ["lmstudio.internal"] },
172-}),
173-);
214+expectEnsureLoadedFields({
215+baseUrl: "http://lmstudio.internal:1234/v1",
216+modelKey: "qwen3-8b-instruct",
217+requestedContextLength: 64000,
218+apiKey: "lmstudio-token",
219+ssrfPolicy: { allowedHostnames: ["lmstudio.internal"] },
220+});
174221});
175222176223it("continues inference when preload fails", async () => {
@@ -202,7 +249,7 @@ describe("lmstudio stream wrapper", () => {
202249undefined as never,
203250);
204251const events = await collectEvents(stream);
205-expect(events).toEqual([expect.objectContaining({ type: "done" })]);
252+expectSingleDoneEvent(events);
206253expect(baseStream).toHaveBeenCalledTimes(1);
207254});
208255@@ -237,16 +284,16 @@ describe("lmstudio stream wrapper", () => {
237284),
238285);
239286240-expect(events).toEqual([expect.objectContaining({ type: "done" })]);
287+expectSingleDoneEvent(events);
241288expect(ensureLmstudioModelLoadedMock).not.toHaveBeenCalled();
242289expect(baseStream).toHaveBeenCalledTimes(1);
243-expect(baseStream).toHaveBeenCalledWith(
244-expect.objectContaining({
245-compat: expect.objectContaining({ supportsUsageInStreaming: true }),
246-}),
247-expect.anything(),
248-undefined,
290+const [model] = requireMockCallArg(
291+baseStream as unknown as { mock: { calls: unknown[][] } },
292+"base stream",
249293);
294+expectRecordFields(requireRecord(requireRecord(model, "base stream model").compat, "compat"), {
295+supportsUsageInStreaming: true,
296+});
250297});
251298252299it("dedupes concurrent preload requests for the same model and context", async () => {
@@ -308,8 +355,8 @@ describe("lmstudio stream wrapper", () => {
308355resolvePreload();
309356const [firstEvents, secondEvents] = await Promise.all([firstPromise, secondPromise]);
310357311-expect(firstEvents).toEqual([expect.objectContaining({ type: "done" })]);
312-expect(secondEvents).toEqual([expect.objectContaining({ type: "done" })]);
358+expectSingleDoneEvent(firstEvents);
359+expectSingleDoneEvent(secondEvents);
313360expect(ensureLmstudioModelLoadedMock).toHaveBeenCalledTimes(1);
314361});
315362@@ -343,7 +390,7 @@ describe("lmstudio stream wrapper", () => {
343390undefined as never,
344391),
345392);
346-expect(firstEvents).toEqual([expect.objectContaining({ type: "done" })]);
393+expectSingleDoneEvent(firstEvents);
347394expect(ensureLmstudioModelLoadedMock).toHaveBeenCalledTimes(1);
348395349396const secondEvents = await collectEvents(
@@ -357,7 +404,7 @@ describe("lmstudio stream wrapper", () => {
357404undefined as never,
358405),
359406);
360-expect(secondEvents).toEqual([expect.objectContaining({ type: "done" })]);
407+expectSingleDoneEvent(secondEvents);
361408// The second call must NOT retry preload because cooldown is active, but
362409// the underlying stream must still run so the user gets a response.
363410expect(ensureLmstudioModelLoadedMock).toHaveBeenCalledTimes(1);
@@ -450,19 +497,17 @@ describe("lmstudio stream wrapper", () => {
450497);
451498const events = await collectEvents(stream);
452499453-expect(events).toEqual([expect.objectContaining({ type: "done" })]);
500+expectSingleDoneEvent(events);
454501expect(baseStream).toHaveBeenCalledTimes(1);
455-expect(baseStream).toHaveBeenCalledWith(
456-expect.objectContaining({
457-provider: "lmstudio",
458-compat: expect.objectContaining({
459-supportsDeveloperRole: false,
460-supportsUsageInStreaming: true,
461-}),
462-}),
463-expect.anything(),
464-undefined,
502+expectBaseStreamModelFields(baseStream, { provider: "lmstudio" });
503+const [model] = requireMockCallArg(
504+baseStream as unknown as { mock: { calls: unknown[][] } },
505+"base stream",
465506);
507+expectRecordFields(requireRecord(requireRecord(model, "base stream model").compat, "compat"), {
508+supportsDeveloperRole: false,
509+supportsUsageInStreaming: true,
510+});
466511});
467512468513it("promotes standalone bracketed local-model tool text to a structured tool call", async () => {
@@ -511,12 +556,13 @@ describe("lmstudio stream wrapper", () => {
511556};
512557expect(done.reason).toBe("toolUse");
513558expect(done.message?.stopReason).toBe("toolUse");
514-expect(done.message?.content?.[0]).toMatchObject({
559+const toolCall = requireRecord(done.message?.content?.[0], "tool call content");
560+expectRecordFields(toolCall, {
515561type: "toolCall",
516562name: "mempalace_mempalace_search",
517563arguments: { query: "codename", wing: "personal", room: "identities" },
518564});
519-expect(String(done.message?.content?.[0]?.id)).toMatch(/^call_[a-f0-9]{24}$/);
565+expect(String(toolCall.id)).toMatch(/^call_[a-f0-9]{24}$/);
520566});
521567522568it("promotes standalone Harmony local-model tool text to a structured tool call", async () => {
@@ -555,7 +601,7 @@ describe("lmstudio stream wrapper", () => {
555601reason?: string;
556602};
557603expect(done.reason).toBe("toolUse");
558-expect(done.message?.content?.[0]).toMatchObject({
604+expectRecordFields(requireRecord(done.message?.content?.[0], "tool call content"), {
559605type: "toolCall",
560606name: "read",
561607arguments: { path: "/path/to/file", line_start: 1, line_end: 400 },
@@ -597,8 +643,14 @@ describe("lmstudio stream wrapper", () => {
597643"text_end",
598644"done",
599645]);
600-expect(events.find((event) => event.type === "text_delta")).toMatchObject({
601-delta: rawToolText,
602-});
646+expectRecordFields(
647+requireRecord(
648+events.find((event) => event.type === "text_delta"),
649+"text delta",
650+),
651+{
652+delta: rawToolText,
653+},
654+);
603655});
604656});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。