import { describe, it, expect } from 'vitest';
import { evolveSequence } from '../../src/tools/evolveSequence';
import { testSequences } from '../fixtures/sequences';
describe('evolveSequence Tool', () => {
describe('Basic functionality', () => {
it('should evolve DNA sequences over generations', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.short,
generations: 5,
populationSize: 10,
mutationRate: 0.01,
seed: 12345
});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
const data = JSON.parse(result.content[0].text);
expect(data.summary.totalGenerations).toBe(5);
expect(data.summary.populationSize).toBe(10);
expect(Array.isArray(data.evolutionHistory)).toBe(true);
});
it('should evolve protein sequences', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.protein.short,
generations: 3,
populationSize: 5,
mutationRate: 0.05,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.summary.finalBestSequence).toMatch(/^[ARNDCQEGHILKMFPSTWYV]+$/);
});
it('should use seed for reproducible results', async () => {
const seed = 54321;
const params = {
sequence: testSequences.dna.short,
generations: 3,
populationSize: 5,
mutationRate: 0.1,
seed
};
const result1 = await evolveSequence.handler(params);
const result2 = await evolveSequence.handler(params);
expect(result1.content[0].text).toBe(result2.content[0].text);
});
});
describe('Evolution tracking', () => {
it('should track fitness over generations', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.medium,
generations: 10,
populationSize: 20,
mutationRate: 0.05,
fitnessFunction: 'gc-content',
targetValue: 0.6,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.evolutionHistory).toHaveLength(11); // Initial + 10 generations
data.evolutionHistory.forEach((gen: any) => {
expect(gen.generation).toBeDefined();
expect(gen.bestFitness).toBeGreaterThanOrEqual(0);
expect(gen.worstFitness).toBeGreaterThanOrEqual(0);
expect(gen.averageFitness).toBeGreaterThanOrEqual(0);
expect(gen.bestFitness).toBeGreaterThanOrEqual(gen.worstFitness);
});
});
it('should track population diversity', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.short,
generations: 5,
populationSize: 10,
mutationRate: 0.1,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
data.evolutionHistory.forEach((gen: any) => {
expect(gen.diversity).toBeGreaterThanOrEqual(0);
expect(gen.diversity).toBeLessThanOrEqual(1);
});
});
});
describe('Selection pressure', () => {
it('should handle no selection pressure', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.short,
generations: 3,
populationSize: 8,
mutationRate: 0.05,
selectionPressure: 0,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.summary.selectionPressure).toBe(0);
});
it('should handle strong selection pressure', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.medium,
generations: 5,
populationSize: 20,
mutationRate: 0.05,
selectionPressure: 0.8,
fitnessFunction: 'gc-content',
targetValue: 0.5,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.summary.selectionPressure).toBe(0.8);
// With strong selection, fitness should generally improve or stay stable
const firstFitness = data.evolutionHistory[0].bestFitness;
const lastFitness = data.evolutionHistory[data.evolutionHistory.length - 1].bestFitness;
expect(lastFitness).toBeGreaterThanOrEqual(firstFitness * 0.8); // Allow some variance
});
});
describe('Fitness functions', () => {
it('should optimize GC content', async () => {
const targetGC = 0.7;
const result = await evolveSequence.handler({
sequence: testSequences.dna.medium,
generations: 20,
populationSize: 30,
mutationRate: 0.1,
selectionPressure: 0.5,
fitnessFunction: 'gc-content',
targetValue: targetGC,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
const finalSequence = data.summary.finalBestSequence;
const finalGC = (finalSequence.match(/[GC]/g) || []).length / finalSequence.length;
// Should be closer to target than initial
const initialGC = (testSequences.dna.medium.match(/[GC]/g) || []).length / testSequences.dna.medium.length;
const finalDistance = Math.abs(finalGC - targetGC);
const initialDistance = Math.abs(initialGC - targetGC);
expect(finalDistance).toBeLessThanOrEqual(initialDistance * 1.2); // Allow some tolerance
});
it('should optimize sequence length', async () => {
const targetLength = 100;
const result = await evolveSequence.handler({
sequence: testSequences.dna.short,
generations: 15,
populationSize: 20,
mutationRate: 0.1,
selectionPressure: 0.3,
fitnessFunction: 'length',
targetValue: targetLength,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.summary.fitnessFunction).toBe('length');
});
it('should optimize protein hydrophobicity', async () => {
const targetHydrophobic = 0.6;
const result = await evolveSequence.handler({
sequence: testSequences.protein.medium,
generations: 10,
populationSize: 15,
mutationRate: 0.05,
selectionPressure: 0.4,
fitnessFunction: 'hydrophobic',
targetValue: targetHydrophobic,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.summary.fitnessFunction).toBe('hydrophobic');
expect(data.summary.finalBestSequence).toMatch(/^[ARNDCQEGHILKMFPSTWYV]+$/);
});
});
describe('Lineage tracking', () => {
it('should track lineages when requested', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.short,
generations: 3,
populationSize: 5,
mutationRate: 0.1,
trackLineages: true,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.lineageData).toBeDefined();
expect(Array.isArray(data.lineageData)).toBe(true);
if (data.lineageData.length > 0) {
data.lineageData.forEach((lineage: any) => {
expect(lineage.generation).toBeDefined();
expect(lineage.lineageId).toBeDefined();
expect(lineage.sequence).toBeDefined();
expect(lineage.fitness).toBeDefined();
});
}
});
it('should not track lineages by default', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.short,
generations: 3,
populationSize: 5,
mutationRate: 0.1,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.lineageData).toBeUndefined();
});
});
describe('Output formats', () => {
it('should provide summary output by default', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.short,
generations: 3,
populationSize: 5,
mutationRate: 0.1,
outputFormat: 'summary'
});
const data = JSON.parse(result.content[0].text);
expect(data.summary).toBeDefined();
expect(data.evolutionHistory.length).toBeLessThanOrEqual(10); // Should be sampled
});
it('should provide detailed output when requested', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.short,
generations: 5,
populationSize: 5,
mutationRate: 0.1,
outputFormat: 'detailed'
});
const data = JSON.parse(result.content[0].text);
expect(data.evolutionHistory).toHaveLength(6); // All generations
expect(data.finalPopulation).toBeDefined();
});
it('should provide FASTA output when requested', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.short,
generations: 3,
populationSize: 5,
mutationRate: 0.1,
outputFormat: 'fasta'
});
const data = JSON.parse(result.content[0].text);
expect(data.fastaOutput).toBeDefined();
expect(data.fastaOutput).toMatch(/^>evolved_seq_1/);
});
});
describe('Statistical analysis', () => {
it('should calculate improvement ratio', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.medium,
generations: 10,
populationSize: 20,
mutationRate: 0.05,
selectionPressure: 0.3,
fitnessFunction: 'gc-content',
targetValue: 0.5,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.summary.improvementRatio).toBeGreaterThan(0);
expect(typeof data.summary.improvementRatio).toBe('number');
});
it('should maintain sequence validity throughout evolution', async () => {
const result = await evolveSequence.handler({
sequence: testSequences.dna.medium,
generations: 10,
populationSize: 15,
mutationRate: 0.2, // High mutation rate
selectionPressure: 0.5,
outputFormat: 'detailed',
seed: 12345
});
const data = JSON.parse(result.content[0].text);
// Check that all sequences in evolution history are valid
data.evolutionHistory.forEach((gen: any) => {
expect(gen.bestSequence).toMatch(/^[ATGC]+$/);
expect(gen.bestSequence.length).toBeGreaterThan(0);
});
// Check final population
if (data.finalPopulation) {
data.finalPopulation.forEach((individual: any) => {
expect(individual.sequence).toMatch(/^[ATGC]+$/);
});
}
});
});
describe('Tool definition', () => {
it('should have correct tool definition structure', () => {
expect(evolveSequence.definition.name).toBe('evolve_sequence');
expect(evolveSequence.definition.description).toContain('Simulate evolution');
expect(evolveSequence.definition.inputSchema.type).toBe('object');
expect(evolveSequence.definition.inputSchema.required).toEqual([
'sequence', 'generations', 'populationSize', 'mutationRate'
]);
});
it('should have proper parameter definitions', () => {
const props = evolveSequence.definition.inputSchema.properties;
expect(props.sequence.type).toBe('string');
expect(props.generations.minimum).toBe(1);
expect(props.populationSize.minimum).toBe(2);
expect(props.mutationRate.minimum).toBe(0);
expect(props.mutationRate.maximum).toBe(1);
expect(props.selectionPressure.minimum).toBe(0);
expect(props.selectionPressure.maximum).toBe(1);
expect(props.fitnessFunction.enum).toEqual(['gc-content', 'length', 'hydrophobic', 'custom']);
expect(props.outputFormat.enum).toEqual(['summary', 'detailed', 'fasta']);
});
});
});