import { describe, it, expect } from 'vitest';
import { generateProtein } from '../../src/tools/generateProtein';
import { AMINO_ACIDS } from '../../src/utils/sequenceUtils';
describe('generateProtein Tool', () => {
describe('Basic functionality', () => {
it('should generate protein sequence of specified length', async () => {
const result = await generateProtein.handler({
length: 100
});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
const data = JSON.parse(result.content[0].text);
expect(data.statistics.totalSequences).toBe(1);
expect(data.statistics.averageLength).toBe(100);
});
it('should generate multiple sequences', async () => {
const result = await generateProtein.handler({
length: 50,
count: 3
});
const data = JSON.parse(result.content[0].text);
expect(data.statistics.totalSequences).toBe(3);
});
it('should use seed for reproducible results', async () => {
const seed = 98765;
const result1 = await generateProtein.handler({
length: 100,
seed
});
const result2 = await generateProtein.handler({
length: 100,
seed
});
expect(result1.content[0].text).toBe(result2.content[0].text);
});
});
describe('Generation models', () => {
it('should support random model', async () => {
const result = await generateProtein.handler({
length: 100,
model: 'random',
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.statistics.model).toBe('random');
// Should contain valid amino acids
if (Array.isArray(data.sequences)) {
const sequence = data.sequences[0].sequence;
expect(sequence).toMatch(new RegExp(`^[${AMINO_ACIDS.join('')}]+$`));
}
});
it('should support hydrophobic-bias model', async () => {
const result = await generateProtein.handler({
length: 200,
model: 'hydrophobic-bias',
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.statistics.model).toBe('hydrophobic-bias');
expect(data.statistics.averageHydrophobic).toBeGreaterThan(40); // Should be biased towards hydrophobic
});
it('should support disorder-prone model', async () => {
const result = await generateProtein.handler({
length: 200,
model: 'disorder-prone',
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.statistics.model).toBe('disorder-prone');
// Should contain disorder-prone amino acids
if (Array.isArray(data.sequences)) {
const sequence = data.sequences[0].sequence;
const disorderProne = ['A', 'R', 'G', 'Q', 'S', 'P', 'E', 'K'];
const disorderCount = sequence.split('').filter((aa: string) => disorderProne.includes(aa)).length;
expect(disorderCount / sequence.length).toBeGreaterThan(0.3);
}
});
});
describe('Custom composition', () => {
it('should respect custom amino acid composition', async () => {
const composition = {
'A': 0.5, // 50% Alanine
'G': 0.3, // 30% Glycine
'P': 0.2 // 20% Proline
};
const result = await generateProtein.handler({
length: 1000,
composition,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
if (Array.isArray(data.sequences)) {
const sequence = data.sequences[0].sequence;
const aCount = (sequence.match(/A/g) || []).length;
const gCount = (sequence.match(/G/g) || []).length;
const pCount = (sequence.match(/P/g) || []).length;
// Should be close to expected ratios (with some tolerance)
expect(aCount / sequence.length).toBeCloseTo(0.5, 1);
expect(gCount / sequence.length).toBeCloseTo(0.3, 1);
expect(pCount / sequence.length).toBeCloseTo(0.2, 1);
// Should only contain specified amino acids
expect(sequence).toMatch(/^[AGP]+$/);
}
});
});
describe('Output formats', () => {
it('should output FASTA format by default', async () => {
const result = await generateProtein.handler({
length: 50,
outputFormat: 'fasta'
});
const data = JSON.parse(result.content[0].text);
expect(typeof data.sequences).toBe('string');
expect(data.sequences).toMatch(/^>sim_protein_1/);
expect(data.sequences).toMatch(new RegExp(`\\n[${AMINO_ACIDS.join('')}]+$`));
});
it('should output plain format when requested', async () => {
const result = await generateProtein.handler({
length: 50,
outputFormat: 'plain'
});
const data = JSON.parse(result.content[0].text);
expect(data.rawOutput).toMatch(new RegExp(`^[${AMINO_ACIDS.join('')}]+$`));
});
});
describe('Biochemical analysis', () => {
it('should calculate hydrophobic ratio correctly', async () => {
const result = await generateProtein.handler({
length: 100,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
if (Array.isArray(data.sequences)) {
const seqData = data.sequences[0];
const hydrophobic = new Set(['A', 'V', 'I', 'L', 'M', 'F', 'Y', 'W']);
const sequence = seqData.sequence;
const hydrophobicCount = sequence.split('').filter((aa: string) => hydrophobic.has(aa)).length;
const expectedRatio = Math.round((hydrophobicCount / sequence.length) * 10000) / 100;
expect(seqData.hydrophobicRatio).toBe(expectedRatio);
}
});
it('should calculate charged ratio correctly', async () => {
const result = await generateProtein.handler({
length: 100,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
if (Array.isArray(data.sequences)) {
const seqData = data.sequences[0];
const charged = new Set(['R', 'K', 'D', 'E']);
const sequence = seqData.sequence;
const chargedCount = sequence.split('').filter((aa: string) => charged.has(aa)).length;
const expectedRatio = Math.round((chargedCount / sequence.length) * 10000) / 100;
expect(seqData.chargedRatio).toBe(expectedRatio);
}
});
it('should calculate aromatic ratio correctly', async () => {
const result = await generateProtein.handler({
length: 100,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
if (Array.isArray(data.sequences)) {
const seqData = data.sequences[0];
const aromatic = new Set(['F', 'Y', 'W']);
const sequence = seqData.sequence;
const aromaticCount = sequence.split('').filter((aa: string) => aromatic.has(aa)).length;
const expectedRatio = Math.round((aromaticCount / sequence.length) * 10000) / 100;
expect(seqData.aromaticRatio).toBe(expectedRatio);
}
});
it('should provide amino acid composition', async () => {
const result = await generateProtein.handler({
length: 100,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
if (Array.isArray(data.sequences)) {
const seqData = data.sequences[0];
const composition = seqData.composition;
expect(typeof composition).toBe('object');
// Sum of all compositions should be 100%
const totalPercentage = Object.values(composition).reduce((sum: number, val: unknown) => sum + (val as number), 0);
expect(totalPercentage).toBeCloseTo(100, 1);
// Each amino acid should have a reasonable percentage
Object.values(composition).forEach((percentage: unknown) => {
expect(percentage as number).toBeGreaterThanOrEqual(0);
expect(percentage as number).toBeLessThanOrEqual(100);
});
}
});
});
describe('Input validation', () => {
it('should handle minimum length', async () => {
const result = await generateProtein.handler({
length: 1
});
const data = JSON.parse(result.content[0].text);
expect(data.statistics.averageLength).toBe(1);
});
it('should handle large sequences', async () => {
const result = await generateProtein.handler({
length: 5000,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.statistics.averageLength).toBe(5000);
});
});
describe('Tool definition', () => {
it('should have correct tool definition structure', () => {
expect(generateProtein.definition.name).toBe('generate_protein_sequence');
expect(generateProtein.definition.description).toContain('Generate random protein sequences');
expect(generateProtein.definition.inputSchema.type).toBe('object');
expect(generateProtein.definition.inputSchema.required).toContain('length');
});
it('should have proper parameter definitions', () => {
const props = generateProtein.definition.inputSchema.properties;
expect(props.length.type).toBe('number');
expect(props.count.minimum).toBe(1);
expect(props.model.enum).toEqual(['random', 'hydrophobic-bias', 'disorder-prone']);
expect(props.outputFormat.enum).toEqual(['fasta', 'plain']);
expect(props.composition.type).toBe('object');
});
});
});