| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690 |
- // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
- import { APIPromise } from '@opencode-ai/sdk/core/api-promise';
- import util from 'node:util';
- import Opencode from '@opencode-ai/sdk';
- import { APIUserAbortError } from '@opencode-ai/sdk';
- const defaultFetch = fetch;
- describe('instantiate client', () => {
- const env = process.env;
- beforeEach(() => {
- jest.resetModules();
- process.env = { ...env };
- });
- afterEach(() => {
- process.env = env;
- });
- describe('defaultHeaders', () => {
- const client = new Opencode({
- baseURL: 'http://localhost:5000/',
- defaultHeaders: { 'X-My-Default-Header': '2' },
- });
- test('they are used in the request', async () => {
- const { req } = await client.buildRequest({ path: '/foo', method: 'post' });
- expect(req.headers.get('x-my-default-header')).toEqual('2');
- });
- test('can ignore `undefined` and leave the default', async () => {
- const { req } = await client.buildRequest({
- path: '/foo',
- method: 'post',
- headers: { 'X-My-Default-Header': undefined },
- });
- expect(req.headers.get('x-my-default-header')).toEqual('2');
- });
- test('can be removed with `null`', async () => {
- const { req } = await client.buildRequest({
- path: '/foo',
- method: 'post',
- headers: { 'X-My-Default-Header': null },
- });
- expect(req.headers.has('x-my-default-header')).toBe(false);
- });
- });
- describe('logging', () => {
- const env = process.env;
- beforeEach(() => {
- process.env = { ...env };
- process.env['OPENCODE_LOG'] = undefined;
- });
- afterEach(() => {
- process.env = env;
- });
- const forceAPIResponseForClient = async (client: Opencode) => {
- await new APIPromise(
- client,
- Promise.resolve({
- response: new Response(),
- controller: new AbortController(),
- requestLogID: 'log_000000',
- retryOfRequestLogID: undefined,
- startTime: Date.now(),
- options: {
- method: 'get',
- path: '/',
- },
- }),
- );
- };
- test('debug logs when log level is debug', async () => {
- const debugMock = jest.fn();
- const logger = {
- debug: debugMock,
- info: jest.fn(),
- warn: jest.fn(),
- error: jest.fn(),
- };
- const client = new Opencode({ logger: logger, logLevel: 'debug' });
- await forceAPIResponseForClient(client);
- expect(debugMock).toHaveBeenCalled();
- });
- test('default logLevel is warn', async () => {
- const client = new Opencode({});
- expect(client.logLevel).toBe('warn');
- });
- test('debug logs are skipped when log level is info', async () => {
- const debugMock = jest.fn();
- const logger = {
- debug: debugMock,
- info: jest.fn(),
- warn: jest.fn(),
- error: jest.fn(),
- };
- const client = new Opencode({ logger: logger, logLevel: 'info' });
- await forceAPIResponseForClient(client);
- expect(debugMock).not.toHaveBeenCalled();
- });
- test('debug logs happen with debug env var', async () => {
- const debugMock = jest.fn();
- const logger = {
- debug: debugMock,
- info: jest.fn(),
- warn: jest.fn(),
- error: jest.fn(),
- };
- process.env['OPENCODE_LOG'] = 'debug';
- const client = new Opencode({ logger: logger });
- expect(client.logLevel).toBe('debug');
- await forceAPIResponseForClient(client);
- expect(debugMock).toHaveBeenCalled();
- });
- test('warn when env var level is invalid', async () => {
- const warnMock = jest.fn();
- const logger = {
- debug: jest.fn(),
- info: jest.fn(),
- warn: warnMock,
- error: jest.fn(),
- };
- process.env['OPENCODE_LOG'] = 'not a log level';
- const client = new Opencode({ logger: logger });
- expect(client.logLevel).toBe('warn');
- expect(warnMock).toHaveBeenCalledWith(
- 'process.env[\'OPENCODE_LOG\'] was set to "not a log level", expected one of ["off","error","warn","info","debug"]',
- );
- });
- test('client log level overrides env var', async () => {
- const debugMock = jest.fn();
- const logger = {
- debug: debugMock,
- info: jest.fn(),
- warn: jest.fn(),
- error: jest.fn(),
- };
- process.env['OPENCODE_LOG'] = 'debug';
- const client = new Opencode({ logger: logger, logLevel: 'off' });
- await forceAPIResponseForClient(client);
- expect(debugMock).not.toHaveBeenCalled();
- });
- test('no warning logged for invalid env var level + valid client level', async () => {
- const warnMock = jest.fn();
- const logger = {
- debug: jest.fn(),
- info: jest.fn(),
- warn: warnMock,
- error: jest.fn(),
- };
- process.env['OPENCODE_LOG'] = 'not a log level';
- const client = new Opencode({ logger: logger, logLevel: 'debug' });
- expect(client.logLevel).toBe('debug');
- expect(warnMock).not.toHaveBeenCalled();
- });
- });
- describe('defaultQuery', () => {
- test('with null query params given', () => {
- const client = new Opencode({ baseURL: 'http://localhost:5000/', defaultQuery: { apiVersion: 'foo' } });
- expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/foo?apiVersion=foo');
- });
- test('multiple default query params', () => {
- const client = new Opencode({
- baseURL: 'http://localhost:5000/',
- defaultQuery: { apiVersion: 'foo', hello: 'world' },
- });
- expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/foo?apiVersion=foo&hello=world');
- });
- test('overriding with `undefined`', () => {
- const client = new Opencode({ baseURL: 'http://localhost:5000/', defaultQuery: { hello: 'world' } });
- expect(client.buildURL('/foo', { hello: undefined })).toEqual('http://localhost:5000/foo');
- });
- });
- test('custom fetch', async () => {
- const client = new Opencode({
- baseURL: 'http://localhost:5000/',
- fetch: (url) => {
- return Promise.resolve(
- new Response(JSON.stringify({ url, custom: true }), {
- headers: { 'Content-Type': 'application/json' },
- }),
- );
- },
- });
- const response = await client.get('/foo');
- expect(response).toEqual({ url: 'http://localhost:5000/foo', custom: true });
- });
- test('explicit global fetch', async () => {
- // make sure the global fetch type is assignable to our Fetch type
- const client = new Opencode({ baseURL: 'http://localhost:5000/', fetch: defaultFetch });
- });
- test('custom signal', async () => {
- const client = new Opencode({
- baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
- fetch: (...args) => {
- return new Promise((resolve, reject) =>
- setTimeout(
- () =>
- defaultFetch(...args)
- .then(resolve)
- .catch(reject),
- 300,
- ),
- );
- },
- });
- const controller = new AbortController();
- setTimeout(() => controller.abort(), 200);
- const spy = jest.spyOn(client, 'request');
- await expect(client.get('/foo', { signal: controller.signal })).rejects.toThrowError(APIUserAbortError);
- expect(spy).toHaveBeenCalledTimes(1);
- });
- test('normalized method', async () => {
- let capturedRequest: RequestInit | undefined;
- const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
- capturedRequest = init;
- return new Response(JSON.stringify({}), { headers: { 'Content-Type': 'application/json' } });
- };
- const client = new Opencode({ baseURL: 'http://localhost:5000/', fetch: testFetch });
- await client.patch('/foo');
- expect(capturedRequest?.method).toEqual('PATCH');
- });
- describe('baseUrl', () => {
- test('trailing slash', () => {
- const client = new Opencode({ baseURL: 'http://localhost:5000/custom/path/' });
- expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/custom/path/foo');
- });
- test('no trailing slash', () => {
- const client = new Opencode({ baseURL: 'http://localhost:5000/custom/path' });
- expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/custom/path/foo');
- });
- afterEach(() => {
- process.env['OPENCODE_BASE_URL'] = undefined;
- });
- test('explicit option', () => {
- const client = new Opencode({ baseURL: 'https://example.com' });
- expect(client.baseURL).toEqual('https://example.com');
- });
- test('env variable', () => {
- process.env['OPENCODE_BASE_URL'] = 'https://example.com/from_env';
- const client = new Opencode({});
- expect(client.baseURL).toEqual('https://example.com/from_env');
- });
- test('empty env variable', () => {
- process.env['OPENCODE_BASE_URL'] = ''; // empty
- const client = new Opencode({});
- expect(client.baseURL).toEqual('http://localhost:54321');
- });
- test('blank env variable', () => {
- process.env['OPENCODE_BASE_URL'] = ' '; // blank
- const client = new Opencode({});
- expect(client.baseURL).toEqual('http://localhost:54321');
- });
- test('in request options', () => {
- const client = new Opencode({});
- expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual(
- 'http://localhost:5000/option/foo',
- );
- });
- test('in request options overridden by client options', () => {
- const client = new Opencode({ baseURL: 'http://localhost:5000/client' });
- expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual(
- 'http://localhost:5000/client/foo',
- );
- });
- test('in request options overridden by env variable', () => {
- process.env['OPENCODE_BASE_URL'] = 'http://localhost:5000/env';
- const client = new Opencode({});
- expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual(
- 'http://localhost:5000/env/foo',
- );
- });
- });
- test('maxRetries option is correctly set', () => {
- const client = new Opencode({ maxRetries: 4 });
- expect(client.maxRetries).toEqual(4);
- // default
- const client2 = new Opencode({});
- expect(client2.maxRetries).toEqual(2);
- });
- describe('withOptions', () => {
- test('creates a new client with overridden options', async () => {
- const client = new Opencode({ baseURL: 'http://localhost:5000/', maxRetries: 3 });
- const newClient = client.withOptions({
- maxRetries: 5,
- baseURL: 'http://localhost:5001/',
- });
- // Verify the new client has updated options
- expect(newClient.maxRetries).toEqual(5);
- expect(newClient.baseURL).toEqual('http://localhost:5001/');
- // Verify the original client is unchanged
- expect(client.maxRetries).toEqual(3);
- expect(client.baseURL).toEqual('http://localhost:5000/');
- // Verify it's a different instance
- expect(newClient).not.toBe(client);
- expect(newClient.constructor).toBe(client.constructor);
- });
- test('inherits options from the parent client', async () => {
- const client = new Opencode({
- baseURL: 'http://localhost:5000/',
- defaultHeaders: { 'X-Test-Header': 'test-value' },
- defaultQuery: { 'test-param': 'test-value' },
- });
- const newClient = client.withOptions({
- baseURL: 'http://localhost:5001/',
- });
- // Test inherited options remain the same
- expect(newClient.buildURL('/foo', null)).toEqual('http://localhost:5001/foo?test-param=test-value');
- const { req } = await newClient.buildRequest({ path: '/foo', method: 'get' });
- expect(req.headers.get('x-test-header')).toEqual('test-value');
- });
- test('respects runtime property changes when creating new client', () => {
- const client = new Opencode({ baseURL: 'http://localhost:5000/', timeout: 1000 });
- // Modify the client properties directly after creation
- client.baseURL = 'http://localhost:6000/';
- client.timeout = 2000;
- // Create a new client with withOptions
- const newClient = client.withOptions({
- maxRetries: 10,
- });
- // Verify the new client uses the updated properties, not the original ones
- expect(newClient.baseURL).toEqual('http://localhost:6000/');
- expect(newClient.timeout).toEqual(2000);
- expect(newClient.maxRetries).toEqual(10);
- // Original client should still have its modified properties
- expect(client.baseURL).toEqual('http://localhost:6000/');
- expect(client.timeout).toEqual(2000);
- expect(client.maxRetries).not.toEqual(10);
- // Verify URL building uses the updated baseURL
- expect(newClient.buildURL('/bar', null)).toEqual('http://localhost:6000/bar');
- });
- });
- });
- describe('request building', () => {
- const client = new Opencode({});
- describe('custom headers', () => {
- test('handles undefined', async () => {
- const { req } = await client.buildRequest({
- path: '/foo',
- method: 'post',
- body: { value: 'hello' },
- headers: { 'X-Foo': 'baz', 'x-foo': 'bar', 'x-Foo': undefined, 'x-baz': 'bam', 'X-Baz': null },
- });
- expect(req.headers.get('x-foo')).toEqual('bar');
- expect(req.headers.get('x-Foo')).toEqual('bar');
- expect(req.headers.get('X-Foo')).toEqual('bar');
- expect(req.headers.get('x-baz')).toEqual(null);
- });
- });
- });
- describe('default encoder', () => {
- const client = new Opencode({});
- class Serializable {
- toJSON() {
- return { $type: 'Serializable' };
- }
- }
- class Collection<T> {
- #things: T[];
- constructor(things: T[]) {
- this.#things = Array.from(things);
- }
- toJSON() {
- return Array.from(this.#things);
- }
- [Symbol.iterator]() {
- return this.#things[Symbol.iterator];
- }
- }
- for (const jsonValue of [{}, [], { __proto__: null }, new Serializable(), new Collection(['item'])]) {
- test(`serializes ${util.inspect(jsonValue)} as json`, async () => {
- const { req } = await client.buildRequest({
- path: '/foo',
- method: 'post',
- body: jsonValue,
- });
- expect(req.headers).toBeInstanceOf(Headers);
- expect(req.headers.get('content-type')).toEqual('application/json');
- expect(req.body).toBe(JSON.stringify(jsonValue));
- });
- }
- const encoder = new TextEncoder();
- const asyncIterable = (async function* () {
- yield encoder.encode('a\n');
- yield encoder.encode('b\n');
- yield encoder.encode('c\n');
- })();
- for (const streamValue of [
- [encoder.encode('a\nb\nc\n')][Symbol.iterator](),
- new Response('a\nb\nc\n').body,
- asyncIterable,
- ]) {
- test(`converts ${util.inspect(streamValue)} to ReadableStream`, async () => {
- const { req } = await client.buildRequest({
- path: '/foo',
- method: 'post',
- body: streamValue,
- });
- expect(req.headers).toBeInstanceOf(Headers);
- expect(req.headers.get('content-type')).toEqual(null);
- expect(req.body).toBeInstanceOf(ReadableStream);
- expect(await new Response(req.body).text()).toBe('a\nb\nc\n');
- });
- }
- test(`can set content-type for ReadableStream`, async () => {
- const { req } = await client.buildRequest({
- path: '/foo',
- method: 'post',
- body: new Response('a\nb\nc\n').body,
- headers: { 'Content-Type': 'text/plain' },
- });
- expect(req.headers).toBeInstanceOf(Headers);
- expect(req.headers.get('content-type')).toEqual('text/plain');
- expect(req.body).toBeInstanceOf(ReadableStream);
- expect(await new Response(req.body).text()).toBe('a\nb\nc\n');
- });
- });
- describe('retries', () => {
- test('retry on timeout', async () => {
- let count = 0;
- const testFetch = async (
- url: string | URL | Request,
- { signal }: RequestInit = {},
- ): Promise<Response> => {
- if (count++ === 0) {
- return new Promise(
- (resolve, reject) => signal?.addEventListener('abort', () => reject(new Error('timed out'))),
- );
- }
- return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
- };
- const client = new Opencode({ timeout: 10, fetch: testFetch });
- expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
- expect(count).toEqual(2);
- expect(
- await client
- .request({ path: '/foo', method: 'get' })
- .asResponse()
- .then((r) => r.text()),
- ).toEqual(JSON.stringify({ a: 1 }));
- expect(count).toEqual(3);
- });
- test('retry count header', async () => {
- let count = 0;
- let capturedRequest: RequestInit | undefined;
- const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
- count++;
- if (count <= 2) {
- return new Response(undefined, {
- status: 429,
- headers: {
- 'Retry-After': '0.1',
- },
- });
- }
- capturedRequest = init;
- return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
- };
- const client = new Opencode({ fetch: testFetch, maxRetries: 4 });
- expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
- expect((capturedRequest!.headers as Headers).get('x-stainless-retry-count')).toEqual('2');
- expect(count).toEqual(3);
- });
- test('omit retry count header', async () => {
- let count = 0;
- let capturedRequest: RequestInit | undefined;
- const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
- count++;
- if (count <= 2) {
- return new Response(undefined, {
- status: 429,
- headers: {
- 'Retry-After': '0.1',
- },
- });
- }
- capturedRequest = init;
- return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
- };
- const client = new Opencode({ fetch: testFetch, maxRetries: 4 });
- expect(
- await client.request({
- path: '/foo',
- method: 'get',
- headers: { 'X-Stainless-Retry-Count': null },
- }),
- ).toEqual({ a: 1 });
- expect((capturedRequest!.headers as Headers).has('x-stainless-retry-count')).toBe(false);
- });
- test('omit retry count header by default', async () => {
- let count = 0;
- let capturedRequest: RequestInit | undefined;
- const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
- count++;
- if (count <= 2) {
- return new Response(undefined, {
- status: 429,
- headers: {
- 'Retry-After': '0.1',
- },
- });
- }
- capturedRequest = init;
- return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
- };
- const client = new Opencode({
- fetch: testFetch,
- maxRetries: 4,
- defaultHeaders: { 'X-Stainless-Retry-Count': null },
- });
- expect(
- await client.request({
- path: '/foo',
- method: 'get',
- }),
- ).toEqual({ a: 1 });
- expect(capturedRequest!.headers as Headers).not.toHaveProperty('x-stainless-retry-count');
- });
- test('overwrite retry count header', async () => {
- let count = 0;
- let capturedRequest: RequestInit | undefined;
- const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
- count++;
- if (count <= 2) {
- return new Response(undefined, {
- status: 429,
- headers: {
- 'Retry-After': '0.1',
- },
- });
- }
- capturedRequest = init;
- return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
- };
- const client = new Opencode({ fetch: testFetch, maxRetries: 4 });
- expect(
- await client.request({
- path: '/foo',
- method: 'get',
- headers: { 'X-Stainless-Retry-Count': '42' },
- }),
- ).toEqual({ a: 1 });
- expect((capturedRequest!.headers as Headers).get('x-stainless-retry-count')).toEqual('42');
- });
- test('retry on 429 with retry-after', async () => {
- let count = 0;
- const testFetch = async (
- url: string | URL | Request,
- { signal }: RequestInit = {},
- ): Promise<Response> => {
- if (count++ === 0) {
- return new Response(undefined, {
- status: 429,
- headers: {
- 'Retry-After': '0.1',
- },
- });
- }
- return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
- };
- const client = new Opencode({ fetch: testFetch });
- expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
- expect(count).toEqual(2);
- expect(
- await client
- .request({ path: '/foo', method: 'get' })
- .asResponse()
- .then((r) => r.text()),
- ).toEqual(JSON.stringify({ a: 1 }));
- expect(count).toEqual(3);
- });
- test('retry on 429 with retry-after-ms', async () => {
- let count = 0;
- const testFetch = async (
- url: string | URL | Request,
- { signal }: RequestInit = {},
- ): Promise<Response> => {
- if (count++ === 0) {
- return new Response(undefined, {
- status: 429,
- headers: {
- 'Retry-After-Ms': '10',
- },
- });
- }
- return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
- };
- const client = new Opencode({ fetch: testFetch });
- expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
- expect(count).toEqual(2);
- expect(
- await client
- .request({ path: '/foo', method: 'get' })
- .asResponse()
- .then((r) => r.text()),
- ).toEqual(JSON.stringify({ a: 1 }));
- expect(count).toEqual(3);
- });
- });
|