test / helpers.ts
test / helpers.ts
// Test harness: the SDK's real Chat and ChatMessage classes, with a fake controller standing in
// for LM Studio. Stored history keeps each processed user message as the preprocessor returned it.
import { Chat, ChatMessage } from "@lmstudio/sdk";
import * as assert from "node:assert/strict";
import * as path from "node:path";
import { preprocess } from "../src/preprocessor";
import { toolsProvider } from "../src/tools";
export type AnyTool = { name: string; implementation: (params: any, ctx: any) => Promise<string> };
export class FakeCtl {
statuses: string[] = [];
constructor(public chatId: string | null, public history: Chat, public config?: Record<string, unknown>) {}
getWorkingDirectory(): string {
if (!this.chatId) throw new Error("This prediction process is not attached to a working directory.");
return path.join("C:\\Users\\someone\\.lmstudio\\working-directories", this.chatId);
}
getPluginConfig(): { get(key: string): unknown } {
if (!this.config) throw new Error("no config in tests");
const defaults: Record<string, unknown> = { recall: true, recallThresholdTokens: 24000, autoStart: true, heavyTurnCalls: 3, staleAfterTurns: 2, fullDigestTokens: 600, verbatimWindowTokens: 6000, showStatus: true };
const cfg = this.config;
return { get: (key: string) => (key in cfg ? cfg[key] : defaults[key]) };
}
async pullHistory(): Promise<Chat> {
return this.history.asMutableCopy();
}
createStatus(s: { text: string }) {
this.statuses.push(s.text);
return { setState() {}, remove() {} };
}
}
// A chat as LM Studio stores it: every processed user message is kept with what was appended.
export class Session {
history: Chat;
config?: Record<string, unknown>;
constructor(public chatId: string | null) {
this.history = Chat.from([{ role: "system", content: "You are a helpful assistant." }]);
}
ctl(): FakeCtl {
return new FakeCtl(this.chatId, this.history, this.config);
}
async send(text: string, opts: { files?: boolean } = {}): Promise<{ text: string; statuses: string[] }> {
const ctl = this.ctl();
const msg = ChatMessage.create("user", text);
if (opts.files) {
(msg as any).data.content.push({ type: "file", name: "notes.txt", identifier: "f1", sizeBytes: 10, fileType: "text/plain" });
}
const out = await preprocess(ctl as any, msg.asImmutableCopy());
const outText = typeof out === "string" ? out : out.getText();
this.history.append(typeof out === "string" ? ChatMessage.create("user", out) : out);
return { text: outText, statuses: ctl.statuses };
}
reply(text: string): void {
this.history.append("assistant", text);
}
// One assistant round of tool calls with their results, as LM Studio stores them.
toolRound(calls: Array<{ name: string; args?: Record<string, unknown>; result?: string }>): void {
calls.forEach((c, i) => {
const id = `call-${this.history.length}-${i}`;
this.history.append({ role: "assistant", content: [{ type: "toolCallRequest", toolCallRequest: { id, type: "function", name: c.name, arguments: c.args ?? {} } }] } as any);
this.history.append({ role: "tool", content: [{ type: "toolCallResult", toolCallId: id, content: c.result ?? "status: ok" }] } as any);
});
}
toolOutput(chars: number): void {
this.history.append({ role: "assistant", content: [{ type: "toolCallRequest", toolCallRequest: { id: "c", type: "function", name: "read_webpage", arguments: {} } }] } as any);
this.history.append({ role: "tool", content: [{ type: "toolCallResult", toolCallId: "c", content: "x ".repeat(chars / 2) }] } as any);
}
async tools(): Promise<AnyTool[]> {
return (await toolsProvider(this.ctl() as any)) as unknown as AnyTool[];
}
async call(name: string, params: any): Promise<string> {
const t = (await this.tools()).find((x) => x.name === name);
assert.ok(t, `tool ${name} offered`);
return t.implementation(params, { status() {}, warn() {}, signal: new AbortController().signal, callId: 1 });
}
}
// Test harness: the SDK's real Chat and ChatMessage classes, with a fake controller standing in
// for LM Studio. Stored history keeps each processed user message as the preprocessor returned it.
import { Chat, ChatMessage } from "@lmstudio/sdk";
import * as assert from "node:assert/strict";
import * as path from "node:path";
import { preprocess } from "../src/preprocessor";
import { toolsProvider } from "../src/tools";
export type AnyTool = { name: string; implementation: (params: any, ctx: any) => Promise<string> };
export class FakeCtl {
statuses: string[] = [];
constructor(public chatId: string | null, public history: Chat, public config?: Record<string, unknown>) {}
getWorkingDirectory(): string {
if (!this.chatId) throw new Error("This prediction process is not attached to a working directory.");
return path.join("C:\\Users\\someone\\.lmstudio\\working-directories", this.chatId);
}
getPluginConfig(): { get(key: string): unknown } {
if (!this.config) throw new Error("no config in tests");
const defaults: Record<string, unknown> = { recall: true, recallThresholdTokens: 24000, autoStart: true, heavyTurnCalls: 3, staleAfterTurns: 2, fullDigestTokens: 600, verbatimWindowTokens: 6000, showStatus: true };
const cfg = this.config;
return { get: (key: string) => (key in cfg ? cfg[key] : defaults[key]) };
}
async pullHistory(): Promise<Chat> {
return this.history.asMutableCopy();
}
createStatus(s: { text: string }) {
this.statuses.push(s.text);
return { setState() {}, remove() {} };
}
}
// A chat as LM Studio stores it: every processed user message is kept with what was appended.
export class Session {
history: Chat;
config?: Record<string, unknown>;
constructor(public chatId: string | null) {
this.history = Chat.from([{ role: "system", content: "You are a helpful assistant." }]);
}
ctl(): FakeCtl {
return new FakeCtl(this.chatId, this.history, this.config);
}
async send(text: string, opts: { files?: boolean } = {}): Promise<{ text: string; statuses: string[] }> {
const ctl = this.ctl();
const msg = ChatMessage.create("user", text);
if (opts.files) {
(msg as any).data.content.push({ type: "file", name: "notes.txt", identifier: "f1", sizeBytes: 10, fileType: "text/plain" });
}
const out = await preprocess(ctl as any, msg.asImmutableCopy());
const outText = typeof out === "string" ? out : out.getText();
this.history.append(typeof out === "string" ? ChatMessage.create("user", out) : out);
return { text: outText, statuses: ctl.statuses };
}
reply(text: string): void {
this.history.append("assistant", text);
}
// One assistant round of tool calls with their results, as LM Studio stores them.
toolRound(calls: Array<{ name: string; args?: Record<string, unknown>; result?: string }>): void {
calls.forEach((c, i) => {
const id = `call-${this.history.length}-${i}`;
this.history.append({ role: "assistant", content: [{ type: "toolCallRequest", toolCallRequest: { id, type: "function", name: c.name, arguments: c.args ?? {} } }] } as any);
this.history.append({ role: "tool", content: [{ type: "toolCallResult", toolCallId: id, content: c.result ?? "status: ok" }] } as any);
});
}
toolOutput(chars: number): void {
this.history.append({ role: "assistant", content: [{ type: "toolCallRequest", toolCallRequest: { id: "c", type: "function", name: "read_webpage", arguments: {} } }] } as any);
this.history.append({ role: "tool", content: [{ type: "toolCallResult", toolCallId: "c", content: "x ".repeat(chars / 2) }] } as any);
}
async tools(): Promise<AnyTool[]> {
return (await toolsProvider(this.ctl() as any)) as unknown as AnyTool[];
}
async call(name: string, params: any): Promise<string> {
const t = (await this.tools()).find((x) => x.name === name);
assert.ok(t, `tool ${name} offered`);
return t.implementation(params, { status() {}, warn() {}, signal: new AbortController().signal, callId: 1 });
}
}