/**
* Tests for create_company tool
*
* Tests company creation with required/optional fields, validation, and error handling.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { handleCreateCompany } from '../../../src/tools/create-company.js';
import { createMockAttioClient } from '../../helpers/mock-attio-client.js';
import { loadFixture } from '../../helpers/fixtures.js';
import * as attioClientModule from '../../../src/attio-client.js';
vi.mock('../../../src/attio-client.js', () => ({
createAttioClient: vi.fn(),
}));
describe('create_company', () => {
let mockClient: ReturnType<typeof createMockAttioClient>;
let originalApiKey: string | undefined;
beforeEach(() => {
originalApiKey = process.env.ATTIO_API_KEY;
process.env.ATTIO_API_KEY = 'test-api-key';
mockClient = createMockAttioClient();
vi.mocked(attioClientModule.createAttioClient).mockReturnValue(
mockClient as any
);
});
afterEach(() => {
if (originalApiKey) {
process.env.ATTIO_API_KEY = originalApiKey;
} else {
delete process.env.ATTIO_API_KEY;
}
mockClient.reset();
});
describe('Successful creation', () => {
it('should create company with only name (minimum required)', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'companies',
record_id: 'rec-new-123',
},
created_at: '2024-11-15T10:00:00Z',
web_url: 'https://app.attio.com/companies/rec-new-123',
values: {
name: [{ value: 'New Company' }],
},
},
};
mockClient.mockResponse('POST', '/objects/companies/records', {
data: mockResponse,
});
const result = await handleCreateCompany({ name: 'New Company' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.name).toBe('New Company');
expect(parsed.data.record_id).toBe('rec-new-123');
expect(parsed.data.workspace_id).toBe('ws-123');
expect(parsed.data.created_at).toBeDefined();
expect(parsed.data.web_url).toBeDefined();
});
it('should create company with all optional fields', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'companies',
record_id: 'rec-full-123',
},
created_at: '2024-11-15T10:00:00Z',
web_url: 'https://app.attio.com/companies/rec-full-123',
values: {
name: [{ value: 'Full Company' }],
domains: [
{ domain: 'example.com', root_domain: 'example.com' },
{ domain: 'example.io', root_domain: 'example.io' },
],
description: [{ value: 'Test description' }],
linkedin: [{ value: 'https://linkedin.com/company/example' }],
},
},
};
mockClient.mockResponse('POST', '/objects/companies/records', {
data: mockResponse,
});
const result = await handleCreateCompany({
name: 'Full Company',
domains: ['example.com', 'example.io'],
description: 'Test description',
linkedin: 'https://linkedin.com/company/example',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.name).toBe('Full Company');
expect(parsed.data.domains).toEqual(['example.com', 'example.io']);
expect(parsed.data.description).toBe('Test description');
expect(parsed.data.linkedin).toBe('https://linkedin.com/company/example');
});
it('should trim whitespace from name', async () => {
mockClient.mockResponse('POST', '/objects/companies/records', {
data: {
data: {
id: { record_id: 'rec-123' },
values: { name: [{ value: 'Trimmed Company' }] },
},
},
});
await handleCreateCompany({ name: ' Trimmed Company ' });
const calls = mockClient.getCallsFor('POST', '/objects/companies/records');
expect(calls[0].body).toMatchObject({
data: {
values: {
name: [{ value: 'Trimmed Company' }],
},
},
});
});
it('should trim whitespace from domains', async () => {
mockClient.mockResponse('POST', '/objects/companies/records', {
data: {
data: {
id: { record_id: 'rec-123' },
values: { name: [{ value: 'Test' }] },
},
},
});
await handleCreateCompany({
name: 'Test',
domains: [' example.com ', ' test.io '],
});
const calls = mockClient.getCallsFor('POST', '/objects/companies/records');
expect(calls[0].body).toMatchObject({
data: {
values: {
domains: [{ domain: 'example.com' }, { domain: 'test.io' }],
},
},
});
});
it('should filter out empty domains', async () => {
mockClient.mockResponse('POST', '/objects/companies/records', {
data: {
data: {
id: { record_id: 'rec-123' },
values: { name: [{ value: 'Test' }] },
},
},
});
await handleCreateCompany({
name: 'Test',
domains: ['example.com', '', ' ', 'test.io'],
});
const calls = mockClient.getCallsFor('POST', '/objects/companies/records');
expect(calls[0].body).toMatchObject({
data: {
values: {
domains: [{ domain: 'example.com' }, { domain: 'test.io' }],
},
},
});
});
it('should not include domains field if array is empty after filtering', async () => {
mockClient.mockResponse('POST', '/objects/companies/records', {
data: {
data: {
id: { record_id: 'rec-123' },
values: { name: [{ value: 'Test' }] },
},
},
});
await handleCreateCompany({
name: 'Test',
domains: ['', ' '],
});
const calls = mockClient.getCallsFor('POST', '/objects/companies/records');
// Empty array is fine - domains field not added to request
const domains = calls[0].body.data.values.domains;
expect(domains === undefined || domains.length === 0).toBe(true);
});
it('should trim whitespace from description', async () => {
mockClient.mockResponse('POST', '/objects/companies/records', {
data: {
data: {
id: { record_id: 'rec-123' },
values: { name: [{ value: 'Test' }] },
},
},
});
await handleCreateCompany({
name: 'Test',
description: ' Test description ',
});
const calls = mockClient.getCallsFor('POST', '/objects/companies/records');
expect(calls[0].body).toMatchObject({
data: {
values: {
description: [{ value: 'Test description' }],
},
},
});
});
it('should not include description if empty after trimming', async () => {
mockClient.mockResponse('POST', '/objects/companies/records', {
data: {
data: {
id: { record_id: 'rec-123' },
values: { name: [{ value: 'Test' }] },
},
},
});
await handleCreateCompany({
name: 'Test',
description: ' ',
});
const calls = mockClient.getCallsFor('POST', '/objects/companies/records');
expect(calls[0].body.data.values.description).toBeUndefined();
});
it('should include all metadata in response', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-456',
object_id: 'companies',
record_id: 'rec-789',
},
created_at: '2024-11-15T12:34:56Z',
web_url: 'https://app.attio.com/companies/rec-789',
values: {
name: [{ value: 'Test Company' }],
},
},
};
mockClient.mockResponse('POST', '/objects/companies/records', {
data: mockResponse,
});
const result = await handleCreateCompany({ name: 'Test Company' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.data).toMatchObject({
record_id: 'rec-789',
workspace_id: 'ws-456',
object_id: 'companies',
created_at: '2024-11-15T12:34:56Z',
web_url: 'https://app.attio.com/companies/rec-789',
});
});
it('should handle null values in response gracefully', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'companies',
record_id: 'rec-123',
},
created_at: '2024-11-15T10:00:00Z',
web_url: 'https://app.attio.com/companies/rec-123',
values: {
name: [{ value: 'Test' }],
// domains, description, linkedin not included
},
},
};
mockClient.mockResponse('POST', '/objects/companies/records', {
data: mockResponse,
});
const result = await handleCreateCompany({ name: 'Test' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.data.name).toBe('Test');
expect(parsed.data.domains).toEqual([]);
expect(parsed.data.description).toBeNull();
expect(parsed.data.linkedin).toBeNull();
});
});
describe('Validation errors', () => {
it('should fail if ATTIO_API_KEY is not configured', async () => {
delete process.env.ATTIO_API_KEY;
const result = await handleCreateCompany({ name: 'Test' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('validation_error');
expect(parsed.error.message).toContain('ATTIO_API_KEY');
});
it('should fail if name is empty string', async () => {
const result = await handleCreateCompany({ name: '' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('validation_error');
expect(parsed.error.field).toBe('name');
expect(parsed.error.message).toContain('required');
});
it('should fail if name is only whitespace', async () => {
const result = await handleCreateCompany({ name: ' ' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.field).toBe('name');
});
});
describe('API errors', () => {
it('should handle uniqueness constraint conflicts', async () => {
const conflictError = {
statusCode: 400,
message:
'uniqueness constraint violated: The value "Acme Corp" provided for attribute with slug "name"',
};
mockClient.mockResponse('POST', '/objects/companies/records', {
error: conflictError,
});
const result = await handleCreateCompany({ name: 'Acme Corp' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('conflict_error');
expect(parsed.error.field).toBe('name');
expect(parsed.error.suggestions).toContain('Search for the existing record');
});
it('should handle validation errors from API', async () => {
// Simulate Attio API validation error (400 with validation_errors)
mockClient.mockResponse('POST', '/objects/companies/records', {
error: {
statusCode: 400,
message: 'Validation failed',
response: {
message: 'Validation failed',
validation_errors: [
{
path: ['name'],
message: 'Required',
},
],
},
},
});
const result = await handleCreateCompany({ name: 'Test' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('validation_error');
expect(parsed.error.validation_errors).toBeDefined();
});
it('should handle authentication errors', async () => {
const authError = loadFixture('api-errors.json', 'authentication');
mockClient.mockResponse('POST', '/objects/companies/records', {
error: {
statusCode: authError.error.status,
message: authError.error.message,
},
});
const result = await handleCreateCompany({ name: 'Test' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('authentication_error');
expect(parsed.error.suggestions).toContain('Verify ATTIO_API_KEY is correct');
});
it('should handle rate limit errors', async () => {
const rateLimitError = loadFixture('api-errors.json', 'rate-limit');
mockClient.mockResponse('POST', '/objects/companies/records', {
error: {
statusCode: rateLimitError.error.status,
message: rateLimitError.error.message,
response: { retry_after: 60 },
},
});
const result = await handleCreateCompany({ name: 'Test' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('rate_limit_error');
expect(parsed.error.retryable).toBe(true);
});
});
describe('Request format', () => {
it('should send data in correct Attio format', async () => {
mockClient.mockResponse('POST', '/objects/companies/records', {
data: {
data: {
id: { record_id: 'rec-123' },
values: {},
},
},
});
await handleCreateCompany({
name: 'Test Company',
domains: ['test.com'],
description: 'A test',
linkedin: 'https://linkedin.com/company/test',
});
const calls = mockClient.getCallsFor('POST', '/objects/companies/records');
expect(calls).toHaveLength(1);
expect(calls[0].body).toEqual({
data: {
values: {
name: [{ value: 'Test Company' }],
domains: [{ domain: 'test.com' }],
description: [{ value: 'A test' }],
linkedin: [{ value: 'https://linkedin.com/company/test' }],
},
},
});
});
});
});