From 3dc112cec0175f2ca71a0419019ea9e38d88259a Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:15:45 +0000 Subject: [PATCH 1/3] refactor(utils): split errors.ts into separate files - Split StackOneError into error-stackone.ts - Split StackOneAPIError into error-stackone-api.ts - Remove re-export file errors.ts - Update all imports to use direct paths Improves searchability and code organisation. --- src/feedback.ts | 2 +- src/index.ts | 3 ++- src/requestBuilder.ts | 2 +- src/rpc-client.ts | 2 +- src/tool.ts | 2 +- src/toolsets.ts | 2 +- src/utils/{errors.ts => error-stackone-api.ts} | 11 +---------- src/utils/error-stackone.ts | 9 +++++++++ src/utils/try-import.ts | 2 +- 9 files changed, 18 insertions(+), 17 deletions(-) rename src/utils/{errors.ts => error-stackone-api.ts} (94%) create mode 100644 src/utils/error-stackone.ts diff --git a/src/feedback.ts b/src/feedback.ts index d564df2..3ece780 100644 --- a/src/feedback.ts +++ b/src/feedback.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { DEFAULT_BASE_URL } from './consts'; import { BaseTool } from './tool'; import type { ExecuteConfig, ExecuteOptions, JsonObject, JsonValue, ToolParameters } from './types'; -import { StackOneError } from './utils/errors'; +import { StackOneError } from './utils/error-stackone'; interface FeedbackToolOptions { baseUrl?: string; diff --git a/src/index.ts b/src/index.ts index a20fc6e..e32aa7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,8 @@ export { BaseTool, StackOneTool, Tools } from './tool'; export { createFeedbackTool } from './feedback'; -export { StackOneAPIError, StackOneError } from './utils/errors'; +export { StackOneError } from './utils/error-stackone'; +export { StackOneAPIError } from './utils/error-stackone-api'; export { StackOneToolSet, diff --git a/src/requestBuilder.ts b/src/requestBuilder.ts index 61bcf11..1a53aa8 100644 --- a/src/requestBuilder.ts +++ b/src/requestBuilder.ts @@ -7,7 +7,7 @@ import { type JsonObject, ParameterLocation, } from './types'; -import { StackOneAPIError } from './utils/errors'; +import { StackOneAPIError } from './utils/error-stackone-api'; interface SerializationOptions { maxDepth?: number; diff --git a/src/rpc-client.ts b/src/rpc-client.ts index f2e1f8d..ceb90dc 100644 --- a/src/rpc-client.ts +++ b/src/rpc-client.ts @@ -8,7 +8,7 @@ import { rpcActionResponseSchema, rpcClientConfigSchema, } from './schema'; -import { StackOneAPIError } from './utils/errors'; +import { StackOneAPIError } from './utils/error-stackone-api'; // Re-export types for consumers and to make types portable export type { RpcActionResponse } from './schema'; diff --git a/src/tool.ts b/src/tool.ts index 39b2dfc..af0f612 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -20,7 +20,7 @@ import type { ToolParameters, } from './types'; -import { StackOneError } from './utils/errors'; +import { StackOneError } from './utils/error-stackone'; import { TfidfIndex } from './utils/tfidf-index'; import { tryImport } from './utils/try-import'; diff --git a/src/toolsets.ts b/src/toolsets.ts index f14d613..cedeadf 100644 --- a/src/toolsets.ts +++ b/src/toolsets.ts @@ -13,7 +13,7 @@ import type { RpcExecuteConfig, ToolParameters, } from './types'; -import { StackOneError } from './utils/errors'; +import { StackOneError } from './utils/error-stackone'; /** * Converts RpcActionResponse to JsonObject in a type-safe manner. diff --git a/src/utils/errors.ts b/src/utils/error-stackone-api.ts similarity index 94% rename from src/utils/errors.ts rename to src/utils/error-stackone-api.ts index dd95bea..fba587a 100644 --- a/src/utils/errors.ts +++ b/src/utils/error-stackone-api.ts @@ -1,14 +1,5 @@ import { USER_AGENT } from '../consts'; - -/** - * Base exception for StackOne errors - */ -export class StackOneError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - this.name = 'StackOneError'; - } -} +import { StackOneError } from './error-stackone'; /** * Raised when the StackOne API returns an error diff --git a/src/utils/error-stackone.ts b/src/utils/error-stackone.ts new file mode 100644 index 0000000..d81b28c --- /dev/null +++ b/src/utils/error-stackone.ts @@ -0,0 +1,9 @@ +/** + * Base exception for StackOne errors + */ +export class StackOneError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'StackOneError'; + } +} diff --git a/src/utils/try-import.ts b/src/utils/try-import.ts index 338c32d..0ba1540 100644 --- a/src/utils/try-import.ts +++ b/src/utils/try-import.ts @@ -1,4 +1,4 @@ -import { StackOneError } from './errors'; +import { StackOneError } from './error-stackone'; /** * Dynamically import an optional dependency with a friendly error message From 60b81d8d087d02b5dd765079d1829845cca5e872 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:16:01 +0000 Subject: [PATCH 2/3] test: improve test coverage from 85% to 93% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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% --- src/feedback.test.ts | 2 +- src/requestBuilder.test.ts | 2 +- src/rpc-client.test.ts | 2 +- src/tool.test.ts | 187 +++++++++++++++++++++++- src/toolsets.test.ts | 128 +++++++++++++++++ src/utils/error-stackone-api.test.ts | 208 +++++++++++++++++++++++++++ src/utils/error-stackone.test.ts | 15 ++ src/utils/try-import.test.ts | 2 +- vitest.config.ts | 2 +- 9 files changed, 542 insertions(+), 6 deletions(-) create mode 100644 src/utils/error-stackone-api.test.ts create mode 100644 src/utils/error-stackone.test.ts diff --git a/src/feedback.test.ts b/src/feedback.test.ts index c8bf18c..2c16d85 100644 --- a/src/feedback.test.ts +++ b/src/feedback.test.ts @@ -1,6 +1,6 @@ import { http, HttpResponse } from 'msw'; import { server } from '../mocks/node'; -import { StackOneError } from './utils/errors'; +import { StackOneError } from './utils/error-stackone'; import { createFeedbackTool } from './feedback'; interface FeedbackResultItem { diff --git a/src/requestBuilder.test.ts b/src/requestBuilder.test.ts index 258b2ec..8996d5e 100644 --- a/src/requestBuilder.test.ts +++ b/src/requestBuilder.test.ts @@ -1,7 +1,7 @@ import { http, HttpResponse } from 'msw'; import { server } from '../mocks/node'; import { type HttpExecuteConfig, type JsonObject, ParameterLocation } from './types'; -import { StackOneAPIError } from './utils/errors'; +import { StackOneAPIError } from './utils/error-stackone-api'; import { RequestBuilder } from './requestBuilder'; describe('RequestBuilder', () => { diff --git a/src/rpc-client.test.ts b/src/rpc-client.test.ts index 47d5a56..c6ce823 100644 --- a/src/rpc-client.test.ts +++ b/src/rpc-client.test.ts @@ -1,6 +1,6 @@ import { RpcClient } from './rpc-client'; import { stackOneHeadersSchema } from './headers'; -import { StackOneAPIError } from './utils/errors'; +import { StackOneAPIError } from './utils/error-stackone-api'; test('should successfully execute an RPC action', async () => { const client = new RpcClient({ diff --git a/src/tool.test.ts b/src/tool.test.ts index 319699f..876f256 100644 --- a/src/tool.test.ts +++ b/src/tool.test.ts @@ -33,7 +33,7 @@ import { ParameterLocation, type ToolParameters, } from './types'; -import { StackOneAPIError } from './utils/errors'; +import { StackOneAPIError } from './utils/error-stackone-api'; // Create a mock tool for testing const createMockTool = (headers?: Record): BaseTool => { @@ -328,6 +328,157 @@ describe('StackOneTool', () => { }); }); +describe('BaseTool - additional coverage', () => { + it('should throw error when execute is called on non-HTTP tool', async () => { + const rpcTool = new BaseTool( + 'rpc_tool', + 'RPC tool', + { type: 'object', properties: {} }, + { + kind: 'rpc', + method: 'test_method', + url: 'https://api.example.com/rpc', + payloadKeys: { action: 'action', body: 'body' }, + }, + ); + + await expect(rpcTool.execute({})).rejects.toThrow( + 'BaseTool.execute is only available for HTTP-backed tools', + ); + }); + + it('should throw error for invalid parameter type', async () => { + const tool = createMockTool(); + + // @ts-expect-error - intentionally passing invalid type + await expect(tool.execute(12345)).rejects.toThrow('Invalid parameters type'); + }); + + it('should create execution metadata for RPC config in toAISDK', async () => { + const rpcTool = new BaseTool( + 'rpc_tool', + 'RPC tool', + { type: 'object', properties: {} }, + { + kind: 'rpc', + method: 'test_method', + url: 'https://api.example.com/rpc', + payloadKeys: { action: 'action', body: 'body', headers: 'headers' }, + }, + ); + + const aiSdkTool = await rpcTool.toAISDK({ executable: false }); + const execution = aiSdkTool.rpc_tool.execution; + + expect(execution).toBeDefined(); + expect(execution?.config.kind).toBe('rpc'); + if (execution?.config.kind === 'rpc') { + expect(execution.config.method).toBe('test_method'); + expect(execution.config.url).toBe('https://api.example.com/rpc'); + expect(execution.config.payloadKeys).toEqual({ + action: 'action', + body: 'body', + headers: 'headers', + }); + } + }); + + it('should create execution metadata for local config in toAISDK', async () => { + const localTool = new BaseTool( + 'local_tool', + 'Local tool', + { type: 'object', properties: {} }, + { + kind: 'local', + identifier: 'local_test', + description: 'local://test', + }, + ); + + const aiSdkTool = await localTool.toAISDK({ executable: false }); + const execution = aiSdkTool.local_tool.execution; + + expect(execution).toBeDefined(); + expect(execution?.config.kind).toBe('local'); + if (execution?.config.kind === 'local') { + expect(execution.config.identifier).toBe('local_test'); + expect(execution.config.description).toBe('local://test'); + } + }); + + it('should allow providing custom execution metadata in toAISDK', async () => { + const tool = createMockTool(); + const customExecution = { + config: { + kind: 'http' as const, + method: 'POST' as const, + url: 'https://custom.example.com', + bodyType: 'json' as const, + params: [], + }, + headers: { 'X-Custom': 'value' }, + }; + + const aiSdkTool = await tool.toAISDK({ execution: customExecution }); + const execution = aiSdkTool.test_tool.execution; + + expect(execution).toBeDefined(); + expect(execution?.config.kind).toBe('http'); + if (execution?.config.kind === 'http') { + expect(execution.config.url).toBe('https://custom.example.com'); + } + expect(execution?.headers).toEqual({ 'X-Custom': 'value' }); + }); + + it('should return undefined execution when execution option is false', async () => { + const tool = createMockTool(); + + const aiSdkTool = await tool.toAISDK({ execution: false }); + expect(aiSdkTool.test_tool.execution).toBeUndefined(); + }); + + it('should return undefined execute when executable option is false', async () => { + const tool = createMockTool(); + + const aiSdkTool = await tool.toAISDK({ executable: false }); + expect(aiSdkTool.test_tool.execute).toBeUndefined(); + }); + + it('should get headers from tool without requestBuilder', () => { + const rpcTool = new BaseTool( + 'rpc_tool', + 'RPC tool', + { type: 'object', properties: {} }, + { + kind: 'rpc', + method: 'test_method', + url: 'https://api.example.com/rpc', + payloadKeys: { action: 'action', body: 'body' }, + }, + { 'X-Custom': 'value' }, + ); + + expect(rpcTool.getHeaders()).toEqual({ 'X-Custom': 'value' }); + }); + + it('should set headers on tool without requestBuilder', () => { + const rpcTool = new BaseTool( + 'rpc_tool', + 'RPC tool', + { type: 'object', properties: {} }, + { + kind: 'rpc', + method: 'test_method', + url: 'https://api.example.com/rpc', + payloadKeys: { action: 'action', body: 'body' }, + }, + ); + + rpcTool.setHeaders({ 'X-New-Header': 'new-value' }); + expect(rpcTool.getHeaders()).toEqual({ 'X-New-Header': 'new-value' }); + }); +}); + describe('Tools', () => { it('should get tool by name', () => { const tool = createMockTool(); @@ -952,6 +1103,40 @@ describe('Meta Search Tools', () => { }); }); + describe('Error handling', () => { + it('should wrap non-StackOneError in meta_search_tools execute', async () => { + const filterTool = metaTools.getTool('meta_search_tools'); + assert(filterTool, 'filterTool should be defined'); + + // Pass invalid params type to trigger JSON.parse error on non-JSON string + await expect(filterTool.execute('not valid json')).rejects.toThrow('Error executing tool:'); + }); + + it('should wrap non-StackOneError in meta_execute_tool execute', async () => { + const executeTool = metaTools.getTool('meta_execute_tool'); + assert(executeTool, 'executeTool should be defined'); + + // Pass invalid JSON string to trigger JSON.parse error + await expect(executeTool.execute('not valid json')).rejects.toThrow('Error executing tool:'); + }); + + it('should throw StackOneError for invalid params type in meta_search_tools', async () => { + const filterTool = metaTools.getTool('meta_search_tools'); + assert(filterTool, 'filterTool should be defined'); + + // @ts-expect-error - intentionally passing invalid type + await expect(filterTool.execute(123)).rejects.toThrow('Invalid parameters type'); + }); + + it('should throw StackOneError for invalid params type in meta_execute_tool', async () => { + const executeTool = metaTools.getTool('meta_execute_tool'); + assert(executeTool, 'executeTool should be defined'); + + // @ts-expect-error - intentionally passing invalid type + await expect(executeTool.execute(true)).rejects.toThrow('Invalid parameters type'); + }); + }); + describe('Integration: meta tools workflow', () => { it('should discover and execute tools in sequence', async () => { const filterTool = metaTools.getTool('meta_search_tools'); diff --git a/src/toolsets.test.ts b/src/toolsets.test.ts index 3cf8b01..66acfd7 100644 --- a/src/toolsets.test.ts +++ b/src/toolsets.test.ts @@ -383,6 +383,134 @@ describe('StackOneToolSet', () => { }); }); + describe('tool execution', () => { + it('should execute tool with dryRun option', async () => { + const toolset = new StackOneToolSet({ + baseUrl: 'https://api.stackone-dev.com', + apiKey: 'test-key', + accountId: 'test-account', + }); + + const tools = await toolset.fetchTools(); + const tool = tools.toArray().find((t) => t.name === 'dummy_action'); + assert(tool, 'tool should be defined'); + + const result = await tool.execute({ body: { name: 'test' } }, { dryRun: true }); + + expect(result.url).toBe('https://api.stackone-dev.com/actions/rpc'); + expect(result.method).toBe('POST'); + expect(result.headers).toBeDefined(); + expect(result.body).toBeDefined(); + expect(result.mappedParams).toEqual({ body: { name: 'test' } }); + }); + + it('should execute tool with path, query, and headers params', async () => { + const toolset = new StackOneToolSet({ + baseUrl: 'https://api.stackone-dev.com', + apiKey: 'test-key', + accountId: 'test-account', + }); + + const tools = await toolset.fetchTools(); + const tool = tools.toArray().find((t) => t.name === 'dummy_action'); + assert(tool, 'tool should be defined'); + + const result = await tool.execute( + { + body: { name: 'test' }, + path: { id: '123' }, + query: { limit: 10 }, + headers: { 'x-custom': 'value' }, + }, + { dryRun: true }, + ); + + expect(result.mappedParams).toEqual({ + body: { name: 'test' }, + path: { id: '123' }, + query: { limit: 10 }, + headers: { 'x-custom': 'value' }, + }); + }); + + it('should execute tool with string parameters', async () => { + const toolset = new StackOneToolSet({ + baseUrl: 'https://api.stackone-dev.com', + apiKey: 'test-key', + accountId: 'test-account', + }); + + const tools = await toolset.fetchTools(); + const tool = tools.toArray().find((t) => t.name === 'dummy_action'); + assert(tool, 'tool should be defined'); + + const result = await tool.execute(JSON.stringify({ body: { name: 'test' } }), { + dryRun: true, + }); + + expect(result.mappedParams).toEqual({ body: { name: 'test' } }); + }); + + it('should throw StackOneError for invalid parameter type', async () => { + const toolset = new StackOneToolSet({ + baseUrl: 'https://api.stackone-dev.com', + apiKey: 'test-key', + accountId: 'test-account', + }); + + const tools = await toolset.fetchTools(); + const tool = tools.toArray().find((t) => t.name === 'dummy_action'); + assert(tool, 'tool should be defined'); + + // @ts-expect-error - intentionally passing invalid type + await expect(tool.execute(12345)).rejects.toThrow('Invalid parameters type'); + }); + + it('should wrap non-StackOneError in execute', async () => { + const toolset = new StackOneToolSet({ + baseUrl: 'https://api.stackone-dev.com', + apiKey: 'test-key', + accountId: 'test-account', + }); + + const tools = await toolset.fetchTools(); + const tool = tools.toArray().find((t) => t.name === 'dummy_action'); + assert(tool, 'tool should be defined'); + + // Pass invalid JSON string to trigger JSON.parse error + await expect(tool.execute('not valid json')).rejects.toThrow('Error executing RPC action'); + }); + + it('should include extra params in rpcBody', async () => { + const toolset = new StackOneToolSet({ + baseUrl: 'https://api.stackone-dev.com', + apiKey: 'test-key', + accountId: 'test-account', + }); + + const tools = await toolset.fetchTools(); + const tool = tools.toArray().find((t) => t.name === 'dummy_action'); + assert(tool, 'tool should be defined'); + + const result = await tool.execute( + { + body: { nested: 'value' }, + extraParam: 'extra-value', + anotherParam: 123, + }, + { dryRun: true }, + ); + + // The body should include both the nested body and extra params + const parsedBody = JSON.parse(result.body as string); + expect(parsedBody.body).toEqual({ + nested: 'value', + extraParam: 'extra-value', + anotherParam: 123, + }); + }); + }); + describe('provider and action filtering', () => { it('filters tools by providers', async () => { const toolset = new StackOneToolSet({ diff --git a/src/utils/error-stackone-api.test.ts b/src/utils/error-stackone-api.test.ts new file mode 100644 index 0000000..c52ca1a --- /dev/null +++ b/src/utils/error-stackone-api.test.ts @@ -0,0 +1,208 @@ +import { USER_AGENT } from '../consts'; +import { StackOneAPIError } from './error-stackone-api'; + +describe('StackOneAPIError', () => { + it('should create an error with basic properties', () => { + const error = new StackOneAPIError('API failed', 500, { error: 'Server error' }); + expect(error.name).toBe('StackOneAPIError'); + expect(error.statusCode).toBe(500); + expect(error.responseBody).toEqual({ error: 'Server error' }); + expect(error.message).toBe('API failed'); + }); + + it('should append message from responseBody if present', () => { + const error = new StackOneAPIError('API failed', 400, { + message: 'Invalid request body', + }); + expect(error.message).toBe('API failed: Invalid request body'); + }); + + it('should not append message if responseBody.message is not a string', () => { + const error = new StackOneAPIError('API failed', 400, { message: 123 }); + expect(error.message).toBe('API failed'); + }); + + it('should not append message if responseBody.message is empty', () => { + const error = new StackOneAPIError('API failed', 400, { message: '' }); + expect(error.message).toBe('API failed'); + }); + + it('should not append message if responseBody is null', () => { + const error = new StackOneAPIError('API failed', 400, null); + expect(error.message).toBe('API failed'); + }); + + it('should extract provider errors from responseBody', () => { + const providerErrors = [{ status: 401, url: 'https://provider.com/api' }]; + const error = new StackOneAPIError('API failed', 400, { + provider_errors: providerErrors, + }); + expect(error.providerErrors).toEqual(providerErrors); + }); + + it('should not extract provider errors if not an array', () => { + const error = new StackOneAPIError('API failed', 400, { + provider_errors: 'not an array', + }); + expect(error.providerErrors).toBeUndefined(); + }); + + it('should store requestBody when provided', () => { + const requestBody = { foo: 'bar' }; + const error = new StackOneAPIError('API failed', 400, {}, requestBody); + expect(error.requestBody).toEqual(requestBody); + }); + + it('should support error cause via options', () => { + const cause = new Error('Network error'); + const error = new StackOneAPIError('API failed', 500, {}, undefined, { cause }); + expect(error.cause).toBe(cause); + }); + + it('should format basic error message via toString', () => { + const error = new StackOneAPIError('Request failed', 404, {}); + const result = error.toString(); + expect(result).toContain('API Error: 404'); + expect(result).toContain('Request Headers:'); + expect(result).toContain('Authorization: [REDACTED]'); + expect(result).toContain(`User-Agent: ${USER_AGENT}`); + }); + + it('should include endpoint URL when present in message', () => { + const error = new StackOneAPIError( + 'Request failed for https://api.stackone.com/unified/hris/employees', + 404, + {}, + ); + const result = error.toString(); + expect(result).toContain('Endpoint: https://api.stackone.com/unified/hris/employees'); + }); + + it('should format object requestBody as JSON', () => { + const requestBody = { name: 'John', age: 30 }; + const error = new StackOneAPIError('Request failed', 400, {}, requestBody); + const result = error.toString(); + expect(result).toContain('Request Body:'); + expect(result).toContain('"name": "John"'); + expect(result).toContain('"age": 30'); + }); + + it('should format string requestBody', () => { + const error = new StackOneAPIError('Request failed', 400, {}, 'raw string body'); + const result = error.toString(); + expect(result).toContain('Request Body:'); + expect(result).toContain('raw string body'); + }); + + it('should format non-object non-string requestBody', () => { + const error = new StackOneAPIError('Request failed', 400, {}, 12345); + const result = error.toString(); + expect(result).toContain('Request Body:'); + expect(result).toContain('12345'); + }); + + it('should handle circular reference in requestBody gracefully', () => { + const circular: Record = { name: 'test' }; + circular['self'] = circular; + const error = new StackOneAPIError('Request failed', 400, {}, circular); + const result = error.toString(); + expect(result).toContain('[Unable to stringify request body]'); + }); + + it('should format provider errors with status', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: [{ status: 401 }], + }); + const result = error.toString(); + expect(result).toContain('Provider Error:'); + expect(result).toContain('401'); + }); + + it('should format provider errors with raw error message', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: [ + { + status: 403, + raw: { error: 'Access denied' }, + }, + ], + }); + const result = error.toString(); + expect(result).toContain('403'); + expect(result).toContain('Access denied'); + }); + + it('should format provider errors with URL', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: [ + { + status: 500, + url: 'https://provider.example.com/api/v1/resource', + }, + ], + }); + const result = error.toString(); + expect(result).toContain('Provider Endpoint: https://provider.example.com/api/v1/resource'); + }); + + it('should handle provider error that is not an object', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: ['string error'], + }); + const result = error.toString(); + expect(result).not.toContain('Provider Error:'); + }); + + it('should handle null provider error', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: [null], + }); + const result = error.toString(); + expect(result).not.toContain('Provider Error:'); + }); + + it('should handle provider error with non-number status', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: [{ status: 'not a number' }], + }); + const result = error.toString(); + expect(result).toContain('Provider Error:'); + expect(result).not.toContain('not a number'); + }); + + it('should handle provider error with non-object raw', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: [{ status: 500, raw: 'not an object' }], + }); + const result = error.toString(); + expect(result).toContain('Provider Error:'); + expect(result).toContain('500'); + expect(result).not.toContain('not an object'); + }); + + it('should handle provider error with null raw', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: [{ status: 500, raw: null }], + }); + const result = error.toString(); + expect(result).toContain('Provider Error:'); + expect(result).toContain('500'); + }); + + it('should handle provider error with raw.error that is not a string', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: [{ status: 500, raw: { error: 123 } }], + }); + const result = error.toString(); + expect(result).toContain('500'); + expect(result).not.toContain('123'); + }); + + it('should handle provider error with non-string url', () => { + const error = new StackOneAPIError('Request failed', 400, { + provider_errors: [{ status: 500, url: 12345 }], + }); + const result = error.toString(); + expect(result).not.toContain('Provider Endpoint:'); + }); +}); diff --git a/src/utils/error-stackone.test.ts b/src/utils/error-stackone.test.ts new file mode 100644 index 0000000..7f3ecc3 --- /dev/null +++ b/src/utils/error-stackone.test.ts @@ -0,0 +1,15 @@ +import { StackOneError } from './error-stackone'; + +describe('StackOneError', () => { + it('should create an error with the correct name', () => { + const error = new StackOneError('Test error'); + expect(error.name).toBe('StackOneError'); + expect(error.message).toBe('Test error'); + }); + + it('should support error cause via options', () => { + const cause = new Error('Original error'); + const error = new StackOneError('Wrapped error', { cause }); + expect(error.cause).toBe(cause); + }); +}); diff --git a/src/utils/try-import.test.ts b/src/utils/try-import.test.ts index 1bdbbe0..bb8797e 100644 --- a/src/utils/try-import.test.ts +++ b/src/utils/try-import.test.ts @@ -1,4 +1,4 @@ -import { StackOneError } from './errors'; +import { StackOneError } from './error-stackone'; import { tryImport } from './try-import'; describe('tryImport', () => { diff --git a/vitest.config.ts b/vitest.config.ts index 7177643..50684d1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ provider: 'v8', reporter: ['text', 'json', 'json-summary', 'html'], include: ['src/**/*.ts'], - exclude: ['**/*.test.ts', '**/*.test-d.ts'], + exclude: ['**/*.test.ts', '**/*.test-d.ts', '**/index.ts', '**/type.ts'], }, projects: [ { From 9b56837b44ebf11b6bcc38e8e4979370c79c194d Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:37:34 +0000 Subject: [PATCH 3/3] fix(test): replace unified API endpoint with generic endpoint Apply review feedback from @glebedel - this SDK should not have unified endpoints related tests. Changed the test URL from /unified/hris/employees to /tools/execute. --- src/utils/error-stackone-api.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/error-stackone-api.test.ts b/src/utils/error-stackone-api.test.ts index c52ca1a..cca55b9 100644 --- a/src/utils/error-stackone-api.test.ts +++ b/src/utils/error-stackone-api.test.ts @@ -70,12 +70,12 @@ describe('StackOneAPIError', () => { it('should include endpoint URL when present in message', () => { const error = new StackOneAPIError( - 'Request failed for https://api.stackone.com/unified/hris/employees', + 'Request failed for https://api.stackone.com/tools/execute', 404, {}, ); const result = error.toString(); - expect(result).toContain('Endpoint: https://api.stackone.com/unified/hris/employees'); + expect(result).toContain('Endpoint: https://api.stackone.com/tools/execute'); }); it('should format object requestBody as JSON', () => {