index.test.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
  2. import { APIPromise } from '@opencode-ai/sdk/core/api-promise';
  3. import util from 'node:util';
  4. import Opencode from '@opencode-ai/sdk';
  5. import { APIUserAbortError } from '@opencode-ai/sdk';
  6. const defaultFetch = fetch;
  7. describe('instantiate client', () => {
  8. const env = process.env;
  9. beforeEach(() => {
  10. jest.resetModules();
  11. process.env = { ...env };
  12. });
  13. afterEach(() => {
  14. process.env = env;
  15. });
  16. describe('defaultHeaders', () => {
  17. const client = new Opencode({
  18. baseURL: 'http://localhost:5000/',
  19. defaultHeaders: { 'X-My-Default-Header': '2' },
  20. });
  21. test('they are used in the request', async () => {
  22. const { req } = await client.buildRequest({ path: '/foo', method: 'post' });
  23. expect(req.headers.get('x-my-default-header')).toEqual('2');
  24. });
  25. test('can ignore `undefined` and leave the default', async () => {
  26. const { req } = await client.buildRequest({
  27. path: '/foo',
  28. method: 'post',
  29. headers: { 'X-My-Default-Header': undefined },
  30. });
  31. expect(req.headers.get('x-my-default-header')).toEqual('2');
  32. });
  33. test('can be removed with `null`', async () => {
  34. const { req } = await client.buildRequest({
  35. path: '/foo',
  36. method: 'post',
  37. headers: { 'X-My-Default-Header': null },
  38. });
  39. expect(req.headers.has('x-my-default-header')).toBe(false);
  40. });
  41. });
  42. describe('logging', () => {
  43. const env = process.env;
  44. beforeEach(() => {
  45. process.env = { ...env };
  46. process.env['OPENCODE_LOG'] = undefined;
  47. });
  48. afterEach(() => {
  49. process.env = env;
  50. });
  51. const forceAPIResponseForClient = async (client: Opencode) => {
  52. await new APIPromise(
  53. client,
  54. Promise.resolve({
  55. response: new Response(),
  56. controller: new AbortController(),
  57. requestLogID: 'log_000000',
  58. retryOfRequestLogID: undefined,
  59. startTime: Date.now(),
  60. options: {
  61. method: 'get',
  62. path: '/',
  63. },
  64. }),
  65. );
  66. };
  67. test('debug logs when log level is debug', async () => {
  68. const debugMock = jest.fn();
  69. const logger = {
  70. debug: debugMock,
  71. info: jest.fn(),
  72. warn: jest.fn(),
  73. error: jest.fn(),
  74. };
  75. const client = new Opencode({ logger: logger, logLevel: 'debug' });
  76. await forceAPIResponseForClient(client);
  77. expect(debugMock).toHaveBeenCalled();
  78. });
  79. test('default logLevel is warn', async () => {
  80. const client = new Opencode({});
  81. expect(client.logLevel).toBe('warn');
  82. });
  83. test('debug logs are skipped when log level is info', async () => {
  84. const debugMock = jest.fn();
  85. const logger = {
  86. debug: debugMock,
  87. info: jest.fn(),
  88. warn: jest.fn(),
  89. error: jest.fn(),
  90. };
  91. const client = new Opencode({ logger: logger, logLevel: 'info' });
  92. await forceAPIResponseForClient(client);
  93. expect(debugMock).not.toHaveBeenCalled();
  94. });
  95. test('debug logs happen with debug env var', async () => {
  96. const debugMock = jest.fn();
  97. const logger = {
  98. debug: debugMock,
  99. info: jest.fn(),
  100. warn: jest.fn(),
  101. error: jest.fn(),
  102. };
  103. process.env['OPENCODE_LOG'] = 'debug';
  104. const client = new Opencode({ logger: logger });
  105. expect(client.logLevel).toBe('debug');
  106. await forceAPIResponseForClient(client);
  107. expect(debugMock).toHaveBeenCalled();
  108. });
  109. test('warn when env var level is invalid', async () => {
  110. const warnMock = jest.fn();
  111. const logger = {
  112. debug: jest.fn(),
  113. info: jest.fn(),
  114. warn: warnMock,
  115. error: jest.fn(),
  116. };
  117. process.env['OPENCODE_LOG'] = 'not a log level';
  118. const client = new Opencode({ logger: logger });
  119. expect(client.logLevel).toBe('warn');
  120. expect(warnMock).toHaveBeenCalledWith(
  121. 'process.env[\'OPENCODE_LOG\'] was set to "not a log level", expected one of ["off","error","warn","info","debug"]',
  122. );
  123. });
  124. test('client log level overrides env var', async () => {
  125. const debugMock = jest.fn();
  126. const logger = {
  127. debug: debugMock,
  128. info: jest.fn(),
  129. warn: jest.fn(),
  130. error: jest.fn(),
  131. };
  132. process.env['OPENCODE_LOG'] = 'debug';
  133. const client = new Opencode({ logger: logger, logLevel: 'off' });
  134. await forceAPIResponseForClient(client);
  135. expect(debugMock).not.toHaveBeenCalled();
  136. });
  137. test('no warning logged for invalid env var level + valid client level', async () => {
  138. const warnMock = jest.fn();
  139. const logger = {
  140. debug: jest.fn(),
  141. info: jest.fn(),
  142. warn: warnMock,
  143. error: jest.fn(),
  144. };
  145. process.env['OPENCODE_LOG'] = 'not a log level';
  146. const client = new Opencode({ logger: logger, logLevel: 'debug' });
  147. expect(client.logLevel).toBe('debug');
  148. expect(warnMock).not.toHaveBeenCalled();
  149. });
  150. });
  151. describe('defaultQuery', () => {
  152. test('with null query params given', () => {
  153. const client = new Opencode({ baseURL: 'http://localhost:5000/', defaultQuery: { apiVersion: 'foo' } });
  154. expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/foo?apiVersion=foo');
  155. });
  156. test('multiple default query params', () => {
  157. const client = new Opencode({
  158. baseURL: 'http://localhost:5000/',
  159. defaultQuery: { apiVersion: 'foo', hello: 'world' },
  160. });
  161. expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/foo?apiVersion=foo&hello=world');
  162. });
  163. test('overriding with `undefined`', () => {
  164. const client = new Opencode({ baseURL: 'http://localhost:5000/', defaultQuery: { hello: 'world' } });
  165. expect(client.buildURL('/foo', { hello: undefined })).toEqual('http://localhost:5000/foo');
  166. });
  167. });
  168. test('custom fetch', async () => {
  169. const client = new Opencode({
  170. baseURL: 'http://localhost:5000/',
  171. fetch: (url) => {
  172. return Promise.resolve(
  173. new Response(JSON.stringify({ url, custom: true }), {
  174. headers: { 'Content-Type': 'application/json' },
  175. }),
  176. );
  177. },
  178. });
  179. const response = await client.get('/foo');
  180. expect(response).toEqual({ url: 'http://localhost:5000/foo', custom: true });
  181. });
  182. test('explicit global fetch', async () => {
  183. // make sure the global fetch type is assignable to our Fetch type
  184. const client = new Opencode({ baseURL: 'http://localhost:5000/', fetch: defaultFetch });
  185. });
  186. test('custom signal', async () => {
  187. const client = new Opencode({
  188. baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
  189. fetch: (...args) => {
  190. return new Promise((resolve, reject) =>
  191. setTimeout(
  192. () =>
  193. defaultFetch(...args)
  194. .then(resolve)
  195. .catch(reject),
  196. 300,
  197. ),
  198. );
  199. },
  200. });
  201. const controller = new AbortController();
  202. setTimeout(() => controller.abort(), 200);
  203. const spy = jest.spyOn(client, 'request');
  204. await expect(client.get('/foo', { signal: controller.signal })).rejects.toThrowError(APIUserAbortError);
  205. expect(spy).toHaveBeenCalledTimes(1);
  206. });
  207. test('normalized method', async () => {
  208. let capturedRequest: RequestInit | undefined;
  209. const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
  210. capturedRequest = init;
  211. return new Response(JSON.stringify({}), { headers: { 'Content-Type': 'application/json' } });
  212. };
  213. const client = new Opencode({ baseURL: 'http://localhost:5000/', fetch: testFetch });
  214. await client.patch('/foo');
  215. expect(capturedRequest?.method).toEqual('PATCH');
  216. });
  217. describe('baseUrl', () => {
  218. test('trailing slash', () => {
  219. const client = new Opencode({ baseURL: 'http://localhost:5000/custom/path/' });
  220. expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/custom/path/foo');
  221. });
  222. test('no trailing slash', () => {
  223. const client = new Opencode({ baseURL: 'http://localhost:5000/custom/path' });
  224. expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/custom/path/foo');
  225. });
  226. afterEach(() => {
  227. process.env['OPENCODE_BASE_URL'] = undefined;
  228. });
  229. test('explicit option', () => {
  230. const client = new Opencode({ baseURL: 'https://example.com' });
  231. expect(client.baseURL).toEqual('https://example.com');
  232. });
  233. test('env variable', () => {
  234. process.env['OPENCODE_BASE_URL'] = 'https://example.com/from_env';
  235. const client = new Opencode({});
  236. expect(client.baseURL).toEqual('https://example.com/from_env');
  237. });
  238. test('empty env variable', () => {
  239. process.env['OPENCODE_BASE_URL'] = ''; // empty
  240. const client = new Opencode({});
  241. expect(client.baseURL).toEqual('http://localhost:54321');
  242. });
  243. test('blank env variable', () => {
  244. process.env['OPENCODE_BASE_URL'] = ' '; // blank
  245. const client = new Opencode({});
  246. expect(client.baseURL).toEqual('http://localhost:54321');
  247. });
  248. test('in request options', () => {
  249. const client = new Opencode({});
  250. expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual(
  251. 'http://localhost:5000/option/foo',
  252. );
  253. });
  254. test('in request options overridden by client options', () => {
  255. const client = new Opencode({ baseURL: 'http://localhost:5000/client' });
  256. expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual(
  257. 'http://localhost:5000/client/foo',
  258. );
  259. });
  260. test('in request options overridden by env variable', () => {
  261. process.env['OPENCODE_BASE_URL'] = 'http://localhost:5000/env';
  262. const client = new Opencode({});
  263. expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual(
  264. 'http://localhost:5000/env/foo',
  265. );
  266. });
  267. });
  268. test('maxRetries option is correctly set', () => {
  269. const client = new Opencode({ maxRetries: 4 });
  270. expect(client.maxRetries).toEqual(4);
  271. // default
  272. const client2 = new Opencode({});
  273. expect(client2.maxRetries).toEqual(2);
  274. });
  275. describe('withOptions', () => {
  276. test('creates a new client with overridden options', async () => {
  277. const client = new Opencode({ baseURL: 'http://localhost:5000/', maxRetries: 3 });
  278. const newClient = client.withOptions({
  279. maxRetries: 5,
  280. baseURL: 'http://localhost:5001/',
  281. });
  282. // Verify the new client has updated options
  283. expect(newClient.maxRetries).toEqual(5);
  284. expect(newClient.baseURL).toEqual('http://localhost:5001/');
  285. // Verify the original client is unchanged
  286. expect(client.maxRetries).toEqual(3);
  287. expect(client.baseURL).toEqual('http://localhost:5000/');
  288. // Verify it's a different instance
  289. expect(newClient).not.toBe(client);
  290. expect(newClient.constructor).toBe(client.constructor);
  291. });
  292. test('inherits options from the parent client', async () => {
  293. const client = new Opencode({
  294. baseURL: 'http://localhost:5000/',
  295. defaultHeaders: { 'X-Test-Header': 'test-value' },
  296. defaultQuery: { 'test-param': 'test-value' },
  297. });
  298. const newClient = client.withOptions({
  299. baseURL: 'http://localhost:5001/',
  300. });
  301. // Test inherited options remain the same
  302. expect(newClient.buildURL('/foo', null)).toEqual('http://localhost:5001/foo?test-param=test-value');
  303. const { req } = await newClient.buildRequest({ path: '/foo', method: 'get' });
  304. expect(req.headers.get('x-test-header')).toEqual('test-value');
  305. });
  306. test('respects runtime property changes when creating new client', () => {
  307. const client = new Opencode({ baseURL: 'http://localhost:5000/', timeout: 1000 });
  308. // Modify the client properties directly after creation
  309. client.baseURL = 'http://localhost:6000/';
  310. client.timeout = 2000;
  311. // Create a new client with withOptions
  312. const newClient = client.withOptions({
  313. maxRetries: 10,
  314. });
  315. // Verify the new client uses the updated properties, not the original ones
  316. expect(newClient.baseURL).toEqual('http://localhost:6000/');
  317. expect(newClient.timeout).toEqual(2000);
  318. expect(newClient.maxRetries).toEqual(10);
  319. // Original client should still have its modified properties
  320. expect(client.baseURL).toEqual('http://localhost:6000/');
  321. expect(client.timeout).toEqual(2000);
  322. expect(client.maxRetries).not.toEqual(10);
  323. // Verify URL building uses the updated baseURL
  324. expect(newClient.buildURL('/bar', null)).toEqual('http://localhost:6000/bar');
  325. });
  326. });
  327. });
  328. describe('request building', () => {
  329. const client = new Opencode({});
  330. describe('custom headers', () => {
  331. test('handles undefined', async () => {
  332. const { req } = await client.buildRequest({
  333. path: '/foo',
  334. method: 'post',
  335. body: { value: 'hello' },
  336. headers: { 'X-Foo': 'baz', 'x-foo': 'bar', 'x-Foo': undefined, 'x-baz': 'bam', 'X-Baz': null },
  337. });
  338. expect(req.headers.get('x-foo')).toEqual('bar');
  339. expect(req.headers.get('x-Foo')).toEqual('bar');
  340. expect(req.headers.get('X-Foo')).toEqual('bar');
  341. expect(req.headers.get('x-baz')).toEqual(null);
  342. });
  343. });
  344. });
  345. describe('default encoder', () => {
  346. const client = new Opencode({});
  347. class Serializable {
  348. toJSON() {
  349. return { $type: 'Serializable' };
  350. }
  351. }
  352. class Collection<T> {
  353. #things: T[];
  354. constructor(things: T[]) {
  355. this.#things = Array.from(things);
  356. }
  357. toJSON() {
  358. return Array.from(this.#things);
  359. }
  360. [Symbol.iterator]() {
  361. return this.#things[Symbol.iterator];
  362. }
  363. }
  364. for (const jsonValue of [{}, [], { __proto__: null }, new Serializable(), new Collection(['item'])]) {
  365. test(`serializes ${util.inspect(jsonValue)} as json`, async () => {
  366. const { req } = await client.buildRequest({
  367. path: '/foo',
  368. method: 'post',
  369. body: jsonValue,
  370. });
  371. expect(req.headers).toBeInstanceOf(Headers);
  372. expect(req.headers.get('content-type')).toEqual('application/json');
  373. expect(req.body).toBe(JSON.stringify(jsonValue));
  374. });
  375. }
  376. const encoder = new TextEncoder();
  377. const asyncIterable = (async function* () {
  378. yield encoder.encode('a\n');
  379. yield encoder.encode('b\n');
  380. yield encoder.encode('c\n');
  381. })();
  382. for (const streamValue of [
  383. [encoder.encode('a\nb\nc\n')][Symbol.iterator](),
  384. new Response('a\nb\nc\n').body,
  385. asyncIterable,
  386. ]) {
  387. test(`converts ${util.inspect(streamValue)} to ReadableStream`, async () => {
  388. const { req } = await client.buildRequest({
  389. path: '/foo',
  390. method: 'post',
  391. body: streamValue,
  392. });
  393. expect(req.headers).toBeInstanceOf(Headers);
  394. expect(req.headers.get('content-type')).toEqual(null);
  395. expect(req.body).toBeInstanceOf(ReadableStream);
  396. expect(await new Response(req.body).text()).toBe('a\nb\nc\n');
  397. });
  398. }
  399. test(`can set content-type for ReadableStream`, async () => {
  400. const { req } = await client.buildRequest({
  401. path: '/foo',
  402. method: 'post',
  403. body: new Response('a\nb\nc\n').body,
  404. headers: { 'Content-Type': 'text/plain' },
  405. });
  406. expect(req.headers).toBeInstanceOf(Headers);
  407. expect(req.headers.get('content-type')).toEqual('text/plain');
  408. expect(req.body).toBeInstanceOf(ReadableStream);
  409. expect(await new Response(req.body).text()).toBe('a\nb\nc\n');
  410. });
  411. });
  412. describe('retries', () => {
  413. test('retry on timeout', async () => {
  414. let count = 0;
  415. const testFetch = async (
  416. url: string | URL | Request,
  417. { signal }: RequestInit = {},
  418. ): Promise<Response> => {
  419. if (count++ === 0) {
  420. return new Promise(
  421. (resolve, reject) => signal?.addEventListener('abort', () => reject(new Error('timed out'))),
  422. );
  423. }
  424. return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
  425. };
  426. const client = new Opencode({ timeout: 10, fetch: testFetch });
  427. expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
  428. expect(count).toEqual(2);
  429. expect(
  430. await client
  431. .request({ path: '/foo', method: 'get' })
  432. .asResponse()
  433. .then((r) => r.text()),
  434. ).toEqual(JSON.stringify({ a: 1 }));
  435. expect(count).toEqual(3);
  436. });
  437. test('retry count header', async () => {
  438. let count = 0;
  439. let capturedRequest: RequestInit | undefined;
  440. const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
  441. count++;
  442. if (count <= 2) {
  443. return new Response(undefined, {
  444. status: 429,
  445. headers: {
  446. 'Retry-After': '0.1',
  447. },
  448. });
  449. }
  450. capturedRequest = init;
  451. return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
  452. };
  453. const client = new Opencode({ fetch: testFetch, maxRetries: 4 });
  454. expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
  455. expect((capturedRequest!.headers as Headers).get('x-stainless-retry-count')).toEqual('2');
  456. expect(count).toEqual(3);
  457. });
  458. test('omit retry count header', async () => {
  459. let count = 0;
  460. let capturedRequest: RequestInit | undefined;
  461. const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
  462. count++;
  463. if (count <= 2) {
  464. return new Response(undefined, {
  465. status: 429,
  466. headers: {
  467. 'Retry-After': '0.1',
  468. },
  469. });
  470. }
  471. capturedRequest = init;
  472. return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
  473. };
  474. const client = new Opencode({ fetch: testFetch, maxRetries: 4 });
  475. expect(
  476. await client.request({
  477. path: '/foo',
  478. method: 'get',
  479. headers: { 'X-Stainless-Retry-Count': null },
  480. }),
  481. ).toEqual({ a: 1 });
  482. expect((capturedRequest!.headers as Headers).has('x-stainless-retry-count')).toBe(false);
  483. });
  484. test('omit retry count header by default', async () => {
  485. let count = 0;
  486. let capturedRequest: RequestInit | undefined;
  487. const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
  488. count++;
  489. if (count <= 2) {
  490. return new Response(undefined, {
  491. status: 429,
  492. headers: {
  493. 'Retry-After': '0.1',
  494. },
  495. });
  496. }
  497. capturedRequest = init;
  498. return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
  499. };
  500. const client = new Opencode({
  501. fetch: testFetch,
  502. maxRetries: 4,
  503. defaultHeaders: { 'X-Stainless-Retry-Count': null },
  504. });
  505. expect(
  506. await client.request({
  507. path: '/foo',
  508. method: 'get',
  509. }),
  510. ).toEqual({ a: 1 });
  511. expect(capturedRequest!.headers as Headers).not.toHaveProperty('x-stainless-retry-count');
  512. });
  513. test('overwrite retry count header', async () => {
  514. let count = 0;
  515. let capturedRequest: RequestInit | undefined;
  516. const testFetch = async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
  517. count++;
  518. if (count <= 2) {
  519. return new Response(undefined, {
  520. status: 429,
  521. headers: {
  522. 'Retry-After': '0.1',
  523. },
  524. });
  525. }
  526. capturedRequest = init;
  527. return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
  528. };
  529. const client = new Opencode({ fetch: testFetch, maxRetries: 4 });
  530. expect(
  531. await client.request({
  532. path: '/foo',
  533. method: 'get',
  534. headers: { 'X-Stainless-Retry-Count': '42' },
  535. }),
  536. ).toEqual({ a: 1 });
  537. expect((capturedRequest!.headers as Headers).get('x-stainless-retry-count')).toEqual('42');
  538. });
  539. test('retry on 429 with retry-after', async () => {
  540. let count = 0;
  541. const testFetch = async (
  542. url: string | URL | Request,
  543. { signal }: RequestInit = {},
  544. ): Promise<Response> => {
  545. if (count++ === 0) {
  546. return new Response(undefined, {
  547. status: 429,
  548. headers: {
  549. 'Retry-After': '0.1',
  550. },
  551. });
  552. }
  553. return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
  554. };
  555. const client = new Opencode({ fetch: testFetch });
  556. expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
  557. expect(count).toEqual(2);
  558. expect(
  559. await client
  560. .request({ path: '/foo', method: 'get' })
  561. .asResponse()
  562. .then((r) => r.text()),
  563. ).toEqual(JSON.stringify({ a: 1 }));
  564. expect(count).toEqual(3);
  565. });
  566. test('retry on 429 with retry-after-ms', async () => {
  567. let count = 0;
  568. const testFetch = async (
  569. url: string | URL | Request,
  570. { signal }: RequestInit = {},
  571. ): Promise<Response> => {
  572. if (count++ === 0) {
  573. return new Response(undefined, {
  574. status: 429,
  575. headers: {
  576. 'Retry-After-Ms': '10',
  577. },
  578. });
  579. }
  580. return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
  581. };
  582. const client = new Opencode({ fetch: testFetch });
  583. expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
  584. expect(count).toEqual(2);
  585. expect(
  586. await client
  587. .request({ path: '/foo', method: 'get' })
  588. .asResponse()
  589. .then((r) => r.text()),
  590. ).toEqual(JSON.stringify({ a: 1 }));
  591. expect(count).toEqual(3);
  592. });
  593. });