Skip to content

Commit 60b81d8

Browse files
committed
test: improve test coverage from 85% to 93%
- Add comprehensive tests for StackOneError and StackOneAPIError - Add tests for BaseTool RPC/local config and error handling - Add tests for StackOneToolSet dryRun mode and parameter extraction - Exclude type-only files from coverage (index.ts, type.ts) Coverage improvements: - Statements: 85.29% → 92.50% - Branch: 70.61% → 84.05% - Lines: 85.95% → 93.51%
1 parent 3dc112c commit 60b81d8

File tree

9 files changed

+542
-6
lines changed

9 files changed

+542
-6
lines changed

src/feedback.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { http, HttpResponse } from 'msw';
22
import { server } from '../mocks/node';
3-
import { StackOneError } from './utils/errors';
3+
import { StackOneError } from './utils/error-stackone';
44
import { createFeedbackTool } from './feedback';
55

66
interface FeedbackResultItem {

src/requestBuilder.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { http, HttpResponse } from 'msw';
22
import { server } from '../mocks/node';
33
import { type HttpExecuteConfig, type JsonObject, ParameterLocation } from './types';
4-
import { StackOneAPIError } from './utils/errors';
4+
import { StackOneAPIError } from './utils/error-stackone-api';
55
import { RequestBuilder } from './requestBuilder';
66

77
describe('RequestBuilder', () => {

src/rpc-client.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { RpcClient } from './rpc-client';
22
import { stackOneHeadersSchema } from './headers';
3-
import { StackOneAPIError } from './utils/errors';
3+
import { StackOneAPIError } from './utils/error-stackone-api';
44

55
test('should successfully execute an RPC action', async () => {
66
const client = new RpcClient({

src/tool.test.ts

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
ParameterLocation,
3434
type ToolParameters,
3535
} from './types';
36-
import { StackOneAPIError } from './utils/errors';
36+
import { StackOneAPIError } from './utils/error-stackone-api';
3737

3838
// Create a mock tool for testing
3939
const createMockTool = (headers?: Record<string, string>): BaseTool => {
@@ -328,6 +328,157 @@ describe('StackOneTool', () => {
328328
});
329329
});
330330

331+
describe('BaseTool - additional coverage', () => {
332+
it('should throw error when execute is called on non-HTTP tool', async () => {
333+
const rpcTool = new BaseTool(
334+
'rpc_tool',
335+
'RPC tool',
336+
{ type: 'object', properties: {} },
337+
{
338+
kind: 'rpc',
339+
method: 'test_method',
340+
url: 'https://api.example.com/rpc',
341+
payloadKeys: { action: 'action', body: 'body' },
342+
},
343+
);
344+
345+
await expect(rpcTool.execute({})).rejects.toThrow(
346+
'BaseTool.execute is only available for HTTP-backed tools',
347+
);
348+
});
349+
350+
it('should throw error for invalid parameter type', async () => {
351+
const tool = createMockTool();
352+
353+
// @ts-expect-error - intentionally passing invalid type
354+
await expect(tool.execute(12345)).rejects.toThrow('Invalid parameters type');
355+
});
356+
357+
it('should create execution metadata for RPC config in toAISDK', async () => {
358+
const rpcTool = new BaseTool(
359+
'rpc_tool',
360+
'RPC tool',
361+
{ type: 'object', properties: {} },
362+
{
363+
kind: 'rpc',
364+
method: 'test_method',
365+
url: 'https://api.example.com/rpc',
366+
payloadKeys: { action: 'action', body: 'body', headers: 'headers' },
367+
},
368+
);
369+
370+
const aiSdkTool = await rpcTool.toAISDK({ executable: false });
371+
const execution = aiSdkTool.rpc_tool.execution;
372+
373+
expect(execution).toBeDefined();
374+
expect(execution?.config.kind).toBe('rpc');
375+
if (execution?.config.kind === 'rpc') {
376+
expect(execution.config.method).toBe('test_method');
377+
expect(execution.config.url).toBe('https://api.example.com/rpc');
378+
expect(execution.config.payloadKeys).toEqual({
379+
action: 'action',
380+
body: 'body',
381+
headers: 'headers',
382+
});
383+
}
384+
});
385+
386+
it('should create execution metadata for local config in toAISDK', async () => {
387+
const localTool = new BaseTool(
388+
'local_tool',
389+
'Local tool',
390+
{ type: 'object', properties: {} },
391+
{
392+
kind: 'local',
393+
identifier: 'local_test',
394+
description: 'local://test',
395+
},
396+
);
397+
398+
const aiSdkTool = await localTool.toAISDK({ executable: false });
399+
const execution = aiSdkTool.local_tool.execution;
400+
401+
expect(execution).toBeDefined();
402+
expect(execution?.config.kind).toBe('local');
403+
if (execution?.config.kind === 'local') {
404+
expect(execution.config.identifier).toBe('local_test');
405+
expect(execution.config.description).toBe('local://test');
406+
}
407+
});
408+
409+
it('should allow providing custom execution metadata in toAISDK', async () => {
410+
const tool = createMockTool();
411+
const customExecution = {
412+
config: {
413+
kind: 'http' as const,
414+
method: 'POST' as const,
415+
url: 'https://custom.example.com',
416+
bodyType: 'json' as const,
417+
params: [],
418+
},
419+
headers: { 'X-Custom': 'value' },
420+
};
421+
422+
const aiSdkTool = await tool.toAISDK({ execution: customExecution });
423+
const execution = aiSdkTool.test_tool.execution;
424+
425+
expect(execution).toBeDefined();
426+
expect(execution?.config.kind).toBe('http');
427+
if (execution?.config.kind === 'http') {
428+
expect(execution.config.url).toBe('https://custom.example.com');
429+
}
430+
expect(execution?.headers).toEqual({ 'X-Custom': 'value' });
431+
});
432+
433+
it('should return undefined execution when execution option is false', async () => {
434+
const tool = createMockTool();
435+
436+
const aiSdkTool = await tool.toAISDK({ execution: false });
437+
expect(aiSdkTool.test_tool.execution).toBeUndefined();
438+
});
439+
440+
it('should return undefined execute when executable option is false', async () => {
441+
const tool = createMockTool();
442+
443+
const aiSdkTool = await tool.toAISDK({ executable: false });
444+
expect(aiSdkTool.test_tool.execute).toBeUndefined();
445+
});
446+
447+
it('should get headers from tool without requestBuilder', () => {
448+
const rpcTool = new BaseTool(
449+
'rpc_tool',
450+
'RPC tool',
451+
{ type: 'object', properties: {} },
452+
{
453+
kind: 'rpc',
454+
method: 'test_method',
455+
url: 'https://api.example.com/rpc',
456+
payloadKeys: { action: 'action', body: 'body' },
457+
},
458+
{ 'X-Custom': 'value' },
459+
);
460+
461+
expect(rpcTool.getHeaders()).toEqual({ 'X-Custom': 'value' });
462+
});
463+
464+
it('should set headers on tool without requestBuilder', () => {
465+
const rpcTool = new BaseTool(
466+
'rpc_tool',
467+
'RPC tool',
468+
{ type: 'object', properties: {} },
469+
{
470+
kind: 'rpc',
471+
method: 'test_method',
472+
url: 'https://api.example.com/rpc',
473+
payloadKeys: { action: 'action', body: 'body' },
474+
},
475+
);
476+
477+
rpcTool.setHeaders({ 'X-New-Header': 'new-value' });
478+
expect(rpcTool.getHeaders()).toEqual({ 'X-New-Header': 'new-value' });
479+
});
480+
});
481+
331482
describe('Tools', () => {
332483
it('should get tool by name', () => {
333484
const tool = createMockTool();
@@ -952,6 +1103,40 @@ describe('Meta Search Tools', () => {
9521103
});
9531104
});
9541105

1106+
describe('Error handling', () => {
1107+
it('should wrap non-StackOneError in meta_search_tools execute', async () => {
1108+
const filterTool = metaTools.getTool('meta_search_tools');
1109+
assert(filterTool, 'filterTool should be defined');
1110+
1111+
// Pass invalid params type to trigger JSON.parse error on non-JSON string
1112+
await expect(filterTool.execute('not valid json')).rejects.toThrow('Error executing tool:');
1113+
});
1114+
1115+
it('should wrap non-StackOneError in meta_execute_tool execute', async () => {
1116+
const executeTool = metaTools.getTool('meta_execute_tool');
1117+
assert(executeTool, 'executeTool should be defined');
1118+
1119+
// Pass invalid JSON string to trigger JSON.parse error
1120+
await expect(executeTool.execute('not valid json')).rejects.toThrow('Error executing tool:');
1121+
});
1122+
1123+
it('should throw StackOneError for invalid params type in meta_search_tools', async () => {
1124+
const filterTool = metaTools.getTool('meta_search_tools');
1125+
assert(filterTool, 'filterTool should be defined');
1126+
1127+
// @ts-expect-error - intentionally passing invalid type
1128+
await expect(filterTool.execute(123)).rejects.toThrow('Invalid parameters type');
1129+
});
1130+
1131+
it('should throw StackOneError for invalid params type in meta_execute_tool', async () => {
1132+
const executeTool = metaTools.getTool('meta_execute_tool');
1133+
assert(executeTool, 'executeTool should be defined');
1134+
1135+
// @ts-expect-error - intentionally passing invalid type
1136+
await expect(executeTool.execute(true)).rejects.toThrow('Invalid parameters type');
1137+
});
1138+
});
1139+
9551140
describe('Integration: meta tools workflow', () => {
9561141
it('should discover and execute tools in sequence', async () => {
9571142
const filterTool = metaTools.getTool('meta_search_tools');

src/toolsets.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,134 @@ describe('StackOneToolSet', () => {
383383
});
384384
});
385385

386+
describe('tool execution', () => {
387+
it('should execute tool with dryRun option', async () => {
388+
const toolset = new StackOneToolSet({
389+
baseUrl: 'https://api.stackone-dev.com',
390+
apiKey: 'test-key',
391+
accountId: 'test-account',
392+
});
393+
394+
const tools = await toolset.fetchTools();
395+
const tool = tools.toArray().find((t) => t.name === 'dummy_action');
396+
assert(tool, 'tool should be defined');
397+
398+
const result = await tool.execute({ body: { name: 'test' } }, { dryRun: true });
399+
400+
expect(result.url).toBe('https://api.stackone-dev.com/actions/rpc');
401+
expect(result.method).toBe('POST');
402+
expect(result.headers).toBeDefined();
403+
expect(result.body).toBeDefined();
404+
expect(result.mappedParams).toEqual({ body: { name: 'test' } });
405+
});
406+
407+
it('should execute tool with path, query, and headers params', async () => {
408+
const toolset = new StackOneToolSet({
409+
baseUrl: 'https://api.stackone-dev.com',
410+
apiKey: 'test-key',
411+
accountId: 'test-account',
412+
});
413+
414+
const tools = await toolset.fetchTools();
415+
const tool = tools.toArray().find((t) => t.name === 'dummy_action');
416+
assert(tool, 'tool should be defined');
417+
418+
const result = await tool.execute(
419+
{
420+
body: { name: 'test' },
421+
path: { id: '123' },
422+
query: { limit: 10 },
423+
headers: { 'x-custom': 'value' },
424+
},
425+
{ dryRun: true },
426+
);
427+
428+
expect(result.mappedParams).toEqual({
429+
body: { name: 'test' },
430+
path: { id: '123' },
431+
query: { limit: 10 },
432+
headers: { 'x-custom': 'value' },
433+
});
434+
});
435+
436+
it('should execute tool with string parameters', async () => {
437+
const toolset = new StackOneToolSet({
438+
baseUrl: 'https://api.stackone-dev.com',
439+
apiKey: 'test-key',
440+
accountId: 'test-account',
441+
});
442+
443+
const tools = await toolset.fetchTools();
444+
const tool = tools.toArray().find((t) => t.name === 'dummy_action');
445+
assert(tool, 'tool should be defined');
446+
447+
const result = await tool.execute(JSON.stringify({ body: { name: 'test' } }), {
448+
dryRun: true,
449+
});
450+
451+
expect(result.mappedParams).toEqual({ body: { name: 'test' } });
452+
});
453+
454+
it('should throw StackOneError for invalid parameter type', async () => {
455+
const toolset = new StackOneToolSet({
456+
baseUrl: 'https://api.stackone-dev.com',
457+
apiKey: 'test-key',
458+
accountId: 'test-account',
459+
});
460+
461+
const tools = await toolset.fetchTools();
462+
const tool = tools.toArray().find((t) => t.name === 'dummy_action');
463+
assert(tool, 'tool should be defined');
464+
465+
// @ts-expect-error - intentionally passing invalid type
466+
await expect(tool.execute(12345)).rejects.toThrow('Invalid parameters type');
467+
});
468+
469+
it('should wrap non-StackOneError in execute', async () => {
470+
const toolset = new StackOneToolSet({
471+
baseUrl: 'https://api.stackone-dev.com',
472+
apiKey: 'test-key',
473+
accountId: 'test-account',
474+
});
475+
476+
const tools = await toolset.fetchTools();
477+
const tool = tools.toArray().find((t) => t.name === 'dummy_action');
478+
assert(tool, 'tool should be defined');
479+
480+
// Pass invalid JSON string to trigger JSON.parse error
481+
await expect(tool.execute('not valid json')).rejects.toThrow('Error executing RPC action');
482+
});
483+
484+
it('should include extra params in rpcBody', async () => {
485+
const toolset = new StackOneToolSet({
486+
baseUrl: 'https://api.stackone-dev.com',
487+
apiKey: 'test-key',
488+
accountId: 'test-account',
489+
});
490+
491+
const tools = await toolset.fetchTools();
492+
const tool = tools.toArray().find((t) => t.name === 'dummy_action');
493+
assert(tool, 'tool should be defined');
494+
495+
const result = await tool.execute(
496+
{
497+
body: { nested: 'value' },
498+
extraParam: 'extra-value',
499+
anotherParam: 123,
500+
},
501+
{ dryRun: true },
502+
);
503+
504+
// The body should include both the nested body and extra params
505+
const parsedBody = JSON.parse(result.body as string);
506+
expect(parsedBody.body).toEqual({
507+
nested: 'value',
508+
extraParam: 'extra-value',
509+
anotherParam: 123,
510+
});
511+
});
512+
});
513+
386514
describe('provider and action filtering', () => {
387515
it('filters tools by providers', async () => {
388516
const toolset = new StackOneToolSet({

0 commit comments

Comments
 (0)