UNPKG

@aj-archipelago/cortex

Version:

Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.

766 lines (626 loc) 26 kB
import test from 'ava'; import sinon from 'sinon'; import PathwayManager from '../../../lib/pathwayManager.js'; import { Prompt } from '../../../server/prompt.js'; // Mock config const mockConfig = { storageType: 'local', filePath: '/tmp/test-pathways.json', publishKey: 'test-key' }; test.beforeEach(t => { t.context.pathwayManager = new PathwayManager(mockConfig); }); test('transformPrompts handles array of strings (legacy format)', async t => { const pathway = { prompt: [ 'First prompt text', 'Second prompt text' ], systemPrompt: 'You are a helpful assistant' }; const result = await t.context.pathwayManager.transformPrompts(pathway); t.is(result.prompt.length, 2); t.true(result.prompt[0] instanceof Prompt); t.true(result.prompt[1] instanceof Prompt); // Check that the prompt text is correctly embedded t.is(result.prompt[0].messages[1].content, '{{text}}\n\nFirst prompt text'); t.is(result.prompt[1].messages[1].content, '{{text}}\n\nSecond prompt text'); // Check system prompt is included t.is(result.prompt[0].messages[0].content, 'You are a helpful assistant'); t.is(result.prompt[1].messages[0].content, 'You are a helpful assistant'); // Names should be null for legacy format t.is(result.prompt[0].name, null); t.is(result.prompt[1].name, null); }); test('transformPrompts handles array of objects (new format)', async t => { const pathway = { prompt: [ { name: 'First Prompt', prompt: 'First prompt text' }, { name: 'Second Prompt', prompt: 'Second prompt text' } ], systemPrompt: 'You are a helpful assistant' }; const result = await t.context.pathwayManager.transformPrompts(pathway); t.is(result.prompt.length, 2); t.true(result.prompt[0] instanceof Prompt); t.true(result.prompt[1] instanceof Prompt); // Check that the prompt text is correctly embedded t.is(result.prompt[0].messages[1].content, '{{text}}\n\nFirst prompt text'); t.is(result.prompt[1].messages[1].content, '{{text}}\n\nSecond prompt text'); // Check system prompt is included t.is(result.prompt[0].messages[0].content, 'You are a helpful assistant'); t.is(result.prompt[1].messages[0].content, 'You are a helpful assistant'); // Names should be preserved for new format t.is(result.prompt[0].name, 'First Prompt'); t.is(result.prompt[1].name, 'Second Prompt'); }); test('transformPrompts handles mixed format arrays', async t => { const pathway = { prompt: [ 'Legacy prompt text', { name: 'Named Prompt', prompt: 'Named prompt text' } ], systemPrompt: 'You are a helpful assistant' }; const result = await t.context.pathwayManager.transformPrompts(pathway); t.is(result.prompt.length, 2); // First prompt (legacy format) t.is(result.prompt[0].messages[1].content, '{{text}}\n\nLegacy prompt text'); t.is(result.prompt[0].name, null); // Second prompt (new format) t.is(result.prompt[1].messages[1].content, '{{text}}\n\nNamed prompt text'); t.is(result.prompt[1].name, 'Named Prompt'); }); test('transformPrompts preserves other pathway properties', async t => { const pathway = { prompt: [{ name: 'Test', prompt: 'Test prompt' }], systemPrompt: 'System prompt', name: 'Test Pathway', model: 'gpt-4', otherProperty: 'value' }; const result = await t.context.pathwayManager.transformPrompts(pathway); t.is(result.name, 'Test Pathway'); t.is(result.model, 'gpt-4'); t.is(result.otherProperty, 'value'); t.is(result.systemPrompt, 'System prompt'); }); test('transformPrompts handles empty prompt array', async t => { const pathway = { prompt: [], systemPrompt: 'You are a helpful assistant' }; const result = await t.context.pathwayManager.transformPrompts(pathway); t.is(result.prompt.length, 0); t.true(Array.isArray(result.prompt)); }); test('_createPromptObject handles string prompt with default name', t => { const promptItem = 'Test prompt text'; const systemPrompt = 'You are a helpful assistant'; const defaultName = 'test_prompt'; const result = t.context.pathwayManager._createPromptObject(promptItem, systemPrompt, defaultName); t.true(result instanceof Prompt); t.is(result.name, 'test_prompt'); t.is(result.messages.length, 2); t.is(result.messages[0].role, 'system'); t.is(result.messages[0].content, 'You are a helpful assistant'); t.is(result.messages[1].role, 'user'); t.is(result.messages[1].content, '{{text}}\n\nTest prompt text'); }); test('_createPromptObject handles string prompt without default name', t => { const promptItem = 'Test prompt text'; const systemPrompt = 'You are a helpful assistant'; const result = t.context.pathwayManager._createPromptObject(promptItem, systemPrompt); t.true(result instanceof Prompt); t.is(result.name, null); t.is(result.messages.length, 2); t.is(result.messages[0].role, 'system'); t.is(result.messages[0].content, 'You are a helpful assistant'); t.is(result.messages[1].role, 'user'); t.is(result.messages[1].content, '{{text}}\n\nTest prompt text'); }); test('_createPromptObject handles object prompt with name', t => { const promptItem = { name: 'Custom Prompt', prompt: 'Test prompt text' }; const systemPrompt = 'You are a helpful assistant'; const defaultName = 'fallback_name'; const result = t.context.pathwayManager._createPromptObject(promptItem, systemPrompt, defaultName); t.true(result instanceof Prompt); t.is(result.name, 'Custom Prompt'); t.is(result.messages.length, 2); t.is(result.messages[0].role, 'system'); t.is(result.messages[0].content, 'You are a helpful assistant'); t.is(result.messages[1].role, 'user'); t.is(result.messages[1].content, '{{text}}\n\nTest prompt text'); }); test('_createPromptObject handles object prompt without name', t => { const promptItem = { prompt: 'Test prompt text' }; const systemPrompt = 'You are a helpful assistant'; const defaultName = 'fallback_name'; const result = t.context.pathwayManager._createPromptObject(promptItem, systemPrompt, defaultName); t.true(result instanceof Prompt); t.is(result.name, 'fallback_name'); // Uses defaultName when promptItem.name is undefined t.is(result.messages.length, 2); t.is(result.messages[0].role, 'system'); t.is(result.messages[0].content, 'You are a helpful assistant'); t.is(result.messages[1].role, 'user'); t.is(result.messages[1].content, '{{text}}\n\nTest prompt text'); }); test('_createPromptObject handles empty system prompt', t => { const promptItem = 'Test prompt text'; const systemPrompt = ''; const defaultName = 'test_prompt'; const result = t.context.pathwayManager._createPromptObject(promptItem, systemPrompt, defaultName); t.true(result instanceof Prompt); t.is(result.name, 'test_prompt'); t.is(result.messages[0].content, '{{text}}\n\nTest prompt text'); }); test('_createPromptObject handles null system prompt', t => { const promptItem = 'Test prompt text'; const systemPrompt = null; const defaultName = 'test_prompt'; const result = t.context.pathwayManager._createPromptObject(promptItem, systemPrompt, defaultName); t.true(result instanceof Prompt); t.is(result.name, 'test_prompt'); t.is(result.messages[0].content, '{{text}}\n\nTest prompt text'); }); test('putPathway requires userId and secret', async t => { const pathway = { prompt: ['test'] }; // Missing both await t.throwsAsync( () => t.context.pathwayManager.putPathway('test', pathway), { message: 'Both userId and secret are mandatory for adding or updating a pathway' } ); // Missing secret await t.throwsAsync( () => t.context.pathwayManager.putPathway('test', pathway, 'user123'), { message: 'Both userId and secret are mandatory for adding or updating a pathway' } ); // Missing userId await t.throwsAsync( () => t.context.pathwayManager.putPathway('test', pathway, null, 'secret123'), { message: 'Both userId and secret are mandatory for adding or updating a pathway' } ); }); test('putPathway stores pathway with correct format', async t => { // Mock the storage and loading methods const mockPathways = {}; t.context.pathwayManager.pathways = mockPathways; t.context.pathwayManager.getLatestPathways = sinon.stub().resolves(mockPathways); t.context.pathwayManager.savePathways = sinon.stub().resolves(); t.context.pathwayManager.loadPathways = sinon.stub().resolves(); const pathway = { prompt: [ { name: 'Test Prompt', prompt: 'Test prompt text' } ], systemPrompt: 'System prompt', model: 'gpt-4' }; const result = await t.context.pathwayManager.putPathway( 'testPathway', pathway, 'user123', 'secret123', 'Test Pathway Display' ); t.is(result, 'testPathway'); t.truthy(mockPathways['user123']); t.truthy(mockPathways['user123']['testPathway']); const storedPathway = mockPathways['user123']['testPathway']; t.is(storedPathway.secret, 'secret123'); t.is(storedPathway.displayName, 'Test Pathway Display'); t.deepEqual(storedPathway.prompt, pathway.prompt); t.is(storedPathway.systemPrompt, 'System prompt'); t.is(storedPathway.model, 'gpt-4'); }); test('getPathways returns array of pathways for each prompt (string format)', async t => { const pathwayTemplate = { prompt: [ 'First prompt text', 'Second prompt text', 'Third prompt text' ], systemPrompt: 'You are a helpful assistant', model: 'gpt-4', enableCache: true, customProperty: 'test-value' }; const result = await t.context.pathwayManager.getPathways(pathwayTemplate); t.is(result.length, 3); // Check each pathway has the correct structure result.forEach((pathway, index) => { t.is(pathway.systemPrompt, 'You are a helpful assistant'); t.is(pathway.model, 'gpt-4'); t.is(pathway.enableCache, true); t.is(pathway.customProperty, 'test-value'); t.is(pathway.prompt.length, 1); t.true(pathway.prompt[0] instanceof Prompt); // Check the prompt content const expectedContent = `{{text}}\n\n${pathwayTemplate.prompt[index]}`; t.is(pathway.prompt[0].messages[1].content, expectedContent); t.is(pathway.prompt[0].messages[0].content, 'You are a helpful assistant'); t.is(pathway.prompt[0].name, `prompt_${index}`); }); }); test('getPathways returns array of pathways for each prompt (object format)', async t => { const pathwayTemplate = { prompt: [ { name: 'Grammar Check', prompt: 'Check the grammar of this text' }, { name: 'Tone Analysis', prompt: 'Analyze the tone of this text' }, { name: 'Summary', prompt: 'Summarize this text' } ], systemPrompt: 'You are an expert editor', model: 'gpt-4-turbo' }; const result = await t.context.pathwayManager.getPathways(pathwayTemplate); t.is(result.length, 3); // Check each pathway has the correct structure and names const expectedNames = ['Grammar Check', 'Tone Analysis', 'Summary']; const expectedPrompts = [ 'Check the grammar of this text', 'Analyze the tone of this text', 'Summarize this text' ]; result.forEach((pathway, index) => { t.is(pathway.systemPrompt, 'You are an expert editor'); t.is(pathway.model, 'gpt-4-turbo'); t.is(pathway.prompt.length, 1); t.true(pathway.prompt[0] instanceof Prompt); // Check the prompt content and name const expectedContent = `{{text}}\n\n${expectedPrompts[index]}`; t.is(pathway.prompt[0].messages[1].content, expectedContent); t.is(pathway.prompt[0].messages[0].content, 'You are an expert editor'); t.is(pathway.prompt[0].name, expectedNames[index]); }); }); test('getPathways handles mixed format prompt arrays', async t => { const pathwayTemplate = { prompt: [ 'Legacy string prompt', { name: 'Named Prompt', prompt: 'Named prompt text' }, { prompt: 'Unnamed object prompt' } ], systemPrompt: 'Mixed format system' }; const result = await t.context.pathwayManager.getPathways(pathwayTemplate); t.is(result.length, 3); // First pathway (string format) t.is(result[0].prompt[0].messages[1].content, '{{text}}\n\nLegacy string prompt'); t.is(result[0].prompt[0].name, 'prompt_0'); // Second pathway (named object) t.is(result[1].prompt[0].messages[1].content, '{{text}}\n\nNamed prompt text'); t.is(result[1].prompt[0].name, 'Named Prompt'); // Third pathway (unnamed object) t.is(result[2].prompt[0].messages[1].content, '{{text}}\n\nUnnamed object prompt'); t.is(result[2].prompt[0].name, 'prompt_2'); }); test('getPathways throws error for non-array prompt', async t => { const pathwayTemplate = { prompt: 'This should be an array', systemPrompt: 'Test system prompt' }; await t.throwsAsync( () => t.context.pathwayManager.getPathways(pathwayTemplate), { message: 'pathwayTemplate.prompt must be an array' } ); }); test('getPathways handles empty prompt array', async t => { const pathwayTemplate = { prompt: [], systemPrompt: 'Empty array test' }; const result = await t.context.pathwayManager.getPathways(pathwayTemplate); t.is(result.length, 0); t.true(Array.isArray(result)); }); test('getPathways preserves all template properties', async t => { const pathwayTemplate = { prompt: [ { name: 'Test Prompt', prompt: 'Test content' } ], systemPrompt: 'System test', model: 'gpt-4', enableCache: false, inputParameters: { temperature: 0.7 }, customProperty: 'preserved', displayName: 'Test Display Name' }; const result = await t.context.pathwayManager.getPathways(pathwayTemplate); t.is(result.length, 1); const pathway = result[0]; t.is(pathway.systemPrompt, 'System test'); t.is(pathway.model, 'gpt-4'); t.is(pathway.enableCache, false); t.deepEqual(pathway.inputParameters, { temperature: 0.7 }); t.is(pathway.customProperty, 'preserved'); t.is(pathway.displayName, 'Test Display Name'); }); test('getPathways filters by promptNames (object format)', async t => { const pathwayTemplate = { prompt: [ { name: 'Grammar Check', prompt: 'Check the grammar of this text' }, { name: 'Tone Analysis', prompt: 'Analyze the tone of this text' }, { name: 'Summary', prompt: 'Summarize this text' }, { name: 'Translation', prompt: 'Translate this text' } ], systemPrompt: 'You are an expert editor' }; const result = await t.context.pathwayManager.getPathways( pathwayTemplate, ['Grammar Check', 'Summary'] ); t.is(result.length, 2); // Check that only the requested prompts are included t.is(result[0].prompt[0].name, 'Grammar Check'); t.is(result[0].prompt[0].messages[1].content, '{{text}}\n\nCheck the grammar of this text'); t.is(result[1].prompt[0].name, 'Summary'); t.is(result[1].prompt[0].messages[1].content, '{{text}}\n\nSummarize this text'); // Ensure no _originalPromptName property remains t.false('_originalPromptName' in result[0]); t.false('_originalPromptName' in result[1]); }); test('getPathways filters by promptNames (string format)', async t => { const pathwayTemplate = { prompt: [ 'First prompt text', 'Second prompt text', 'Third prompt text' ], systemPrompt: 'You are a helpful assistant' }; const result = await t.context.pathwayManager.getPathways( pathwayTemplate, ['prompt_0', 'prompt_2'] ); t.is(result.length, 2); // Check that only the requested prompts are included t.is(result[0].prompt[0].name, 'prompt_0'); t.is(result[0].prompt[0].messages[1].content, '{{text}}\n\nFirst prompt text'); t.is(result[1].prompt[0].name, 'prompt_2'); t.is(result[1].prompt[0].messages[1].content, '{{text}}\n\nThird prompt text'); }); test('getPathways filters by promptNames (mixed format)', async t => { const pathwayTemplate = { prompt: [ 'Legacy string prompt', { name: 'Named Prompt', prompt: 'Named prompt text' }, { prompt: 'Unnamed object prompt' } ], systemPrompt: 'Mixed format system' }; const result = await t.context.pathwayManager.getPathways( pathwayTemplate, ['prompt_0', 'Named Prompt'] ); t.is(result.length, 2); // Check first filtered result (string format) t.is(result[0].prompt[0].name, 'prompt_0'); t.is(result[0].prompt[0].messages[1].content, '{{text}}\n\nLegacy string prompt'); // Check second filtered result (named object) t.is(result[1].prompt[0].name, 'Named Prompt'); t.is(result[1].prompt[0].messages[1].content, '{{text}}\n\nNamed prompt text'); }); test('getPathways returns empty array when no promptNames match', async t => { const pathwayTemplate = { prompt: [ { name: 'Grammar Check', prompt: 'Check grammar' }, { name: 'Tone Analysis', prompt: 'Analyze tone' } ], systemPrompt: 'System prompt' }; const result = await t.context.pathwayManager.getPathways( pathwayTemplate, ['Non-existent Prompt', 'Another Missing Prompt'] ); t.is(result.length, 0); t.true(Array.isArray(result)); }); test('getPathways returns all pathways when promptNames is empty array', async t => { const pathwayTemplate = { prompt: [ { name: 'First', prompt: 'First prompt' }, { name: 'Second', prompt: 'Second prompt' } ], systemPrompt: 'System prompt' }; const result = await t.context.pathwayManager.getPathways( pathwayTemplate, [] ); t.is(result.length, 2); }); test('getPathways returns all pathways when promptNames is null', async t => { const pathwayTemplate = { prompt: [ { name: 'First', prompt: 'First prompt' }, { name: 'Second', prompt: 'Second prompt' } ], systemPrompt: 'System prompt' }; const result = await t.context.pathwayManager.getPathways(pathwayTemplate, null); t.is(result.length, 2); }); test('isLegacyPromptFormat identifies legacy format (array of strings)', t => { const userId = 'testUser'; const pathwayName = 'testPathway'; // Set up pathway with legacy prompts t.context.pathwayManager.pathways = { [userId]: { [pathwayName]: { prompt: [ 'First prompt text', 'Second prompt text', 'Third prompt text' ] } } }; const result = t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); t.true(result); }); test('isLegacyPromptFormat identifies new format (array of objects)', t => { const userId = 'testUser'; const pathwayName = 'testPathway'; // Set up pathway with new format prompts t.context.pathwayManager.pathways = { [userId]: { [pathwayName]: { prompt: [ { name: 'First Prompt', prompt: 'First prompt text' }, { name: 'Second Prompt', prompt: 'Second prompt text' }, { prompt: 'Third prompt text' } // name is optional ] } } }; const result = t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); t.false(result); }); test('isLegacyPromptFormat handles empty array (defaults to new format)', t => { const userId = 'testUser'; const pathwayName = 'testPathway'; // Set up pathway with empty prompts array t.context.pathwayManager.pathways = { [userId]: { [pathwayName]: { prompt: [] } } }; const result = t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); t.false(result); }); test('isLegacyPromptFormat handles mixed format (treats as legacy)', t => { const userId = 'testUser'; const pathwayName = 'testPathway'; // Set up pathway with mixed format prompts t.context.pathwayManager.pathways = { [userId]: { [pathwayName]: { prompt: [ 'Legacy string prompt', { name: 'New format prompt', prompt: 'New format text' } ] } } }; const result = t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); t.true(result); }); test('isLegacyPromptFormat throws error for invalid parameters', t => { t.throws(() => { t.context.pathwayManager.isLegacyPromptFormat('', 'pathwayName'); }, { message: 'userId must be a non-empty string' }); t.throws(() => { t.context.pathwayManager.isLegacyPromptFormat(null, 'pathwayName'); }, { message: 'userId must be a non-empty string' }); t.throws(() => { t.context.pathwayManager.isLegacyPromptFormat('userId', ''); }, { message: 'pathwayName must be a non-empty string' }); t.throws(() => { t.context.pathwayManager.isLegacyPromptFormat('userId', null); }, { message: 'pathwayName must be a non-empty string' }); }); test('isLegacyPromptFormat handles objects with missing prompt property (treats as legacy)', t => { const userId = 'testUser'; const pathwayName = 'testPathway'; // Set up pathway with invalid objects t.context.pathwayManager.pathways = { [userId]: { [pathwayName]: { prompt: [ { name: 'Missing prompt property' }, { name: 'Another invalid object', notPrompt: 'invalid' } ] } } }; const result = t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); t.true(result); }); test('isLegacyPromptFormat handles objects with null prompt property (treats as legacy)', t => { const userId = 'testUser'; const pathwayName = 'testPathway'; // Set up pathway with null prompt properties t.context.pathwayManager.pathways = { [userId]: { [pathwayName]: { prompt: [ { name: 'Null prompt', prompt: null }, { name: 'Another null prompt', prompt: null } ] } } }; const result = t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); t.true(result); }); test('isLegacyPromptFormat handles array with null elements (treats as legacy)', t => { const userId = 'testUser'; const pathwayName = 'testPathway'; // Set up pathway with null elements t.context.pathwayManager.pathways = { [userId]: { [pathwayName]: { prompt: [ null, 'Some string prompt', null ] } } }; const result = t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); t.true(result); }); test('isLegacyPromptFormat handles single string element', t => { const userId = 'testUser'; const pathwayName = 'testPathway'; // Set up pathway with single string element t.context.pathwayManager.pathways = { [userId]: { [pathwayName]: { prompt: ['Only prompt'] } } }; const result = t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); t.true(result); }); test('isLegacyPromptFormat handles single object element', t => { const userId = 'testUser'; const pathwayName = 'testPathway'; // Set up pathway with single object element t.context.pathwayManager.pathways = { [userId]: { [pathwayName]: { prompt: [{ name: 'Only prompt', prompt: 'Only prompt text' }] } } }; const result = t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); t.false(result); }); test('isLegacyPromptFormat throws error when pathway not found', t => { const userId = 'testUser'; const pathwayName = 'nonExistentPathway'; // Set up empty pathways t.context.pathwayManager.pathways = {}; t.throws(() => { t.context.pathwayManager.isLegacyPromptFormat(userId, pathwayName); }, { message: `Pathway 'nonExistentPathway' not found for user 'testUser'` }); }); test('getPathways throws error for non-array promptNames', async t => { const pathwayTemplate = { prompt: [ { name: 'Test', prompt: 'Test prompt' } ], systemPrompt: 'System prompt' }; await t.throwsAsync( () => t.context.pathwayManager.getPathways(pathwayTemplate, 'not-an-array'), { message: 'promptNames must be an array if provided' } ); }); // Note: executeSpecificPrompts tests have been moved to GraphQL integration tests // since the function is now part of the GraphQL resolver layer, not the PathwayManager