Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export class LilySdk {
public get http(): HttpClient {
return this.httpClient;
}
/**
* The HttpClient only when it was explicitly injected by the caller.
* Derived instances from `withConfig` reuse an injected client (so custom
* transport behavior is preserved), but never the default fetch client:
* that one is rebuilt from the merged config so `baseUrl`/credential
* overrides actually take effect on the transport.
*/
private readonly injectedHttpClient: HttpClient | undefined;
public readonly agents: AgentClient;
public readonly wallets: WalletClient;
public readonly payments: PaymentClient;
Expand All @@ -28,6 +36,7 @@ export class LilySdk {
public constructor(config?: Partial<LilySdkConfig>, httpClient?: HttpClient) {
this.config = resolveLilySdkConfig(config ?? {});
this.httpClient = httpClient ?? createFetchHttpClient(this.config);
this.injectedHttpClient = httpClient;

this.agents = new AgentClient(this.httpClient);
this.wallets = new WalletClient(this.httpClient);
Expand Down Expand Up @@ -126,6 +135,11 @@ export class LilySdk {
: {}),
};

return new LilySdk(merged);
// Reuse the transport only when the caller injected a custom HttpClient.
// The default fetch client is rebuilt from the merged config so that
// baseUrl/credential overrides are captured in the transport closure.
return this.injectedHttpClient !== undefined
? new LilySdk(merged, this.injectedHttpClient)
: new LilySdk(merged);
}
}
61 changes: 60 additions & 1 deletion tests/sdk-withConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { LilySdk } from '../src/sdk';
import type { HttpClient } from '../src/http/types';
import type { HttpClient, HttpRequest } from '../src/http/types';

describe('LilySdk.withConfig', () => {
const baseConfig = {
Expand Down Expand Up @@ -44,6 +44,65 @@ describe('LilySdk.withConfig', () => {
expect(tenantSdk.config.apiKey).toBe('tenant-key');
});

it('preserves an injected custom HttpClient across withConfig (issue #442)', async () => {
const calls: string[] = [];
const mockHttpClient: HttpClient = {
request: (request: HttpRequest) => {
calls.push(request.path);
return Promise.resolve({
status: 200,
data: { tenant: 'injected-client' },
headers: {},
});
},
} as unknown as HttpClient;

const sdk = new LilySdk(baseConfig, mockHttpClient);
const tenantSdk = sdk.withConfig({ apiKey: 'tenant2' });

// The child instance must route through the parent's injected client.
expect(tenantSdk.httpClient).toBe(mockHttpClient);

const result = await tenantSdk.request<{ tenant: string }>({
method: 'GET',
path: '/v1/agents',
});
expect(calls).toEqual(['/v1/agents']);
expect(result.tenant).toBe('injected-client');
});

it('rebuilds the default fetch client when none was injected (issue #405 semantics)', async () => {
const sdk = new LilySdk(baseConfig);
const derived = sdk.withConfig({ baseUrl: 'https://tenant.example.com' });

// The derived instance must NOT share the source transport, otherwise
// the baseUrl override would never reach the request closure.
expect(derived.httpClient).not.toBe(sdk.httpClient);

const fetchCalls: URL[] = [];
const trackingFetch: typeof fetch = ((input: RequestInfo | URL) => {
fetchCalls.push(new URL(String(input)));
return Promise.resolve(
new Response(JSON.stringify({}), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
}) as unknown as typeof fetch;

const routed = new LilySdk({
baseUrl: 'https://api.example.com',
fetch: trackingFetch,
});
const tenant = routed.withConfig({
baseUrl: 'https://tenant.example.com',
apiKey: 'tenant-key',
});
await tenant.request({ method: 'GET', path: '/v1/ping' });
expect(fetchCalls).toHaveLength(1);
expect(fetchCalls[0].origin).toBe('https://tenant.example.com');
});

it('does not mutate the original SDK instance', () => {
const sdk = new LilySdk(baseConfig);
const originalApiKey = sdk.config.apiKey;
Expand Down