import { describe, it, expect } from 'vitest';
import { simulatePhylogeny } from '../../src/tools/simulatePhylogeny';
import { testSequences, testTrees } from '../fixtures/sequences';
describe('simulatePhylogeny Tool', () => {
describe('Basic functionality', () => {
it('should simulate phylogeny with random tree', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 4,
seed: 12345
});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
const data = JSON.parse(result.content[0].text);
expect(data.parameters.numTaxa).toBe(4);
expect(data.treeStatistics.numTaxa).toBe(4);
expect(typeof data.newickTree).toBe('string');
});
it('should simulate phylogeny with provided tree', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
treeStructure: testTrees.balanced,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.treeStatistics.numTaxa).toBe(4); // From balanced tree
expect(data.newickTree).toBeDefined();
});
it('should use seed for reproducible results', async () => {
const seed = 98765;
const params = {
rootSequence: testSequences.dna.short,
numTaxa: 3,
seed
};
const result1 = await simulatePhylogeny.handler(params);
const result2 = await simulatePhylogeny.handler(params);
const data1 = JSON.parse(result1.content[0].text);
const data2 = JSON.parse(result2.content[0].text);
// Check that key properties are the same (tree structure may vary slightly due to random generation)
expect(data1.parameters.seed).toBe(data2.parameters.seed);
expect(data1.treeStatistics.numTaxa).toBe(data2.treeStatistics.numTaxa);
expect(Object.keys(data1.sequences).length).toBe(Object.keys(data2.sequences).length);
});
});
describe('Tree generation and parsing', () => {
it('should generate random trees with correct number of taxa', async () => {
const taxaCounts = [3, 5, 8];
for (const numTaxa of taxaCounts) {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
numTaxa,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.treeStatistics.numTaxa).toBe(numTaxa);
expect(Object.keys(data.sequences)).toHaveLength(numTaxa);
}
});
it('should parse Newick format trees correctly', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
treeStructure: testTrees.withBranchLengths,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.treeStatistics.numTaxa).toBe(4);
expect(data.treeStatistics.totalBranchLength).toBeGreaterThan(0);
});
});
describe('Substitution models', () => {
it('should support JC69 model', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 4,
substitutionModel: 'JC69',
mutationRate: 0.1,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.parameters.substitutionModel).toBe('JC69');
});
it('should support K80 model', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 4,
substitutionModel: 'K80',
mutationRate: 0.1,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.parameters.substitutionModel).toBe('K80');
});
it('should support HKY85 model', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 4,
substitutionModel: 'HKY85',
mutationRate: 0.1,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.parameters.substitutionModel).toBe('HKY85');
});
it('should support GTR model', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 4,
substitutionModel: 'GTR',
mutationRate: 0.1,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.parameters.substitutionModel).toBe('GTR');
});
});
describe('Molecular clock', () => {
it('should support molecular clock', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
numTaxa: 4,
molecularClock: true,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.parameters.molecularClock).toBe(true);
});
it('should support relaxed molecular clock', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
numTaxa: 4,
molecularClock: false,
branchLengthVariation: 0.3,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.parameters.molecularClock).toBe(false);
});
});
describe('Sequence evolution', () => {
it('should evolve DNA sequences on tree', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 4,
mutationRate: 0.2,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
Object.values(data.sequences).forEach((sequence: any) => {
expect(typeof sequence).toBe('string');
expect(sequence).toMatch(/^[ATGC]+$/);
expect(sequence.length).toBeGreaterThan(0);
});
});
it('should evolve protein sequences on tree', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.protein.medium,
numTaxa: 3,
mutationRate: 0.1,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
Object.values(data.sequences).forEach((sequence: any) => {
expect(typeof sequence).toBe('string');
expect(sequence).toMatch(/^[ARNDCQEGHILKMFPSTWYV]+$/);
});
});
it('should show sequence divergence with mutation', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 4,
mutationRate: 0.3, // High mutation rate
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.sequenceStatistics.avgDivergenceFromRoot).toBeGreaterThan(0);
});
});
describe('Output formats', () => {
it('should output FASTA format by default', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
numTaxa: 3,
outputFormat: 'fasta',
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(typeof data.formattedOutput).toBe('string');
expect(data.formattedOutput).toMatch(/^>Taxon_/); // Any taxon name is fine
expect(data.formattedOutput).toMatch(/\n[ATGC]+/);
expect(data.formattedOutput.split('>').length - 1).toBe(3); // Should have 3 sequences
});
it('should output NEXUS format when requested', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
numTaxa: 3,
outputFormat: 'nexus',
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.formattedOutput).toContain('#NEXUS');
expect(data.formattedOutput).toContain('BEGIN DATA;');
expect(data.formattedOutput).toContain('BEGIN TREES;');
});
it('should output PHYLIP format when requested', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
numTaxa: 3,
outputFormat: 'phylip',
seed: 12345
});
const data = JSON.parse(result.content[0].text);
const lines = data.formattedOutput.split('\n');
expect(lines[0]).toMatch(/^\d+ \d+$/); // First line should be "numTaxa seqLength"
});
});
describe('Statistics and analysis', () => {
it('should calculate tree statistics correctly', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 5,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
const stats = data.treeStatistics;
expect(stats.numTaxa).toBe(5);
expect(stats.totalBranchLength).toBeGreaterThan(0);
expect(stats.maxDepth).toBeGreaterThan(0);
expect(stats.avgBranchLength).toBeGreaterThan(0);
expect(typeof stats.totalBranchLength).toBe('number');
});
it('should calculate sequence divergence statistics', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 4,
mutationRate: 0.1,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
const seqStats = data.sequenceStatistics;
expect(seqStats.avgDivergenceFromRoot).toBeGreaterThanOrEqual(0);
expect(seqStats.maxDivergenceFromRoot).toBeGreaterThanOrEqual(seqStats.avgDivergenceFromRoot);
expect(seqStats.minDivergenceFromRoot).toBeLessThanOrEqual(seqStats.avgDivergenceFromRoot);
expect(seqStats.maxDivergenceFromRoot).toBeLessThanOrEqual(100);
});
});
describe('Newick tree output', () => {
it('should generate valid Newick format', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
numTaxa: 4,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
const newick = data.newickTree;
expect(typeof newick).toBe('string');
expect(newick).toContain('(');
expect(newick).toContain(')');
expect(newick).toContain(':');
// Should have correct number of taxa names
const taxonMatches = newick.match(/Taxon_\d+/g);
expect(taxonMatches).toHaveLength(4);
});
});
describe('Input validation', () => {
it('should handle minimum number of taxa', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
numTaxa: 2,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.treeStatistics.numTaxa).toBe(2);
});
it('should handle large trees', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.short,
numTaxa: 10,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.treeStatistics.numTaxa).toBe(10);
expect(Object.keys(data.sequences)).toHaveLength(10);
});
it('should handle high mutation rates', async () => {
const result = await simulatePhylogeny.handler({
rootSequence: testSequences.dna.medium,
numTaxa: 3,
mutationRate: 0.8,
seed: 12345
});
const data = JSON.parse(result.content[0].text);
expect(data.sequenceStatistics.avgDivergenceFromRoot).toBeGreaterThan(0);
});
});
describe('Tool definition', () => {
it('should have correct tool definition structure', () => {
expect(simulatePhylogeny.definition.name).toBe('simulate_phylogeny');
expect(simulatePhylogeny.definition.description).toContain('Simulate phylogenetic tree');
expect(simulatePhylogeny.definition.inputSchema.type).toBe('object');
expect(simulatePhylogeny.definition.inputSchema.required).toContain('rootSequence');
});
it('should have proper parameter definitions', () => {
const props = simulatePhylogeny.definition.inputSchema.properties;
expect(props.rootSequence.type).toBe('string');
expect(props.numTaxa.minimum).toBe(2);
expect(props.mutationRate.minimum).toBe(0);
expect(props.branchLengthVariation.minimum).toBe(0);
expect(props.branchLengthVariation.maximum).toBe(1);
expect(props.substitutionModel.enum).toEqual(['JC69', 'K80', 'HKY85', 'GTR']);
expect(props.outputFormat.enum).toEqual(['fasta', 'nexus', 'phylip']);
});
});
});