mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 18:24:21 +01:00
chore: generate sdk into packages/sdk
This commit is contained in:
77
packages/sdk/tests/api-resources/app.test.ts
Normal file
77
packages/sdk/tests/api-resources/app.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import Opencode from '@opencode-ai/sdk';
|
||||
|
||||
const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010' });
|
||||
|
||||
describe('resource app', () => {
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('get', async () => {
|
||||
const responsePromise = client.app.get();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('init', async () => {
|
||||
const responsePromise = client.app.init();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('log: only required params', async () => {
|
||||
const responsePromise = client.app.log({ level: 'debug', message: 'message', service: 'service' });
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('log: required and optional params', async () => {
|
||||
const response = await client.app.log({
|
||||
level: 'debug',
|
||||
message: 'message',
|
||||
service: 'service',
|
||||
extra: { foo: 'bar' },
|
||||
});
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('modes', async () => {
|
||||
const responsePromise = client.app.modes();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('providers', async () => {
|
||||
const responsePromise = client.app.providers();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
});
|
||||
19
packages/sdk/tests/api-resources/config.test.ts
Normal file
19
packages/sdk/tests/api-resources/config.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import Opencode from '@opencode-ai/sdk';
|
||||
|
||||
const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010' });
|
||||
|
||||
describe('resource config', () => {
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('get', async () => {
|
||||
const responsePromise = client.config.get();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
});
|
||||
19
packages/sdk/tests/api-resources/event.test.ts
Normal file
19
packages/sdk/tests/api-resources/event.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import Opencode from '@opencode-ai/sdk';
|
||||
|
||||
const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010' });
|
||||
|
||||
describe('resource event', () => {
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('list', async () => {
|
||||
const responsePromise = client.event.list();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
});
|
||||
36
packages/sdk/tests/api-resources/file.test.ts
Normal file
36
packages/sdk/tests/api-resources/file.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import Opencode from '@opencode-ai/sdk';
|
||||
|
||||
const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010' });
|
||||
|
||||
describe('resource file', () => {
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('read: only required params', async () => {
|
||||
const responsePromise = client.file.read({ path: 'path' });
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('read: required and optional params', async () => {
|
||||
const response = await client.file.read({ path: 'path' });
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('status', async () => {
|
||||
const responsePromise = client.file.status();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
});
|
||||
58
packages/sdk/tests/api-resources/find.test.ts
Normal file
58
packages/sdk/tests/api-resources/find.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import Opencode from '@opencode-ai/sdk';
|
||||
|
||||
const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010' });
|
||||
|
||||
describe('resource find', () => {
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('files: only required params', async () => {
|
||||
const responsePromise = client.find.files({ query: 'query' });
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('files: required and optional params', async () => {
|
||||
const response = await client.find.files({ query: 'query' });
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('symbols: only required params', async () => {
|
||||
const responsePromise = client.find.symbols({ query: 'query' });
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('symbols: required and optional params', async () => {
|
||||
const response = await client.find.symbols({ query: 'query' });
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('text: only required params', async () => {
|
||||
const responsePromise = client.find.text({ pattern: 'pattern' });
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('text: required and optional params', async () => {
|
||||
const response = await client.find.text({ pattern: 'pattern' });
|
||||
});
|
||||
});
|
||||
161
packages/sdk/tests/api-resources/session.test.ts
Normal file
161
packages/sdk/tests/api-resources/session.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import Opencode from '@opencode-ai/sdk';
|
||||
|
||||
const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010' });
|
||||
|
||||
describe('resource session', () => {
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('create', async () => {
|
||||
const responsePromise = client.session.create();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('list', async () => {
|
||||
const responsePromise = client.session.list();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('delete', async () => {
|
||||
const responsePromise = client.session.delete('id');
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('abort', async () => {
|
||||
const responsePromise = client.session.abort('id');
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('chat: only required params', async () => {
|
||||
const responsePromise = client.session.chat('id', {
|
||||
modelID: 'modelID',
|
||||
parts: [{ text: 'text', type: 'text' }],
|
||||
providerID: 'providerID',
|
||||
});
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('chat: required and optional params', async () => {
|
||||
const response = await client.session.chat('id', {
|
||||
modelID: 'modelID',
|
||||
parts: [{ text: 'text', type: 'text', id: 'id', synthetic: true, time: { start: 0, end: 0 } }],
|
||||
providerID: 'providerID',
|
||||
messageID: 'msg',
|
||||
mode: 'mode',
|
||||
tools: { foo: true },
|
||||
});
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('init: only required params', async () => {
|
||||
const responsePromise = client.session.init('id', {
|
||||
messageID: 'messageID',
|
||||
modelID: 'modelID',
|
||||
providerID: 'providerID',
|
||||
});
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('init: required and optional params', async () => {
|
||||
const response = await client.session.init('id', {
|
||||
messageID: 'messageID',
|
||||
modelID: 'modelID',
|
||||
providerID: 'providerID',
|
||||
});
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('messages', async () => {
|
||||
const responsePromise = client.session.messages('id');
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('share', async () => {
|
||||
const responsePromise = client.session.share('id');
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('summarize: only required params', async () => {
|
||||
const responsePromise = client.session.summarize('id', { modelID: 'modelID', providerID: 'providerID' });
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('summarize: required and optional params', async () => {
|
||||
const response = await client.session.summarize('id', { modelID: 'modelID', providerID: 'providerID' });
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('unshare', async () => {
|
||||
const responsePromise = client.session.unshare('id');
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
});
|
||||
36
packages/sdk/tests/api-resources/tui.test.ts
Normal file
36
packages/sdk/tests/api-resources/tui.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import Opencode from '@opencode-ai/sdk';
|
||||
|
||||
const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010' });
|
||||
|
||||
describe('resource tui', () => {
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('appendPrompt: only required params', async () => {
|
||||
const responsePromise = client.tui.appendPrompt({ text: 'text' });
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('appendPrompt: required and optional params', async () => {
|
||||
const response = await client.tui.appendPrompt({ text: 'text' });
|
||||
});
|
||||
|
||||
// skipped: tests are disabled for the time being
|
||||
test.skip('openHelp', async () => {
|
||||
const responsePromise = client.tui.openHelp();
|
||||
const rawResponse = await responsePromise.asResponse();
|
||||
expect(rawResponse).toBeInstanceOf(Response);
|
||||
const response = await responsePromise;
|
||||
expect(response).not.toBeInstanceOf(Response);
|
||||
const dataAndResponse = await responsePromise.withResponse();
|
||||
expect(dataAndResponse.data).toBe(response);
|
||||
expect(dataAndResponse.response).toBe(rawResponse);
|
||||
});
|
||||
});
|
||||
80
packages/sdk/tests/base64.test.ts
Normal file
80
packages/sdk/tests/base64.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { fromBase64, toBase64 } from '@opencode-ai/sdk/internal/utils/base64';
|
||||
|
||||
describe.each(['Buffer', 'atob'])('with %s', (mode) => {
|
||||
let originalBuffer: BufferConstructor;
|
||||
beforeAll(() => {
|
||||
if (mode === 'atob') {
|
||||
originalBuffer = globalThis.Buffer;
|
||||
// @ts-expect-error Can't assign undefined to BufferConstructor
|
||||
delete globalThis.Buffer;
|
||||
}
|
||||
});
|
||||
afterAll(() => {
|
||||
if (mode === 'atob') {
|
||||
globalThis.Buffer = originalBuffer;
|
||||
}
|
||||
});
|
||||
test('toBase64', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: 'hello world',
|
||||
expected: 'aGVsbG8gd29ybGQ=',
|
||||
},
|
||||
{
|
||||
input: new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]),
|
||||
expected: 'aGVsbG8gd29ybGQ=',
|
||||
},
|
||||
{
|
||||
input: undefined,
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
input: new Uint8Array([
|
||||
229, 102, 215, 230, 65, 22, 46, 87, 243, 176, 99, 99, 31, 174, 8, 242, 83, 142, 169, 64, 122, 123,
|
||||
193, 71,
|
||||
]),
|
||||
expected: '5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH',
|
||||
},
|
||||
{
|
||||
input: '✓',
|
||||
expected: '4pyT',
|
||||
},
|
||||
{
|
||||
input: new Uint8Array([226, 156, 147]),
|
||||
expected: '4pyT',
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, expected }) => {
|
||||
expect(toBase64(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test('fromBase64', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: 'aGVsbG8gd29ybGQ=',
|
||||
expected: new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]),
|
||||
},
|
||||
{
|
||||
input: '',
|
||||
expected: new Uint8Array([]),
|
||||
},
|
||||
{
|
||||
input: '5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH',
|
||||
expected: new Uint8Array([
|
||||
229, 102, 215, 230, 65, 22, 46, 87, 243, 176, 99, 99, 31, 174, 8, 242, 83, 142, 169, 64, 122, 123,
|
||||
193, 71,
|
||||
]),
|
||||
},
|
||||
{
|
||||
input: '4pyT',
|
||||
expected: new Uint8Array([226, 156, 147]),
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, expected }) => {
|
||||
expect(fromBase64(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
88
packages/sdk/tests/buildHeaders.test.ts
Normal file
88
packages/sdk/tests/buildHeaders.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { inspect } from 'node:util';
|
||||
import { buildHeaders, type HeadersLike, type NullableHeaders } from '@opencode-ai/sdk/internal/headers';
|
||||
|
||||
function inspectNullableHeaders(headers: NullableHeaders) {
|
||||
return `NullableHeaders {${[
|
||||
...[...headers.values.entries()].map(([name, value]) => ` ${inspect(name)}: ${inspect(value)}`),
|
||||
...[...headers.nulls].map((name) => ` ${inspect(name)}: null`),
|
||||
].join(', ')} }`;
|
||||
}
|
||||
|
||||
describe('buildHeaders', () => {
|
||||
const cases: [HeadersLike[], string][] = [
|
||||
[[new Headers({ 'content-type': 'text/plain' })], `NullableHeaders { 'content-type': 'text/plain' }`],
|
||||
[
|
||||
[
|
||||
{
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
{
|
||||
'Content-Type': undefined,
|
||||
},
|
||||
],
|
||||
`NullableHeaders { 'content-type': 'text/plain' }`,
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
{
|
||||
'Content-Type': null,
|
||||
},
|
||||
],
|
||||
`NullableHeaders { 'content-type': null }`,
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
cookie: 'name1=value1',
|
||||
Cookie: 'name2=value2',
|
||||
},
|
||||
],
|
||||
`NullableHeaders { 'cookie': 'name2=value2' }`,
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
cookie: 'name1=value1',
|
||||
Cookie: undefined,
|
||||
},
|
||||
],
|
||||
`NullableHeaders { 'cookie': 'name1=value1' }`,
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
cookie: ['name1=value1', 'name2=value2'],
|
||||
},
|
||||
],
|
||||
`NullableHeaders { 'cookie': 'name1=value1; name2=value2' }`,
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
'x-foo': ['name1=value1', 'name2=value2'],
|
||||
},
|
||||
],
|
||||
`NullableHeaders { 'x-foo': 'name1=value1, name2=value2' }`,
|
||||
],
|
||||
[
|
||||
[
|
||||
[
|
||||
['cookie', 'name1=value1'],
|
||||
['cookie', 'name2=value2'],
|
||||
['Cookie', 'name3=value3'],
|
||||
],
|
||||
],
|
||||
`NullableHeaders { 'cookie': 'name1=value1; name2=value2; name3=value3' }`,
|
||||
],
|
||||
[[undefined], `NullableHeaders { }`],
|
||||
[[null], `NullableHeaders { }`],
|
||||
];
|
||||
for (const [input, expected] of cases) {
|
||||
test(expected, () => {
|
||||
expect(inspectNullableHeaders(buildHeaders(input))).toEqual(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
85
packages/sdk/tests/form.test.ts
Normal file
85
packages/sdk/tests/form.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { multipartFormRequestOptions, createForm } from '@opencode-ai/sdk/internal/uploads';
|
||||
import { toFile } from '@opencode-ai/sdk/core/uploads';
|
||||
|
||||
describe('form data validation', () => {
|
||||
test('valid values do not error', async () => {
|
||||
await multipartFormRequestOptions(
|
||||
{
|
||||
body: {
|
||||
foo: 'foo',
|
||||
string: 1,
|
||||
bool: true,
|
||||
file: await toFile(Buffer.from('some-content')),
|
||||
blob: new Blob(['Some content'], { type: 'text/plain' }),
|
||||
},
|
||||
},
|
||||
fetch,
|
||||
);
|
||||
});
|
||||
|
||||
test('null', async () => {
|
||||
await expect(() =>
|
||||
multipartFormRequestOptions(
|
||||
{
|
||||
body: {
|
||||
null: null,
|
||||
},
|
||||
},
|
||||
fetch,
|
||||
),
|
||||
).rejects.toThrow(TypeError);
|
||||
});
|
||||
|
||||
test('undefined is stripped', async () => {
|
||||
const form = await createForm(
|
||||
{
|
||||
foo: undefined,
|
||||
bar: 'baz',
|
||||
},
|
||||
fetch,
|
||||
);
|
||||
expect(form.has('foo')).toBe(false);
|
||||
expect(form.get('bar')).toBe('baz');
|
||||
});
|
||||
|
||||
test('nested undefined property is stripped', async () => {
|
||||
const form = await createForm(
|
||||
{
|
||||
bar: {
|
||||
baz: undefined,
|
||||
},
|
||||
},
|
||||
fetch,
|
||||
);
|
||||
expect(Array.from(form.entries())).toEqual([]);
|
||||
|
||||
const form2 = await createForm(
|
||||
{
|
||||
bar: {
|
||||
foo: 'string',
|
||||
baz: undefined,
|
||||
},
|
||||
},
|
||||
fetch,
|
||||
);
|
||||
expect(Array.from(form2.entries())).toEqual([['bar[foo]', 'string']]);
|
||||
});
|
||||
|
||||
test('nested undefined array item is stripped', async () => {
|
||||
const form = await createForm(
|
||||
{
|
||||
bar: [undefined, undefined],
|
||||
},
|
||||
fetch,
|
||||
);
|
||||
expect(Array.from(form.entries())).toEqual([]);
|
||||
|
||||
const form2 = await createForm(
|
||||
{
|
||||
bar: [undefined, 'foo'],
|
||||
},
|
||||
fetch,
|
||||
);
|
||||
expect(Array.from(form2.entries())).toEqual([['bar[]', 'foo']]);
|
||||
});
|
||||
});
|
||||
690
packages/sdk/tests/index.test.ts
Normal file
690
packages/sdk/tests/index.test.ts
Normal file
@@ -0,0 +1,690 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
128
packages/sdk/tests/internal/decoders/line.test.ts
Normal file
128
packages/sdk/tests/internal/decoders/line.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { findDoubleNewlineIndex, LineDecoder } from '@opencode-ai/sdk/internal/decoders/line';
|
||||
|
||||
function decodeChunks(chunks: string[], { flush }: { flush: boolean } = { flush: false }): string[] {
|
||||
const decoder = new LineDecoder();
|
||||
const lines: string[] = [];
|
||||
for (const chunk of chunks) {
|
||||
lines.push(...decoder.decode(chunk));
|
||||
}
|
||||
|
||||
if (flush) {
|
||||
lines.push(...decoder.flush());
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
describe('line decoder', () => {
|
||||
test('basic', () => {
|
||||
// baz is not included because the line hasn't ended yet
|
||||
expect(decodeChunks(['foo', ' bar\nbaz'])).toEqual(['foo bar']);
|
||||
});
|
||||
|
||||
test('basic with \\r', () => {
|
||||
expect(decodeChunks(['foo', ' bar\r\nbaz'])).toEqual(['foo bar']);
|
||||
expect(decodeChunks(['foo', ' bar\r\nbaz'], { flush: true })).toEqual(['foo bar', 'baz']);
|
||||
});
|
||||
|
||||
test('trailing new lines', () => {
|
||||
expect(decodeChunks(['foo', ' bar', 'baz\n', 'thing\n'])).toEqual(['foo barbaz', 'thing']);
|
||||
});
|
||||
|
||||
test('trailing new lines with \\r', () => {
|
||||
expect(decodeChunks(['foo', ' bar', 'baz\r\n', 'thing\r\n'])).toEqual(['foo barbaz', 'thing']);
|
||||
});
|
||||
|
||||
test('escaped new lines', () => {
|
||||
expect(decodeChunks(['foo', ' bar\\nbaz\n'])).toEqual(['foo bar\\nbaz']);
|
||||
});
|
||||
|
||||
test('escaped new lines with \\r', () => {
|
||||
expect(decodeChunks(['foo', ' bar\\r\\nbaz\n'])).toEqual(['foo bar\\r\\nbaz']);
|
||||
});
|
||||
|
||||
test('\\r & \\n split across multiple chunks', () => {
|
||||
expect(decodeChunks(['foo\r', '\n', 'bar'], { flush: true })).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
test('single \\r', () => {
|
||||
expect(decodeChunks(['foo\r', 'bar'], { flush: true })).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
test('double \\r', () => {
|
||||
expect(decodeChunks(['foo\r', 'bar\r'], { flush: true })).toEqual(['foo', 'bar']);
|
||||
expect(decodeChunks(['foo\r', '\r', 'bar'], { flush: true })).toEqual(['foo', '', 'bar']);
|
||||
// implementation detail that we don't yield the single \r line until a new \r or \n is encountered
|
||||
expect(decodeChunks(['foo\r', '\r', 'bar'], { flush: false })).toEqual(['foo']);
|
||||
});
|
||||
|
||||
test('double \\r then \\r\\n', () => {
|
||||
expect(decodeChunks(['foo\r', '\r', '\r', '\n', 'bar', '\n'])).toEqual(['foo', '', '', 'bar']);
|
||||
expect(decodeChunks(['foo\n', '\n', '\n', 'bar', '\n'])).toEqual(['foo', '', '', 'bar']);
|
||||
});
|
||||
|
||||
test('double newline', () => {
|
||||
expect(decodeChunks(['foo\n\nbar'], { flush: true })).toEqual(['foo', '', 'bar']);
|
||||
expect(decodeChunks(['foo', '\n', '\nbar'], { flush: true })).toEqual(['foo', '', 'bar']);
|
||||
expect(decodeChunks(['foo\n', '\n', 'bar'], { flush: true })).toEqual(['foo', '', 'bar']);
|
||||
expect(decodeChunks(['foo', '\n', '\n', 'bar'], { flush: true })).toEqual(['foo', '', 'bar']);
|
||||
});
|
||||
|
||||
test('multi-byte characters across chunks', () => {
|
||||
const decoder = new LineDecoder();
|
||||
|
||||
// bytes taken from the string 'известни' and arbitrarily split
|
||||
// so that some multi-byte characters span multiple chunks
|
||||
expect(decoder.decode(new Uint8Array([0xd0]))).toHaveLength(0);
|
||||
expect(decoder.decode(new Uint8Array([0xb8, 0xd0, 0xb7, 0xd0]))).toHaveLength(0);
|
||||
expect(
|
||||
decoder.decode(new Uint8Array([0xb2, 0xd0, 0xb5, 0xd1, 0x81, 0xd1, 0x82, 0xd0, 0xbd, 0xd0, 0xb8])),
|
||||
).toHaveLength(0);
|
||||
|
||||
const decoded = decoder.decode(new Uint8Array([0xa]));
|
||||
expect(decoded).toEqual(['известни']);
|
||||
});
|
||||
|
||||
test('flushing trailing newlines', () => {
|
||||
expect(decodeChunks(['foo\n', '\nbar'], { flush: true })).toEqual(['foo', '', 'bar']);
|
||||
});
|
||||
|
||||
test('flushing empty buffer', () => {
|
||||
expect(decodeChunks([], { flush: true })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findDoubleNewlineIndex', () => {
|
||||
test('finds \\n\\n', () => {
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\n\nbar'))).toBe(5);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('\n\nbar'))).toBe(2);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\n\n'))).toBe(5);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('\n\n'))).toBe(2);
|
||||
});
|
||||
|
||||
test('finds \\r\\r', () => {
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\rbar'))).toBe(5);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('\r\rbar'))).toBe(2);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\r'))).toBe(5);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('\r\r'))).toBe(2);
|
||||
});
|
||||
|
||||
test('finds \\r\\n\\r\\n', () => {
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\n\r\nbar'))).toBe(7);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('\r\n\r\nbar'))).toBe(4);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\n\r\n'))).toBe(7);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('\r\n\r\n'))).toBe(4);
|
||||
});
|
||||
|
||||
test('returns -1 when no double newline found', () => {
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\nbar'))).toBe(-1);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\rbar'))).toBe(-1);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\nbar'))).toBe(-1);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode(''))).toBe(-1);
|
||||
});
|
||||
|
||||
test('handles incomplete patterns', () => {
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\n\r'))).toBe(-1);
|
||||
expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\n'))).toBe(-1);
|
||||
});
|
||||
});
|
||||
462
packages/sdk/tests/path.test.ts
Normal file
462
packages/sdk/tests/path.test.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
import { createPathTagFunction, encodeURIPath } from '@opencode-ai/sdk/internal/utils/path';
|
||||
import { inspect } from 'node:util';
|
||||
import { runInNewContext } from 'node:vm';
|
||||
|
||||
describe('path template tag function', () => {
|
||||
test('validates input', () => {
|
||||
const testParams = ['', '.', '..', 'x', '%2e', '%2E', '%2e%2e', '%2E%2e', '%2e%2E', '%2E%2E'];
|
||||
const testCases = [
|
||||
['/path_params/', '/a'],
|
||||
['/path_params/', '/'],
|
||||
['/path_params/', ''],
|
||||
['', '/a'],
|
||||
['', '/'],
|
||||
['', ''],
|
||||
['a'],
|
||||
[''],
|
||||
['/path_params/', ':initiate'],
|
||||
['/path_params/', '.json'],
|
||||
['/path_params/', '?beta=true'],
|
||||
['/path_params/', '.?beta=true'],
|
||||
['/path_params/', '/', '/download'],
|
||||
['/path_params/', '-', '/download'],
|
||||
['/path_params/', '', '/download'],
|
||||
['/path_params/', '.', '/download'],
|
||||
['/path_params/', '..', '/download'],
|
||||
['/plain/path'],
|
||||
];
|
||||
|
||||
function paramPermutations(len: number): string[][] {
|
||||
if (len === 0) return [];
|
||||
if (len === 1) return testParams.map((e) => [e]);
|
||||
const rest = paramPermutations(len - 1);
|
||||
return testParams.flatMap((e) => rest.map((r) => [e, ...r]));
|
||||
}
|
||||
|
||||
// We need to test how %2E is handled, so we use a custom encoder that does no escaping.
|
||||
const rawPath = createPathTagFunction((s) => s);
|
||||
|
||||
const emptyObject = {};
|
||||
const mathObject = Math;
|
||||
const numberObject = new Number();
|
||||
const stringObject = new String();
|
||||
const basicClass = new (class {})();
|
||||
const classWithToString = new (class {
|
||||
toString() {
|
||||
return 'ok';
|
||||
}
|
||||
})();
|
||||
|
||||
// Invalid values
|
||||
expect(() => rawPath`/a/${null}/b`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value of type Null is not a valid path parameter\n' +
|
||||
'/a/null/b\n' +
|
||||
' ^^^^',
|
||||
);
|
||||
expect(() => rawPath`/a/${undefined}/b`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value of type Undefined is not a valid path parameter\n' +
|
||||
'/a/undefined/b\n' +
|
||||
' ^^^^^^^^^',
|
||||
);
|
||||
expect(() => rawPath`/a/${emptyObject}/b`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value of type Object is not a valid path parameter\n' +
|
||||
'/a/[object Object]/b\n' +
|
||||
' ^^^^^^^^^^^^^^^',
|
||||
);
|
||||
expect(() => rawPath`?${mathObject}`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value of type Math is not a valid path parameter\n' +
|
||||
'?[object Math]\n' +
|
||||
' ^^^^^^^^^^^^^',
|
||||
);
|
||||
expect(() => rawPath`/${basicClass}`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value of type Object is not a valid path parameter\n' +
|
||||
'/[object Object]\n' +
|
||||
' ^^^^^^^^^^^^^^',
|
||||
);
|
||||
expect(() => rawPath`/../${''}`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value ".." can\'t be safely passed as a path parameter\n' +
|
||||
'/../\n' +
|
||||
' ^^',
|
||||
);
|
||||
expect(() => rawPath`/../${{}}`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value ".." can\'t be safely passed as a path parameter\n' +
|
||||
'Value of type Object is not a valid path parameter\n' +
|
||||
'/../[object Object]\n' +
|
||||
' ^^ ^^^^^^^^^^^^^^',
|
||||
);
|
||||
|
||||
// Valid values
|
||||
expect(rawPath`/${0}`).toBe('/0');
|
||||
expect(rawPath`/${''}`).toBe('/');
|
||||
expect(rawPath`/${numberObject}`).toBe('/0');
|
||||
expect(rawPath`${stringObject}/`).toBe('/');
|
||||
expect(rawPath`/${classWithToString}`).toBe('/ok');
|
||||
|
||||
// We need to check what happens with cross-realm values, which we might get from
|
||||
// Jest or other frames in a browser.
|
||||
|
||||
const newRealm = runInNewContext('globalThis');
|
||||
expect(newRealm.Object).not.toBe(Object);
|
||||
|
||||
const crossRealmObject = newRealm.Object();
|
||||
const crossRealmMathObject = newRealm.Math;
|
||||
const crossRealmNumber = new newRealm.Number();
|
||||
const crossRealmString = new newRealm.String();
|
||||
const crossRealmClass = new (class extends newRealm.Object {})();
|
||||
const crossRealmClassWithToString = new (class extends newRealm.Object {
|
||||
toString() {
|
||||
return 'ok';
|
||||
}
|
||||
})();
|
||||
|
||||
// Invalid cross-realm values
|
||||
expect(() => rawPath`/a/${crossRealmObject}/b`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value of type Object is not a valid path parameter\n' +
|
||||
'/a/[object Object]/b\n' +
|
||||
' ^^^^^^^^^^^^^^^',
|
||||
);
|
||||
expect(() => rawPath`?${crossRealmMathObject}`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value of type Math is not a valid path parameter\n' +
|
||||
'?[object Math]\n' +
|
||||
' ^^^^^^^^^^^^^',
|
||||
);
|
||||
expect(() => rawPath`/${crossRealmClass}`).toThrow(
|
||||
'Path parameters result in path with invalid segments:\n' +
|
||||
'Value of type Object is not a valid path parameter\n' +
|
||||
'/[object Object]\n' +
|
||||
' ^^^^^^^^^^^^^^^',
|
||||
);
|
||||
|
||||
// Valid cross-realm values
|
||||
expect(rawPath`/${crossRealmNumber}`).toBe('/0');
|
||||
expect(rawPath`${crossRealmString}/`).toBe('/');
|
||||
expect(rawPath`/${crossRealmClassWithToString}`).toBe('/ok');
|
||||
|
||||
const results: {
|
||||
[pathParts: string]: {
|
||||
[params: string]: { valid: boolean; result?: string; error?: string };
|
||||
};
|
||||
} = {};
|
||||
|
||||
for (const pathParts of testCases) {
|
||||
const pathResults: Record<string, { valid: boolean; result?: string; error?: string }> = {};
|
||||
results[JSON.stringify(pathParts)] = pathResults;
|
||||
for (const params of paramPermutations(pathParts.length - 1)) {
|
||||
const stringRaw = String.raw({ raw: pathParts }, ...params);
|
||||
const plainString = String.raw(
|
||||
{ raw: pathParts.map((e) => e.replace(/\./g, 'x')) },
|
||||
...params.map((e) => 'X'.repeat(e.length)),
|
||||
);
|
||||
const normalizedStringRaw = new URL(stringRaw, 'https://example.com').href;
|
||||
const normalizedPlainString = new URL(plainString, 'https://example.com').href;
|
||||
const pathResultsKey = JSON.stringify(params);
|
||||
try {
|
||||
const result = rawPath(pathParts, ...params);
|
||||
expect(result).toBe(stringRaw);
|
||||
// there are no special segments, so the length of the normalized path is
|
||||
// equal to the length of the normalized plain path.
|
||||
expect(normalizedStringRaw.length).toBe(normalizedPlainString.length);
|
||||
pathResults[pathResultsKey] = {
|
||||
valid: true,
|
||||
result,
|
||||
};
|
||||
} catch (e) {
|
||||
const error = String(e);
|
||||
expect(error).toMatch(/Path parameters result in path with invalid segment/);
|
||||
// there are special segments, so the length of the normalized path is
|
||||
// different than the length of the normalized plain path.
|
||||
expect(normalizedStringRaw.length).not.toBe(normalizedPlainString.length);
|
||||
pathResults[pathResultsKey] = {
|
||||
valid: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(results).toMatchObject({
|
||||
'["/path_params/","/a"]': {
|
||||
'["x"]': { valid: true, result: '/path_params/x/a' },
|
||||
'[""]': { valid: true, result: '/path_params//a' },
|
||||
'["%2E%2e"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2E%2e/a\n' +
|
||||
' ^^^^^^',
|
||||
},
|
||||
'["%2E"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2E/a\n' +
|
||||
' ^^^',
|
||||
},
|
||||
},
|
||||
'["/path_params/","/"]': {
|
||||
'["x"]': { valid: true, result: '/path_params/x/' },
|
||||
'[""]': { valid: true, result: '/path_params//' },
|
||||
'["%2e%2E"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2e%2E/\n' +
|
||||
' ^^^^^^',
|
||||
},
|
||||
'["%2e"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2e" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2e/\n' +
|
||||
' ^^^',
|
||||
},
|
||||
},
|
||||
'["/path_params/",""]': {
|
||||
'[""]': { valid: true, result: '/path_params/' },
|
||||
'["x"]': { valid: true, result: '/path_params/x' },
|
||||
'["%2E"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2E\n' +
|
||||
' ^^^',
|
||||
},
|
||||
'["%2E%2e"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2E%2e\n' +
|
||||
' ^^^^^^',
|
||||
},
|
||||
},
|
||||
'["","/a"]': {
|
||||
'[""]': { valid: true, result: '/a' },
|
||||
'["x"]': { valid: true, result: 'x/a' },
|
||||
'["%2E"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E" can\'t be safely passed as a path parameter\n%2E/a\n^^^',
|
||||
},
|
||||
'["%2e%2E"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
|
||||
'%2e%2E/a\n' +
|
||||
'^^^^^^',
|
||||
},
|
||||
},
|
||||
'["","/"]': {
|
||||
'["x"]': { valid: true, result: 'x/' },
|
||||
'[""]': { valid: true, result: '/' },
|
||||
'["%2E%2e"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
|
||||
'%2E%2e/\n' +
|
||||
'^^^^^^',
|
||||
},
|
||||
'["."]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "." can\'t be safely passed as a path parameter\n' +
|
||||
'./\n^',
|
||||
},
|
||||
},
|
||||
'["",""]': {
|
||||
'[""]': { valid: true, result: '' },
|
||||
'["x"]': { valid: true, result: 'x' },
|
||||
'[".."]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value ".." can\'t be safely passed as a path parameter\n' +
|
||||
'..\n^^',
|
||||
},
|
||||
'["."]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "." can\'t be safely passed as a path parameter\n' +
|
||||
'.\n^',
|
||||
},
|
||||
},
|
||||
'["a"]': {},
|
||||
'[""]': {},
|
||||
'["/path_params/",":initiate"]': {
|
||||
'[""]': { valid: true, result: '/path_params/:initiate' },
|
||||
'["."]': { valid: true, result: '/path_params/.:initiate' },
|
||||
},
|
||||
'["/path_params/",".json"]': {
|
||||
'["x"]': { valid: true, result: '/path_params/x.json' },
|
||||
'["."]': { valid: true, result: '/path_params/..json' },
|
||||
},
|
||||
'["/path_params/","?beta=true"]': {
|
||||
'["x"]': { valid: true, result: '/path_params/x?beta=true' },
|
||||
'[""]': { valid: true, result: '/path_params/?beta=true' },
|
||||
'["%2E%2E"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E%2E" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2E%2E?beta=true\n' +
|
||||
' ^^^^^^',
|
||||
},
|
||||
'["%2e%2E"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2e%2E?beta=true\n' +
|
||||
' ^^^^^^',
|
||||
},
|
||||
},
|
||||
'["/path_params/",".?beta=true"]': {
|
||||
'[".."]': { valid: true, result: '/path_params/...?beta=true' },
|
||||
'["x"]': { valid: true, result: '/path_params/x.?beta=true' },
|
||||
'[""]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "." can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/.?beta=true\n' +
|
||||
' ^',
|
||||
},
|
||||
'["%2e"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2e." can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2e.?beta=true\n' +
|
||||
' ^^^^',
|
||||
},
|
||||
},
|
||||
'["/path_params/","/","/download"]': {
|
||||
'["",""]': { valid: true, result: '/path_params///download' },
|
||||
'["","x"]': { valid: true, result: '/path_params//x/download' },
|
||||
'[".","%2e"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "." can\'t be safely passed as a path parameter\n' +
|
||||
'Value "%2e" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/./%2e/download\n' +
|
||||
' ^ ^^^',
|
||||
},
|
||||
'["%2E%2e","%2e"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
|
||||
'Value "%2e" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2E%2e/%2e/download\n' +
|
||||
' ^^^^^^ ^^^',
|
||||
},
|
||||
},
|
||||
'["/path_params/","-","/download"]': {
|
||||
'["","%2e"]': { valid: true, result: '/path_params/-%2e/download' },
|
||||
'["%2E",".."]': { valid: true, result: '/path_params/%2E-../download' },
|
||||
},
|
||||
'["/path_params/","","/download"]': {
|
||||
'["%2E%2e","%2e%2E"]': { valid: true, result: '/path_params/%2E%2e%2e%2E/download' },
|
||||
'["%2E",".."]': { valid: true, result: '/path_params/%2E../download' },
|
||||
'["","%2E"]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E" can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2E/download\n' +
|
||||
' ^^^',
|
||||
},
|
||||
'["%2E","."]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "%2E." can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/%2E./download\n' +
|
||||
' ^^^^',
|
||||
},
|
||||
},
|
||||
'["/path_params/",".","/download"]': {
|
||||
'["%2e%2e",""]': { valid: true, result: '/path_params/%2e%2e./download' },
|
||||
'["","%2e%2e"]': { valid: true, result: '/path_params/.%2e%2e/download' },
|
||||
'["",""]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value "." can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/./download\n' +
|
||||
' ^',
|
||||
},
|
||||
'["","."]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value ".." can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/../download\n' +
|
||||
' ^^',
|
||||
},
|
||||
},
|
||||
'["/path_params/","..","/download"]': {
|
||||
'["","%2E"]': { valid: true, result: '/path_params/..%2E/download' },
|
||||
'["","x"]': { valid: true, result: '/path_params/..x/download' },
|
||||
'["",""]': {
|
||||
valid: false,
|
||||
error:
|
||||
'Error: Path parameters result in path with invalid segments:\n' +
|
||||
'Value ".." can\'t be safely passed as a path parameter\n' +
|
||||
'/path_params/../download\n' +
|
||||
' ^^',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeURIPath', () => {
|
||||
const testCases: string[] = [
|
||||
'',
|
||||
// Every ASCII character
|
||||
...Array.from({ length: 0x7f }, (_, i) => String.fromCharCode(i)),
|
||||
// Unicode BMP codepoint
|
||||
'å',
|
||||
// Unicode supplementary codepoint
|
||||
'😃',
|
||||
];
|
||||
|
||||
for (const param of testCases) {
|
||||
test('properly encodes ' + inspect(param), () => {
|
||||
const encoded = encodeURIPath(param);
|
||||
const naiveEncoded = encodeURIComponent(param);
|
||||
// we should never encode more characters than encodeURIComponent
|
||||
expect(naiveEncoded.length).toBeGreaterThanOrEqual(encoded.length);
|
||||
expect(decodeURIComponent(encoded)).toBe(param);
|
||||
});
|
||||
}
|
||||
|
||||
test("leaves ':' intact", () => {
|
||||
expect(encodeURIPath(':')).toBe(':');
|
||||
});
|
||||
|
||||
test("leaves '@' intact", () => {
|
||||
expect(encodeURIPath('@')).toBe('@');
|
||||
});
|
||||
});
|
||||
219
packages/sdk/tests/streaming.test.ts
Normal file
219
packages/sdk/tests/streaming.test.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import assert from 'assert';
|
||||
import { _iterSSEMessages } from '@opencode-ai/sdk/core/streaming';
|
||||
import { ReadableStreamFrom } from '@opencode-ai/sdk/internal/shims';
|
||||
|
||||
describe('streaming decoding', () => {
|
||||
test('basic', async () => {
|
||||
async function* body(): AsyncGenerator<Buffer> {
|
||||
yield Buffer.from('event: completion\n');
|
||||
yield Buffer.from('data: {"foo":true}\n');
|
||||
yield Buffer.from('\n');
|
||||
}
|
||||
|
||||
const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[
|
||||
Symbol.asyncIterator
|
||||
]();
|
||||
|
||||
let event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(JSON.parse(event.value.data)).toEqual({ foo: true });
|
||||
|
||||
event = await stream.next();
|
||||
expect(event.done).toBeTruthy();
|
||||
});
|
||||
|
||||
test('data without event', async () => {
|
||||
async function* body(): AsyncGenerator<Buffer> {
|
||||
yield Buffer.from('data: {"foo":true}\n');
|
||||
yield Buffer.from('\n');
|
||||
}
|
||||
|
||||
const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[
|
||||
Symbol.asyncIterator
|
||||
]();
|
||||
|
||||
let event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(event.value.event).toBeNull();
|
||||
expect(JSON.parse(event.value.data)).toEqual({ foo: true });
|
||||
|
||||
event = await stream.next();
|
||||
expect(event.done).toBeTruthy();
|
||||
});
|
||||
|
||||
test('event without data', async () => {
|
||||
async function* body(): AsyncGenerator<Buffer> {
|
||||
yield Buffer.from('event: foo\n');
|
||||
yield Buffer.from('\n');
|
||||
}
|
||||
|
||||
const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[
|
||||
Symbol.asyncIterator
|
||||
]();
|
||||
|
||||
let event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(event.value.event).toEqual('foo');
|
||||
expect(event.value.data).toEqual('');
|
||||
|
||||
event = await stream.next();
|
||||
expect(event.done).toBeTruthy();
|
||||
});
|
||||
|
||||
test('multiple events', async () => {
|
||||
async function* body(): AsyncGenerator<Buffer> {
|
||||
yield Buffer.from('event: foo\n');
|
||||
yield Buffer.from('\n');
|
||||
yield Buffer.from('event: ping\n');
|
||||
yield Buffer.from('\n');
|
||||
}
|
||||
|
||||
const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[
|
||||
Symbol.asyncIterator
|
||||
]();
|
||||
|
||||
let event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(event.value.event).toEqual('foo');
|
||||
expect(event.value.data).toEqual('');
|
||||
|
||||
event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(event.value.event).toEqual('ping');
|
||||
expect(event.value.data).toEqual('');
|
||||
|
||||
event = await stream.next();
|
||||
expect(event.done).toBeTruthy();
|
||||
});
|
||||
|
||||
test('multiple events with data', async () => {
|
||||
async function* body(): AsyncGenerator<Buffer> {
|
||||
yield Buffer.from('event: foo\n');
|
||||
yield Buffer.from('data: {"foo":true}\n');
|
||||
yield Buffer.from('\n');
|
||||
yield Buffer.from('event: ping\n');
|
||||
yield Buffer.from('data: {"bar":false}\n');
|
||||
yield Buffer.from('\n');
|
||||
}
|
||||
|
||||
const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[
|
||||
Symbol.asyncIterator
|
||||
]();
|
||||
|
||||
let event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(event.value.event).toEqual('foo');
|
||||
expect(JSON.parse(event.value.data)).toEqual({ foo: true });
|
||||
|
||||
event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(event.value.event).toEqual('ping');
|
||||
expect(JSON.parse(event.value.data)).toEqual({ bar: false });
|
||||
|
||||
event = await stream.next();
|
||||
expect(event.done).toBeTruthy();
|
||||
});
|
||||
|
||||
test('multiple data lines with empty line', async () => {
|
||||
async function* body(): AsyncGenerator<Buffer> {
|
||||
yield Buffer.from('event: ping\n');
|
||||
yield Buffer.from('data: {\n');
|
||||
yield Buffer.from('data: "foo":\n');
|
||||
yield Buffer.from('data: \n');
|
||||
yield Buffer.from('data:\n');
|
||||
yield Buffer.from('data: true}\n');
|
||||
yield Buffer.from('\n\n');
|
||||
}
|
||||
|
||||
const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[
|
||||
Symbol.asyncIterator
|
||||
]();
|
||||
|
||||
let event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(event.value.event).toEqual('ping');
|
||||
expect(JSON.parse(event.value.data)).toEqual({ foo: true });
|
||||
expect(event.value.data).toEqual('{\n"foo":\n\n\ntrue}');
|
||||
|
||||
event = await stream.next();
|
||||
expect(event.done).toBeTruthy();
|
||||
});
|
||||
|
||||
test('data json escaped double new line', async () => {
|
||||
async function* body(): AsyncGenerator<Buffer> {
|
||||
yield Buffer.from('event: ping\n');
|
||||
yield Buffer.from('data: {"foo": "my long\\n\\ncontent"}');
|
||||
yield Buffer.from('\n\n');
|
||||
}
|
||||
|
||||
const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[
|
||||
Symbol.asyncIterator
|
||||
]();
|
||||
|
||||
let event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(event.value.event).toEqual('ping');
|
||||
expect(JSON.parse(event.value.data)).toEqual({ foo: 'my long\n\ncontent' });
|
||||
|
||||
event = await stream.next();
|
||||
expect(event.done).toBeTruthy();
|
||||
});
|
||||
|
||||
test('special new line characters', async () => {
|
||||
async function* body(): AsyncGenerator<Buffer> {
|
||||
yield Buffer.from('data: {"content": "culpa "}\n');
|
||||
yield Buffer.from('\n');
|
||||
yield Buffer.from('data: {"content": "');
|
||||
yield Buffer.from([0xe2, 0x80, 0xa8]);
|
||||
yield Buffer.from('"}\n');
|
||||
yield Buffer.from('\n');
|
||||
yield Buffer.from('data: {"content": "foo"}\n');
|
||||
yield Buffer.from('\n');
|
||||
}
|
||||
|
||||
const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[
|
||||
Symbol.asyncIterator
|
||||
]();
|
||||
|
||||
let event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(JSON.parse(event.value.data)).toEqual({ content: 'culpa ' });
|
||||
|
||||
event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(JSON.parse(event.value.data)).toEqual({ content: Buffer.from([0xe2, 0x80, 0xa8]).toString() });
|
||||
|
||||
event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(JSON.parse(event.value.data)).toEqual({ content: 'foo' });
|
||||
|
||||
event = await stream.next();
|
||||
expect(event.done).toBeTruthy();
|
||||
});
|
||||
|
||||
test('multi-byte characters across chunks', async () => {
|
||||
async function* body(): AsyncGenerator<Buffer> {
|
||||
yield Buffer.from('event: completion\n');
|
||||
yield Buffer.from('data: {"content": "');
|
||||
// bytes taken from the string 'известни' and arbitrarily split
|
||||
// so that some multi-byte characters span multiple chunks
|
||||
yield Buffer.from([0xd0]);
|
||||
yield Buffer.from([0xb8, 0xd0, 0xb7, 0xd0]);
|
||||
yield Buffer.from([0xb2, 0xd0, 0xb5, 0xd1, 0x81, 0xd1, 0x82, 0xd0, 0xbd, 0xd0, 0xb8]);
|
||||
yield Buffer.from('"}\n');
|
||||
yield Buffer.from('\n');
|
||||
}
|
||||
|
||||
const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[
|
||||
Symbol.asyncIterator
|
||||
]();
|
||||
|
||||
let event = await stream.next();
|
||||
assert(event.value);
|
||||
expect(event.value.event).toEqual('completion');
|
||||
expect(JSON.parse(event.value.data)).toEqual({ content: 'известни' });
|
||||
|
||||
event = await stream.next();
|
||||
expect(event.done).toBeTruthy();
|
||||
});
|
||||
});
|
||||
29
packages/sdk/tests/stringifyQuery.test.ts
Normal file
29
packages/sdk/tests/stringifyQuery.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import { Opencode } from '@opencode-ai/sdk';
|
||||
|
||||
const { stringifyQuery } = Opencode.prototype as any;
|
||||
|
||||
describe(stringifyQuery, () => {
|
||||
for (const [input, expected] of [
|
||||
[{ a: '1', b: 2, c: true }, 'a=1&b=2&c=true'],
|
||||
[{ a: null, b: false, c: undefined }, 'a=&b=false'],
|
||||
[{ 'a/b': 1.28341 }, `${encodeURIComponent('a/b')}=1.28341`],
|
||||
[
|
||||
{ 'a/b': 'c/d', 'e=f': 'g&h' },
|
||||
`${encodeURIComponent('a/b')}=${encodeURIComponent('c/d')}&${encodeURIComponent(
|
||||
'e=f',
|
||||
)}=${encodeURIComponent('g&h')}`,
|
||||
],
|
||||
]) {
|
||||
it(`${JSON.stringify(input)} -> ${expected}`, () => {
|
||||
expect(stringifyQuery(input)).toEqual(expected);
|
||||
});
|
||||
}
|
||||
|
||||
for (const value of [[], {}, new Date()]) {
|
||||
it(`${JSON.stringify(value)} -> <error>`, () => {
|
||||
expect(() => stringifyQuery({ value })).toThrow(`Cannot stringify type ${typeof value}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
107
packages/sdk/tests/uploads.test.ts
Normal file
107
packages/sdk/tests/uploads.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import fs from 'fs';
|
||||
import type { ResponseLike } from '@opencode-ai/sdk/internal/to-file';
|
||||
import { toFile } from '@opencode-ai/sdk/core/uploads';
|
||||
import { File } from 'node:buffer';
|
||||
|
||||
class MyClass {
|
||||
name: string = 'foo';
|
||||
}
|
||||
|
||||
function mockResponse({ url, content }: { url: string; content?: Blob }): ResponseLike {
|
||||
return {
|
||||
url,
|
||||
blob: async () => content || new Blob([]),
|
||||
};
|
||||
}
|
||||
|
||||
describe('toFile', () => {
|
||||
it('throws a helpful error for mismatched types', async () => {
|
||||
await expect(
|
||||
// @ts-expect-error intentionally mismatched type
|
||||
toFile({ foo: 'string' }),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unexpected data type: object; constructor: Object; props: ["foo"]"`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error intentionally mismatched type
|
||||
toFile(new MyClass()),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unexpected data type: object; constructor: MyClass; props: ["name"]"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('disallows string at the type-level', async () => {
|
||||
// @ts-expect-error we intentionally do not type support for `string`
|
||||
// to help people avoid passing a file path
|
||||
const file = await toFile('contents');
|
||||
expect(file.text()).resolves.toEqual('contents');
|
||||
});
|
||||
|
||||
it('extracts a file name from a Response', async () => {
|
||||
const response = mockResponse({ url: 'https://example.com/my/audio.mp3' });
|
||||
const file = await toFile(response);
|
||||
expect(file.name).toEqual('audio.mp3');
|
||||
});
|
||||
|
||||
it('extracts a file name from a File', async () => {
|
||||
const input = new File(['foo'], 'input.jsonl');
|
||||
const file = await toFile(input);
|
||||
expect(file.name).toEqual('input.jsonl');
|
||||
});
|
||||
|
||||
it('extracts a file name from a ReadStream', async () => {
|
||||
const input = fs.createReadStream('tests/uploads.test.ts');
|
||||
const file = await toFile(input);
|
||||
expect(file.name).toEqual('uploads.test.ts');
|
||||
});
|
||||
|
||||
it('does not copy File objects', async () => {
|
||||
const input = new File(['foo'], 'input.jsonl', { type: 'jsonl' });
|
||||
const file = await toFile(input);
|
||||
expect(file).toBe(input);
|
||||
expect(file.name).toEqual('input.jsonl');
|
||||
expect(file.type).toBe('jsonl');
|
||||
});
|
||||
|
||||
it('is assignable to File and Blob', async () => {
|
||||
const input = new File(['foo'], 'input.jsonl', { type: 'jsonl' });
|
||||
const result = await toFile(input);
|
||||
const file: File = result;
|
||||
const blob: Blob = result;
|
||||
void file, blob;
|
||||
});
|
||||
});
|
||||
|
||||
describe('missing File error message', () => {
|
||||
let prevGlobalFile: unknown;
|
||||
let prevNodeFile: unknown;
|
||||
beforeEach(() => {
|
||||
// The file shim captures the global File object when it's first imported.
|
||||
// Reset modules before each test so we can test the error thrown when it's undefined.
|
||||
jest.resetModules();
|
||||
const buffer = require('node:buffer');
|
||||
// @ts-ignore
|
||||
prevGlobalFile = globalThis.File;
|
||||
prevNodeFile = buffer.File;
|
||||
// @ts-ignore
|
||||
globalThis.File = undefined;
|
||||
buffer.File = undefined;
|
||||
});
|
||||
afterEach(() => {
|
||||
// Clean up
|
||||
// @ts-ignore
|
||||
globalThis.File = prevGlobalFile;
|
||||
require('node:buffer').File = prevNodeFile;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
test('is thrown', async () => {
|
||||
const uploads = await import('@opencode-ai/sdk/core/uploads');
|
||||
await expect(
|
||||
uploads.toFile(mockResponse({ url: 'https://example.com/my/audio.mp3' })),
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
`[Error: \`File\` is not defined as a global, which is required for file uploads.]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user