/**
* Tests for get_person tool
*
* Tests single record retrieval by ID with full attribute handling.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { handleGetPerson } from '../../../src/tools/get-person.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('get_person', () => {
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 retrieval', () => {
it('should get person by record_id', async () => {
const personData = loadFixture('people.json', 'john-doe');
mockClient.mockResponse('GET', '/objects/people/records/person-john-123', {
data: { data: personData },
});
const result = await handleGetPerson({
record_id: 'person-john-123',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.record_id).toBe('person-john-123');
expect(parsed.data.name).toBe('John Doe');
});
it('should include all standard fields', async () => {
const personData = loadFixture('people.json', 'john-doe');
mockClient.mockResponse('GET', '/objects/people/records/person-john-123', {
data: { data: personData },
});
const result = await handleGetPerson({
record_id: 'person-john-123',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.data).toMatchObject({
record_id: 'person-john-123',
workspace_id: 'workspace-123',
object_id: 'people',
name: 'John Doe',
first_name: 'John',
last_name: 'Doe',
email_addresses: ['john@acme.com'],
});
});
it('should include all_attributes with raw data', async () => {
const personData = loadFixture('people.json', 'john-doe');
mockClient.mockResponse('GET', '/objects/people/records/person-john-123', {
data: { data: personData },
});
const result = await handleGetPerson({
record_id: 'person-john-123',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.data.all_attributes).toBeDefined();
expect(parsed.data.all_attributes.name).toBeDefined();
expect(parsed.data.all_attributes.email_addresses).toBeDefined();
});
it('should handle person with minimal data', async () => {
const minimalPerson = loadFixture('people.json', 'minimal-person');
mockClient.mockResponse(
'GET',
'/objects/people/records/person-minimal-789',
{
data: { data: minimalPerson },
}
);
const result = await handleGetPerson({
record_id: 'person-minimal-789',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.name).toBe('Bob Johnson');
expect(parsed.data.email_addresses).toEqual([]);
expect(parsed.data.description).toBeNull();
expect(parsed.data.linkedin).toBeNull();
});
it('should handle people with multiple email addresses', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'people',
record_id: 'person-multi-123',
},
values: {
name: [
{
first_name: 'Multi',
last_name: 'Email',
full_name: 'Multi Email',
},
],
email_addresses: [
{ email_address: 'primary@example.com' },
{ email_address: 'secondary@example.com' },
{ email_address: 'work@company.com' },
],
},
},
};
mockClient.mockResponse(
'GET',
'/objects/people/records/person-multi-123',
{
data: mockResponse,
}
);
const result = await handleGetPerson({ record_id: 'person-multi-123' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.data.email_addresses).toEqual([
'primary@example.com',
'secondary@example.com',
'work@company.com',
]);
});
it('should use GET method with correct path', async () => {
mockClient.mockResponse('GET', '/objects/people/records/test-id', {
data: {
data: {
id: { record_id: 'test-id' },
values: {
name: [{ first_name: 'Test', last_name: 'Person', full_name: 'Test Person' }],
},
},
},
});
await handleGetPerson({ record_id: 'test-id' });
const calls = mockClient.getCallsFor('GET');
expect(calls).toHaveLength(1);
expect(calls[0].path).toBe('/objects/people/records/test-id');
});
});
describe('Validation errors', () => {
it('should fail if ATTIO_API_KEY is not configured', async () => {
delete process.env.ATTIO_API_KEY;
const result = await handleGetPerson({ record_id: 'test-id' });
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 record_id is empty string', async () => {
const result = await handleGetPerson({ record_id: '' });
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('record_id');
expect(parsed.error.message).toContain('required');
});
it('should fail if record_id is only whitespace', async () => {
const result = await handleGetPerson({ record_id: ' ' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.field).toBe('record_id');
});
});
describe('API errors', () => {
it('should handle 404 Not Found', async () => {
const notFoundError = loadFixture('api-errors.json', 'not-found');
mockClient.mockResponse('GET', '/objects/people/records/nonexistent-id', {
error: {
statusCode: notFoundError.error.status,
message: notFoundError.error.message,
response: { message: notFoundError.error.message },
},
});
const result = await handleGetPerson({ record_id: 'nonexistent-id' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('not_found_error');
expect(parsed.error.http_status).toBe(404);
expect(parsed.error.suggestions).toContain('Verify the record_id is correct');
});
it('should handle authentication errors', async () => {
const authError = loadFixture('api-errors.json', 'authentication');
mockClient.mockResponse('GET', '/objects/people/records/test-id', {
error: {
statusCode: authError.error.status,
message: authError.error.message,
},
});
const result = await handleGetPerson({ record_id: 'test-id' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('authentication_error');
});
it('should handle permission errors', async () => {
const permError = loadFixture('api-errors.json', 'authorization');
mockClient.mockResponse('GET', '/objects/people/records/test-id', {
error: {
statusCode: permError.error.status,
message: permError.error.message,
},
});
const result = await handleGetPerson({ record_id: 'test-id' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('permission_error');
});
it('should handle server errors with retryable flag', async () => {
const serverError = loadFixture('api-errors.json', 'server-error');
mockClient.mockResponse('GET', '/objects/people/records/test-id', {
error: {
statusCode: serverError.error.status,
message: serverError.error.message,
},
});
const result = await handleGetPerson({ record_id: 'test-id' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('server_error');
expect(parsed.error.retryable).toBe(true);
});
it('should include helpful suggestions for errors', async () => {
mockClient.mockResponse('GET', '/objects/people/records/test-id', {
error: {
statusCode: 404,
message: 'Not found',
},
});
const result = await handleGetPerson({ record_id: 'test-id' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.suggestions).toBeDefined();
expect(Array.isArray(parsed.error.suggestions)).toBe(true);
expect(parsed.error.suggestions.length).toBeGreaterThan(0);
});
});
describe('Response transformation', () => {
it('should filter out null email values', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'people',
record_id: 'person-123',
},
values: {
name: [{ first_name: 'Test', last_name: 'User', full_name: 'Test User' }],
email_addresses: [
{ email_address: 'test@example.com' },
{ email_address: null }, // Null email
{ email_address: 'valid@test.com' },
],
},
},
};
mockClient.mockResponse('GET', '/objects/people/records/person-123', {
data: mockResponse,
});
const result = await handleGetPerson({ record_id: 'person-123' });
const parsed = JSON.parse(result.content[0].text);
// Null values should be filtered out
expect(parsed.data.email_addresses).toEqual([
'test@example.com',
'valid@test.com',
]);
});
it('should handle missing optional fields gracefully', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'people',
record_id: 'person-123',
},
values: {
name: [
{
first_name: 'Bare',
last_name: 'Minimum',
full_name: 'Bare Minimum',
},
],
// No email_addresses, description, linkedin
},
},
};
mockClient.mockResponse('GET', '/objects/people/records/person-123', {
data: mockResponse,
});
const result = await handleGetPerson({ record_id: 'person-123' });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.data.name).toBe('Bare Minimum');
expect(parsed.data.email_addresses).toEqual([]);
expect(parsed.data.description).toBeNull();
expect(parsed.data.linkedin).toBeNull();
});
});
});