Skip to content

Commit

Permalink
fix: add client unit tests (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brandon Meyerowitz authored Jul 28, 2023
1 parent 1d8e873 commit 9008324
Show file tree
Hide file tree
Showing 13 changed files with 509 additions and 53 deletions.
2 changes: 1 addition & 1 deletion client/api/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { APIErrorResponse } from "../types";
import type { APIErrorResponse } from "../types";

export class APIError extends Error {
public readonly errorResponse: APIErrorResponse;
Expand Down
188 changes: 188 additions & 0 deletions client/api/request-util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { describe, expect, it } from "vitest";

import type {
ClientOptions,
CompletionConfig,
EmbeddingsConfig,
} from "../types";
import {
getCompletionBody,
getEmbeddingsBody,
getHeaders,
getUrl,
} from "./request-util";

const mockCompletionConfig: Required<CompletionConfig> = {
projectId: "projectId",
userId: "userId",
prompt: "prompt",
chatContext: {
messages: [{ role: "user", content: "content" }],
},
variables: {
test1: "123",
test2: "abc",
},
truncateVariable: {
strategy: "truncate_head",
granularity: "word",
maxPromptTokens: 1,
name: "name",
},
providerConfig: {
provider: "openai",
params: {
type: "chat",
},
},
};

const mockEmbeddingsConfig: Required<EmbeddingsConfig> = {
projectId: "projectId",
userId: "userId",
input: "input",
providerConfig: {
provider: "openai",
params: {
type: "embeddings",
},
},
};

type MockClientOptionsConfig = Required<ClientOptions>;
const mockClientOptions: MockClientOptionsConfig = {
projectId: "newProjectIdFromOptions",
apiKey: "apiKeyFromOptions",
defaultVariables: {
default1: "987",
default2: "zyx",
},
defaultTruncateVariableConfig: {
strategy: "off",
granularity: "line",
maxPromptTokens: 2,
name: "from default client options",
},
_extraParams: {
extraParam1: "extraParam",
},
_apiUrl: "https://new_api_url/",
_extraHeaders: {
extraHeader: "extraHeaderValue",
},
};

describe("getCompletionBody", () => {
it("formats body properly from config", () => {
const body = getCompletionBody(mockCompletionConfig, {});

expect(body).toEqual({
projectId: mockCompletionConfig.projectId,
userId: mockCompletionConfig.userId,
prompt: mockCompletionConfig.prompt,
context: mockCompletionConfig.chatContext,
variables: mockCompletionConfig.variables,
truncateVariable: mockCompletionConfig.truncateVariable,
providerConfig: mockCompletionConfig.providerConfig,
});
});

it("should apply default config from client options.", () => {
const body = getCompletionBody({}, mockClientOptions);

expect(body).toEqual({
projectId: mockClientOptions.projectId,
apiKey: mockClientOptions.apiKey,
truncateVariable: mockClientOptions.defaultTruncateVariableConfig,
...mockClientOptions._extraParams,
});

// If 'variables' is not set in the config, then the client's
// options.defaultVariables is ignored.
expect(body.variables).toBeUndefined();
});

it("should overwrite/merge client options default with config", () => {
const body = getCompletionBody(mockCompletionConfig, mockClientOptions);

expect(body).toEqual({
projectId: mockCompletionConfig.projectId,
userId: mockCompletionConfig.userId,
apiKey: mockClientOptions.apiKey,
prompt: mockCompletionConfig.prompt,
context: mockCompletionConfig.chatContext,
truncateVariable: mockCompletionConfig.truncateVariable,
variables: {
...mockClientOptions.defaultVariables,
...mockCompletionConfig.variables,
},
providerConfig: mockCompletionConfig.providerConfig,
...mockClientOptions._extraParams,
});
});
});

describe("getEmbeddingsBody", () => {
it("formats body properly from config", () => {
const body = getEmbeddingsBody(mockEmbeddingsConfig, {});

expect(body).toEqual({
projectId: mockEmbeddingsConfig.projectId,
userId: mockEmbeddingsConfig.userId,
input: mockEmbeddingsConfig.input,
providerConfig: mockEmbeddingsConfig.providerConfig,
});
});

it("should apply default config from client options.", () => {
const config: EmbeddingsConfig = { input: "input" };
const body = getEmbeddingsBody(config, mockClientOptions);

expect(body).toEqual({
...config,
projectId: mockClientOptions.projectId,
apiKey: mockClientOptions.apiKey,
...mockClientOptions._extraParams,
});
});

it("should overwrite/merge client options default with config", () => {
const body = getEmbeddingsBody(mockEmbeddingsConfig, mockClientOptions);

expect(body).toEqual({
projectId: mockEmbeddingsConfig.projectId,
apiKey: mockClientOptions.apiKey,
userId: mockEmbeddingsConfig.userId,
input: mockEmbeddingsConfig.input,
providerConfig: mockEmbeddingsConfig.providerConfig,
...mockClientOptions._extraParams,
});
});
});

describe("getUrl", () => {
it("should format url properly by default", () => {
expect(getUrl("test-path")).toBe("https://api.commonbase.com/test-path");
});

it("should use _apiUrl from ClientOptions", () => {
expect(getUrl("test-path", mockClientOptions)).toBe(
`${mockClientOptions._apiUrl}/test-path`,
);
});
});

describe("getHeaders", () => {
it("should always add json Content-Type header", () => {
expect(getHeaders({})).toEqual({
"Content-Type": "application/json; charset=utf-8",
});
});

it("should add _extraHeaders from ClientOptions", () => {
expect(getHeaders(mockClientOptions)).toEqual({
...mockClientOptions._extraHeaders,
"Content-Type": "application/json; charset=utf-8",
});
});
});
19 changes: 18 additions & 1 deletion client/api/request-util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
import { ClientOptions, CompletionConfig, EmbeddingsConfig } from "../types";
import type {
ClientOptions,
CompletionConfig,
EmbeddingsConfig,
} from "../types";

const ROOT_API_URL = "https://api.commonbase.com";

export function getUrl(path: string, options?: ClientOptions) {
return `${options?._apiUrl || ROOT_API_URL}/${path}`;
}

export function getHeaders(options: ClientOptions) {
return {
...options._extraHeaders,
"Content-Type": "application/json; charset=utf-8",
};
}

export function getCompletionBody(
config: CompletionConfig,
Expand Down
39 changes: 39 additions & 0 deletions client/api/request.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it, vi } from "vitest";

import { APIError } from "./error";
import { fetchCompletionsAPI, fetchEmbeddingsAPI } from "./request";

const mockFetch = vi.fn();
globalThis.fetch = mockFetch;

describe("fetchCompletionsAPI", () => {
it("should return Response object when fetch successful", () => {
mockFetch.mockReturnValueOnce(Promise.resolve(new Response("{}")));
const res = fetchCompletionsAPI({}, {});
expect(res).toBeInstanceOf(Promise<Response>);
});

it("should throw APIError when fetch failed", () => {
mockFetch.mockReturnValueOnce(
Promise.resolve(new Response("{}", { status: 400 })),
);
expect(fetchCompletionsAPI({}, {})).rejects.toBeInstanceOf(APIError);
});
});

describe("fetchEmbeddingsAPI", () => {
it("should return Response object when fetch successful", () => {
mockFetch.mockReturnValueOnce(Promise.resolve(new Response("{}")));
const res = fetchEmbeddingsAPI({ input: "" }, {});
expect(res).toBeInstanceOf(Promise<Response>);
});

it("should throw APIError when fetch failed", () => {
mockFetch.mockReturnValueOnce(
Promise.resolve(new Response("{}", { status: 400 })),
);
expect(fetchEmbeddingsAPI({ input: "" }, {})).rejects.toBeInstanceOf(
APIError,
);
});
});
26 changes: 11 additions & 15 deletions client/api/request.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import { ClientOptions, CompletionConfig, EmbeddingsConfig } from "../types";
import type {
ClientOptions,
CompletionConfig,
EmbeddingsConfig,
} from "../types";
import { APIError } from "./error";
import { getCompletionBody, getEmbeddingsBody } from "./request-util";

export const ROOT_API_URL = "https://api.commonbase.com";

function getUrl(path: string, options?: ClientOptions) {
return `${options?._apiUrl || ROOT_API_URL}/${path}`;
}

function getHeaders(options: ClientOptions) {
return {
...options._extraHeaders,
"Content-Type": "application/json; charset=utf-8",
};
}
import {
getCompletionBody,
getEmbeddingsBody,
getHeaders,
getUrl,
} from "./request-util";

export async function fetchCompletionsAPI(
config: CompletionConfig,
Expand Down
2 changes: 1 addition & 1 deletion client/chat-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChatClientOptions } from "./types";
import type { ChatClientOptions } from "./types";

const WEBSOCKET_URL = "wss://api.commonbase.com/chats";

Expand Down
54 changes: 54 additions & 0 deletions client/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it, vi } from "vitest";

import { Client } from "./client";
import { CompletionResult } from "./completion-result";
import { StreamConsumer } from "./stream-consumer";

const { mockFetchCompletions, mockFetchEmbeddings } = vi.hoisted(() => ({
mockFetchCompletions: vi.fn(),
mockFetchEmbeddings: vi.fn(),
}));

vi.mock("./api/request", () => ({
fetchCompletionsAPI: mockFetchCompletions,
fetchEmbeddingsAPI: mockFetchEmbeddings,
}));

describe("Client", () => {
it("should return a CompletionResult from createCompletion", () => {
mockFetchCompletions.mockReturnValueOnce(
Promise.resolve(new Response("{}")),
);
expect(new Client().createCompletion({})).resolves.toBeInstanceOf(
CompletionResult,
);
});

it("should return a StreamConsumer from createStreamingCompletion", () => {
mockFetchCompletions.mockReturnValueOnce(
Promise.resolve(new Response("{}")),
);
expect(new Client().createStreamingCompletion({})).resolves.toBeInstanceOf(
StreamConsumer,
);
});

it("should throw error on empty body from createStreamingCompletion", () => {
mockFetchCompletions.mockReturnValueOnce(Promise.resolve(new Response()));
expect(new Client().createStreamingCompletion({})).rejects.toEqual(
new Error("no stream body"),
);
});

it("should return response body json from createEmbedding", () => {
const mockResponse = {
key: "value",
};
mockFetchEmbeddings.mockReturnValueOnce(
Promise.resolve(new Response(JSON.stringify(mockResponse))),
);
expect(new Client().createEmbedding({ input: "" })).resolves.toEqual(
mockResponse,
);
});
});
4 changes: 2 additions & 2 deletions client/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { fetchCompletionsAPI, fetchEmbeddingsAPI } from "./api/request";
import { CompletionResult } from "./completion-result";
import { StreamConsumer } from "./stream-consumer";
import {
import type {
ClientOptions,
CompletionConfig,
CompletionResponse,
CompletionResult,
EmbeddingsConfig,
EmbeddingsResponse,
} from "./types";
Expand Down
Loading

0 comments on commit 9008324

Please sign in to comment.