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
71 changes: 71 additions & 0 deletions src/tool-optional-ai.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { StackOneError } from './utils/error-stackone';
import { peerDependencies } from '../package.json';
import { ParameterLocation, type ExecuteConfig, type ToolParameters } from './types';

const builtToolEsm = new URL('../dist/src/tool.mjs', import.meta.url);
const builtToolCjs = new URL('../dist/src/tool.cjs', import.meta.url);
const distReady = existsSync(builtToolEsm) && existsSync(builtToolCjs);

const createExecuteConfig = (): ExecuteConfig => ({
kind: 'http',
method: 'GET',
url: 'https://api.example.com/test/{id}',
bodyType: 'json',
params: [
{
name: 'id',
location: ParameterLocation.PATH,
type: 'string',
},
],
});

const createParameters = (): ToolParameters => ({
type: 'object',
properties: { id: { type: 'string', description: 'ID parameter' } },
});

describe('BaseTool optional ai peer handling', () => {
afterEach(() => {
vi.resetModules();
vi.restoreAllMocks();
});

it('rejects toClaudeAgentSdkTool with StackOneError and install hint when ai is unavailable', async () => {
vi.doMock('./utils/try-import', () => ({
tryImport: vi.fn(async (moduleName: string, installHint: string) => {
if (moduleName === 'ai') {
throw new StackOneError(
`${moduleName} is not installed. Please install it with: ${installHint}`,
);
}

throw new Error(`Unexpected module request: ${moduleName}`);
}),
}));

const { BaseTool } = await import('./tool');
const tool = new BaseTool('test_tool', 'Test tool', createParameters(), createExecuteConfig());

await expect(tool.toClaudeAgentSdkTool()).rejects.toBeInstanceOf(StackOneError);
await expect(tool.toClaudeAgentSdkTool()).rejects.toThrow(
`ai is not installed. Please install it with: npm install ai (requires ${peerDependencies.ai})`,
);
});

it.skipIf(!distReady)('keeps the built ESM module free of a top-level ai import', async () => {
const builtToolModule = await readFile(builtToolEsm, 'utf8');

expect(builtToolModule).toContain('tryImport("ai"');
expect(builtToolModule).not.toMatch(/^\s*import\s+.*['"]ai['"]/m);
});

it.skipIf(!distReady)('keeps the built CJS module free of a top-level ai require', async () => {
const builtToolModule = await readFile(builtToolCjs, 'utf8');

expect(builtToolModule).toContain('tryImport("ai"');
expect(builtToolModule).not.toMatch(/require\(["']ai["']\)/);
});
});
8 changes: 6 additions & 2 deletions src/tool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type JSONSchema7 as AISDKJSONSchema, jsonSchema } from 'ai';
import type { JSONSchema7 as AISDKJSONSchema } from 'ai';
import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources';
import type { McpSdkServerConfigWithInstance } from '@anthropic-ai/claude-agent-sdk';
import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions';
Expand Down Expand Up @@ -248,7 +248,11 @@ export class BaseTool {
args: Record<string, unknown>,
) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;
}> {
const inputSchema = jsonSchema(this.toJsonSchema());
const ai = await tryImport<typeof import('ai')>(
'ai',
`npm install ai (requires ${peerDependencies.ai})`,
Comment thread
shashi-stackone marked this conversation as resolved.
);
Comment thread
shashi-stackone marked this conversation as resolved.
const inputSchema = ai.jsonSchema(this.toJsonSchema());
Comment thread
shashi-stackone marked this conversation as resolved.
const execute = this.execute.bind(this);

return {
Expand Down
Loading