Refactor: Improve MCP logging, update E2E & tests
Refactors MCP server logging and updates testing infrastructure.
- MCP Server:
- Replaced manual logger wrappers with centralized `createLogWrapper` utility.
- Updated direct function calls to use `{ session, mcpLog }` context.
- Removed deprecated `model` parameter from analyze, expand-all, expand-task tools.
- Adjusted MCP tool import paths and parameter descriptions.
- Documentation:
- Modified `docs/configuration.md`.
- Modified `docs/tutorial.md`.
- Testing:
- E2E Script (`run_e2e.sh`):
- Removed `set -e`.
- Added LLM analysis function (`analyze_log_with_llm`) & integration.
- Adjusted test run directory creation timing.
- Added debug echo statements.
- Deleted Unit Tests: Removed `ai-client-factory.test.js`, `ai-client-utils.test.js`, `ai-services.test.js`.
- Modified Fixtures: Updated `scripts/task-complexity-report.json`.
- Dev Scripts:
- Modified `scripts/dev.js`.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
'task-master-ai': patch
|
'task-master-ai': minor
|
||||||
---
|
---
|
||||||
|
|
||||||
feat(expand): Enhance `expand` and `expand-all` commands
|
feat(expand): Enhance `expand` and `expand-all` commands
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
'task-master-ai': patch
|
'task-master-ai': minor
|
||||||
---
|
---
|
||||||
|
|
||||||
- Adds support for the OpenRouter AI provider. Users can now configure models available through OpenRouter (requiring an `OPENROUTER_API_KEY`) via the `task-master models` command, granting access to a wide range of additional LLMs.
|
Adds support for the OpenRouter AI provider. Users can now configure models available through OpenRouter (requiring an `OPENROUTER_API_KEY`) via the `task-master models` command, granting access to a wide range of additional LLMs.
|
||||||
- IMPORTANT FYI ABOUT OPENROUTER: Taskmaster relies on AI SDK, which itself relies on tool use. It looks like **free** models sometimes do not include tool use. For example, Gemini 2.5 pro (free) failed via OpenRouter (no tool use) but worked fine on the paid version of the model. Custom model support for Open Router is considered experimental and likely will not be further improved for some time.
|
- IMPORTANT FYI ABOUT OPENROUTER: Taskmaster relies on AI SDK, which itself relies on tool use. It looks like **free** models sometimes do not include tool use. For example, Gemini 2.5 pro (free) failed via OpenRouter (no tool use) but worked fine on the paid version of the model. Custom model support for Open Router is considered experimental and likely will not be further improved for some time.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'task-master-ai': minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Refactor AI service interaction to use unified layer and Vercel SDK
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
'task-master-ai': patch
|
'task-master-ai': minor
|
||||||
---
|
---
|
||||||
|
|
||||||
Adds model management and new configuration file .taskmasterconfig which houses the models used for main, research and fallback. Adds models command and setter flags. Adds a --setup flag with an interactive setup. We should be calling this during init. Shows a table of active and available models when models is called without flags. Includes SWE scores and token costs, which are manually entered into the supported_models.json, the new place where models are defined for support. Config-manager.js is the core module responsible for managing the new config."
|
Adds model management and new configuration file .taskmasterconfig which houses the models used for main, research and fallback. Adds models command and setter flags. Adds a --setup flag with an interactive setup. We should be calling this during init. Shows a table of active and available models when models is called without flags. Includes SWE scores and token costs, which are manually entered into the supported_models.json, the new place where models are defined for support. Config-manager.js is the core module responsible for managing the new config."
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
'task-master-ai': patch
|
'task-master-ai': patch
|
||||||
---
|
---
|
||||||
|
|
||||||
- Improves next command to be subtask-aware
|
Improves next command to be subtask-aware
|
||||||
- The logic for determining the "next task" (findNextTask function, used by task-master next and the next_task MCP tool) has been significantly improved. Previously, it only considered top-level tasks, making its recommendation less useful when a parent task containing subtasks was already marked 'in-progress'.
|
- The logic for determining the "next task" (findNextTask function, used by task-master next and the next_task MCP tool) has been significantly improved. Previously, it only considered top-level tasks, making its recommendation less useful when a parent task containing subtasks was already marked 'in-progress'.
|
||||||
- The updated logic now prioritizes finding the next available subtask within any 'in-progress' parent task, considering subtask dependencies and priority.
|
- The updated logic now prioritizes finding the next available subtask within any 'in-progress' parent task, considering subtask dependencies and priority.
|
||||||
- If no suitable subtask is found within active parent tasks, it falls back to recommending the next eligible top-level task based on the original criteria (status, dependencies, priority).
|
- If no suitable subtask is found within active parent tasks, it falls back to recommending the next eligible top-level task based on the original criteria (status, dependencies, priority).
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
'task-master-ai': patch
|
'task-master-ai': minor
|
||||||
---
|
---
|
||||||
|
|
||||||
- feat: Add custom model ID support for Ollama and OpenRouter providers.
|
Adds custom model ID support for Ollama and OpenRouter providers.
|
||||||
- Adds the `--ollama` and `--openrouter` flags to `task-master models --set-<role>` command to set models for those providers outside of the support models list.
|
- Adds the `--ollama` and `--openrouter` flags to `task-master models --set-<role>` command to set models for those providers outside of the support models list.
|
||||||
- Updated `task-master models --setup` interactive mode with options to explicitly enter custom Ollama or OpenRouter model IDs.
|
- Updated `task-master models --setup` interactive mode with options to explicitly enter custom Ollama or OpenRouter model IDs.
|
||||||
- Implemented live validation against OpenRouter API (`/api/v1/models`) when setting a custom OpenRouter model ID (via flag or setup).
|
- Implemented live validation against OpenRouter API (`/api/v1/models`) when setting a custom OpenRouter model ID (via flag or setup).
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
'task-master-ai': minor
|
'task-master-ai': minor
|
||||||
---
|
---
|
||||||
|
|
||||||
Feat: Integrate OpenAI as a new AI provider.
|
Integrate OpenAI as a new AI provider.
|
||||||
Feat: Enhance `models` command/tool to display API key status.
|
- Enhance `models` command/tool to display API key status.
|
||||||
Feat: Implement model-specific `maxTokens` override based on `supported-models.json` to save you if you use an incorrect max token value.
|
- Implement model-specific `maxTokens` override based on `supported-models.json` to save you if you use an incorrect max token value.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
'task-master-ai': patch
|
'task-master-ai': minor
|
||||||
---
|
---
|
||||||
- Tweaks Perplexity AI calls for research mode to max out input tokens and get day-fresh information
|
Tweaks Perplexity AI calls for research mode to max out input tokens and get day-fresh information
|
||||||
- Forces temp at 0.1 for highly deterministic output, no variations
|
- Forces temp at 0.1 for highly deterministic output, no variations
|
||||||
- Adds a system prompt to further improve the output
|
- Adds a system prompt to further improve the output
|
||||||
- Correctly uses the maximum input tokens (8,719, used 8,700) for perplexity
|
- Correctly uses the maximum input tokens (8,719, used 8,700) for perplexity
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
'task-master-ai': patch
|
'task-master-ai': patch
|
||||||
---
|
---
|
||||||
|
|
||||||
- Adds a 'models' CLI and MCP command to get the current model configuration, available models, and gives the ability to set main/research/fallback models."
|
Adds a 'models' CLI and MCP command to get the current model configuration, available models, and gives the ability to set main/research/fallback models."
|
||||||
- In the CLI, `task-master models` shows the current models config. Using the `--setup` flag launches an interactive set up that allows you to easily select the models you want to use for each of the three roles. Use `q` during the interactive setup to cancel the setup.
|
- In the CLI, `task-master models` shows the current models config. Using the `--setup` flag launches an interactive set up that allows you to easily select the models you want to use for each of the three roles. Use `q` during the interactive setup to cancel the setup.
|
||||||
- In the MCP, responses are simplified in RESTful format (instead of the full CLI output). The agent can use the `models` tool with different arguments, including `listAvailableModels` to get available models. Run without arguments, it will return the current configuration. Arguments are available to set the model for each of the three roles. This allows you to manage Taskmaster AI providers and models directly from either the CLI or MCP or both.
|
- In the MCP, responses are simplified in RESTful format (instead of the full CLI output). The agent can use the `models` tool with different arguments, including `listAvailableModels` to get available models. Run without arguments, it will return the current configuration. Arguments are available to set the model for each of the three roles. This allows you to manage Taskmaster AI providers and models directly from either the CLI or MCP or both.
|
||||||
- Updated the CLI help menu when you run `task-master` to include missing commands and .taskmasterconfig information.
|
- Updated the CLI help menu when you run `task-master` to include missing commands and .taskmasterconfig information.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Taskmaster uses two primary methods for configuration:
|
|||||||
1. **`.taskmasterconfig` File (Project Root - Recommended for most settings)**
|
1. **`.taskmasterconfig` File (Project Root - Recommended for most settings)**
|
||||||
|
|
||||||
- This JSON file stores most configuration settings, including AI model selections, parameters, logging levels, and project defaults.
|
- This JSON file stores most configuration settings, including AI model selections, parameters, logging levels, and project defaults.
|
||||||
- **Location:** Create this file in the root directory of your project.
|
- **Location:** This file is created in the root directory of your project when you run the `task-master models --setup` interactive setup. You typically do this during the initialization sequence. Do not manually edit this file beyond adjusting Temperature and Max Tokens depending on your model.
|
||||||
- **Management:** Use the `task-master models --setup` command (or `models` MCP tool) to interactively create and manage this file. You can also set specific models directly using `task-master models --set-<role>=<model_id>`, adding `--ollama` or `--openrouter` flags for custom models. Manual editing is possible but not recommended unless you understand the structure.
|
- **Management:** Use the `task-master models --setup` command (or `models` MCP tool) to interactively create and manage this file. You can also set specific models directly using `task-master models --set-<role>=<model_id>`, adding `--ollama` or `--openrouter` flags for custom models. Manual editing is possible but not recommended unless you understand the structure.
|
||||||
- **Example Structure:**
|
- **Example Structure:**
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MCP (Model Control Protocol) provides the easiest way to get started with Task M
|
|||||||
npm i -g task-master-ai
|
npm i -g task-master-ai
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Add the MCP config to your editor** (Cursor recommended, but it works with other text editors):
|
2. **Add the MCP config to your IDE/MCP Client** (Cursor is recommended, but it works with other clients):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -39,6 +39,13 @@ npm i -g task-master-ai
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**IMPORTANT:** An API key is _required_ for each AI provider you plan on using. Run the `task-master models` command to see your selected models and the status of your API keys across .env and mcp.json
|
||||||
|
|
||||||
|
**To use AI commands in CLI** you MUST have API keys in the .env file
|
||||||
|
**To use AI commands in MCP** you MUST have API keys in the .mcp.json file (or MCP config equivalent)
|
||||||
|
|
||||||
|
We recommend having keys in both places and adding mcp.json to your gitignore so your API keys aren't checked into git.
|
||||||
|
|
||||||
3. **Enable the MCP** in your editor settings
|
3. **Enable the MCP** in your editor settings
|
||||||
|
|
||||||
4. **Prompt the AI** to initialize Task Master:
|
4. **Prompt the AI** to initialize Task Master:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
enableSilentMode,
|
enableSilentMode,
|
||||||
disableSilentMode
|
disableSilentMode
|
||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
|
import { createLogWrapper } from '../../tools/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct function wrapper for adding a new task with error handling.
|
* Direct function wrapper for adding a new task with error handling.
|
||||||
@@ -31,19 +32,13 @@ export async function addTaskDirect(args, log, context = {}) {
|
|||||||
const { tasksJsonPath, prompt, dependencies, priority, research } = args;
|
const { tasksJsonPath, prompt, dependencies, priority, research } = args;
|
||||||
const { session } = context; // Destructure session from context
|
const { session } = context; // Destructure session from context
|
||||||
|
|
||||||
// Define the logger wrapper to ensure compatibility with core report function
|
|
||||||
const logWrapper = {
|
|
||||||
info: (message, ...args) => log.info(message, ...args),
|
|
||||||
warn: (message, ...args) => log.warn(message, ...args),
|
|
||||||
error: (message, ...args) => log.error(message, ...args),
|
|
||||||
debug: (message, ...args) => log.debug && log.debug(message, ...args), // Handle optional debug
|
|
||||||
success: (message, ...args) => log.info(message, ...args) // Map success to info if needed
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||||
enableSilentMode();
|
enableSilentMode();
|
||||||
|
|
||||||
|
// Create logger wrapper using the utility
|
||||||
|
const mcpLog = createLogWrapper(log);
|
||||||
|
|
||||||
|
try {
|
||||||
// Check if tasksJsonPath was provided
|
// Check if tasksJsonPath was provided
|
||||||
if (!tasksJsonPath) {
|
if (!tasksJsonPath) {
|
||||||
log.error('addTaskDirect called without tasksJsonPath');
|
log.error('addTaskDirect called without tasksJsonPath');
|
||||||
@@ -112,8 +107,8 @@ export async function addTaskDirect(args, log, context = {}) {
|
|||||||
taskDependencies,
|
taskDependencies,
|
||||||
taskPriority,
|
taskPriority,
|
||||||
{
|
{
|
||||||
mcpLog: logWrapper,
|
session,
|
||||||
session
|
mcpLog
|
||||||
},
|
},
|
||||||
'json', // outputFormat
|
'json', // outputFormat
|
||||||
manualTaskData, // Pass the manual task data
|
manualTaskData, // Pass the manual task data
|
||||||
@@ -132,8 +127,8 @@ export async function addTaskDirect(args, log, context = {}) {
|
|||||||
taskDependencies,
|
taskDependencies,
|
||||||
taskPriority,
|
taskPriority,
|
||||||
{
|
{
|
||||||
mcpLog: logWrapper,
|
session,
|
||||||
session
|
mcpLog
|
||||||
},
|
},
|
||||||
'json', // outputFormat
|
'json', // outputFormat
|
||||||
null, // manualTaskData is null for AI creation
|
null, // manualTaskData is null for AI creation
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import {
|
|||||||
isSilentMode
|
isSilentMode
|
||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { createLogWrapper } from '../../tools/utils.js'; // Import the new utility
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyze task complexity and generate recommendations
|
* Analyze task complexity and generate recommendations
|
||||||
* @param {Object} args - Function arguments
|
* @param {Object} args - Function arguments
|
||||||
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
|
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
|
||||||
* @param {string} args.outputPath - Explicit absolute path to save the report.
|
* @param {string} args.outputPath - Explicit absolute path to save the report.
|
||||||
* @param {string} [args.model] - Deprecated: LLM model to use for analysis (ignored)
|
|
||||||
* @param {string|number} [args.threshold] - Minimum complexity score to recommend expansion (1-10)
|
* @param {string|number} [args.threshold] - Minimum complexity score to recommend expansion (1-10)
|
||||||
* @param {boolean} [args.research] - Use Perplexity AI for research-backed complexity analysis
|
* @param {boolean} [args.research] - Use Perplexity AI for research-backed complexity analysis
|
||||||
* @param {Object} log - Logger object
|
* @param {Object} log - Logger object
|
||||||
@@ -76,14 +76,8 @@ export async function analyzeTaskComplexityDirect(args, log, context = {}) {
|
|||||||
enableSilentMode();
|
enableSilentMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
const logWrapper = {
|
// Create logger wrapper using the utility
|
||||||
info: (message, ...args) => log.info(message, ...args),
|
const mcpLog = createLogWrapper(log);
|
||||||
warn: (message, ...args) => log.warn(message, ...args),
|
|
||||||
error: (message, ...args) => log.error(message, ...args),
|
|
||||||
debug: (message, ...args) => log.debug && log.debug(message, ...args),
|
|
||||||
success: (message, ...args) => log.info(message, ...args) // Map success to info
|
|
||||||
};
|
|
||||||
// --- End Silent Mode and Logger Wrapper ---
|
|
||||||
|
|
||||||
let report; // To store the result from the core function
|
let report; // To store the result from the core function
|
||||||
|
|
||||||
@@ -92,7 +86,7 @@ export async function analyzeTaskComplexityDirect(args, log, context = {}) {
|
|||||||
// Call the core function, passing options and the context object { session, mcpLog }
|
// Call the core function, passing options and the context object { session, mcpLog }
|
||||||
report = await analyzeTaskComplexity(options, {
|
report = await analyzeTaskComplexity(options, {
|
||||||
session, // Pass the session object
|
session, // Pass the session object
|
||||||
mcpLog: logWrapper // Pass the logger wrapper
|
mcpLog // Pass the logger wrapper
|
||||||
});
|
});
|
||||||
// --- End Core Function Call ---
|
// --- End Core Function Call ---
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
import { expandAllTasks } from '../../../../scripts/modules/task-manager.js';
|
import { expandAllTasks } from '../../../../scripts/modules/task-manager.js';
|
||||||
import {
|
import {
|
||||||
enableSilentMode,
|
enableSilentMode,
|
||||||
disableSilentMode,
|
disableSilentMode
|
||||||
isSilentMode
|
|
||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
|
import { createLogWrapper } from '../../tools/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand all pending tasks with subtasks (Direct Function Wrapper)
|
* Expand all pending tasks with subtasks (Direct Function Wrapper)
|
||||||
@@ -26,14 +26,8 @@ export async function expandAllTasksDirect(args, log, context = {}) {
|
|||||||
// Destructure expected args
|
// Destructure expected args
|
||||||
const { tasksJsonPath, num, research, prompt, force } = args;
|
const { tasksJsonPath, num, research, prompt, force } = args;
|
||||||
|
|
||||||
// Create the standard logger wrapper
|
// Create logger wrapper using the utility
|
||||||
const logWrapper = {
|
const mcpLog = createLogWrapper(log);
|
||||||
info: (message, ...args) => log.info(message, ...args),
|
|
||||||
warn: (message, ...args) => log.warn(message, ...args),
|
|
||||||
error: (message, ...args) => log.error(message, ...args),
|
|
||||||
debug: (message, ...args) => log.debug && log.debug(message, ...args), // Handle optional debug
|
|
||||||
success: (message, ...args) => log.info(message, ...args) // Map success to info if needed
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!tasksJsonPath) {
|
if (!tasksJsonPath) {
|
||||||
log.error('expandAllTasksDirect called without tasksJsonPath');
|
log.error('expandAllTasksDirect called without tasksJsonPath');
|
||||||
@@ -58,15 +52,14 @@ export async function expandAllTasksDirect(args, log, context = {}) {
|
|||||||
const additionalContext = prompt || '';
|
const additionalContext = prompt || '';
|
||||||
const forceFlag = force === true;
|
const forceFlag = force === true;
|
||||||
|
|
||||||
// Call the core function, passing the logger wrapper and session
|
// Call the core function, passing options and the context object { session, mcpLog }
|
||||||
const result = await expandAllTasks(
|
const result = await expandAllTasks(
|
||||||
tasksJsonPath, // Use the provided path
|
tasksJsonPath,
|
||||||
numSubtasks,
|
numSubtasks,
|
||||||
useResearch,
|
useResearch,
|
||||||
additionalContext,
|
additionalContext,
|
||||||
forceFlag,
|
forceFlag,
|
||||||
{ mcpLog: logWrapper, session }, // Pass the wrapper and session
|
{ session, mcpLog }
|
||||||
'json' // Explicitly request JSON output format
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Core function now returns a summary object
|
// Core function now returns a summary object
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Direct function implementation for expanding a task into subtasks
|
* Direct function implementation for expanding a task into subtasks
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import expandTask from '../../../../scripts/modules/task-manager/expand-task.js'; // Correct import path
|
import expandTask from '../../../../scripts/modules/task-manager/expand-task.js';
|
||||||
import {
|
import {
|
||||||
readJSON,
|
readJSON,
|
||||||
writeJSON,
|
writeJSON,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { createLogWrapper } from '../../tools/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct function wrapper for expanding a task into subtasks with error handling.
|
* Direct function wrapper for expanding a task into subtasks with error handling.
|
||||||
@@ -180,28 +181,23 @@ export async function expandTaskDirect(args, log, context = {}) {
|
|||||||
// Save tasks.json with potentially empty subtasks array
|
// Save tasks.json with potentially empty subtasks array
|
||||||
writeJSON(tasksPath, data);
|
writeJSON(tasksPath, data);
|
||||||
|
|
||||||
|
// Create logger wrapper using the utility
|
||||||
|
const mcpLog = createLogWrapper(log);
|
||||||
|
|
||||||
// Process the request
|
// Process the request
|
||||||
try {
|
try {
|
||||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||||
const wasSilent = isSilentMode();
|
const wasSilent = isSilentMode();
|
||||||
if (!wasSilent) enableSilentMode();
|
if (!wasSilent) enableSilentMode();
|
||||||
|
|
||||||
const logWrapper = {
|
// Call the core expandTask function with the wrapped logger
|
||||||
info: (message, ...args) => log.info(message, ...args),
|
|
||||||
warn: (message, ...args) => log.warn(message, ...args),
|
|
||||||
error: (message, ...args) => log.error(message, ...args),
|
|
||||||
debug: (message, ...args) => log.debug && log.debug(message, ...args),
|
|
||||||
success: (message, ...args) => log.info(message, ...args)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call expandTask with session context to ensure AI client is properly initialized
|
|
||||||
const result = await expandTask(
|
const result = await expandTask(
|
||||||
tasksPath,
|
tasksPath,
|
||||||
taskId,
|
taskId,
|
||||||
numSubtasks,
|
numSubtasks,
|
||||||
useResearch,
|
useResearch,
|
||||||
additionalContext,
|
additionalContext,
|
||||||
{ session: session, mcpLog: logWrapper }
|
{ mcpLog, session }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restore normal logging
|
// Restore normal logging
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
enableSilentMode,
|
enableSilentMode,
|
||||||
disableSilentMode
|
disableSilentMode
|
||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
|
import { createLogWrapper } from '../../tools/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or update model configuration
|
* Get or update model configuration
|
||||||
@@ -25,14 +26,7 @@ export async function modelsDirect(args, log, context = {}) {
|
|||||||
const { projectRoot } = args; // Extract projectRoot from args
|
const { projectRoot } = args; // Extract projectRoot from args
|
||||||
|
|
||||||
// Create a logger wrapper that the core functions can use
|
// Create a logger wrapper that the core functions can use
|
||||||
const logWrapper = {
|
const mcpLog = createLogWrapper(log);
|
||||||
info: (message, ...args) => log.info(message, ...args),
|
|
||||||
warn: (message, ...args) => log.warn(message, ...args),
|
|
||||||
error: (message, ...args) => log.error(message, ...args),
|
|
||||||
debug: (message, ...args) =>
|
|
||||||
log.debug ? log.debug(message, ...args) : null,
|
|
||||||
success: (message, ...args) => log.info(message, ...args)
|
|
||||||
};
|
|
||||||
|
|
||||||
log.info(`Executing models_direct with args: ${JSON.stringify(args)}`);
|
log.info(`Executing models_direct with args: ${JSON.stringify(args)}`);
|
||||||
log.info(`Using project root: ${projectRoot}`);
|
log.info(`Using project root: ${projectRoot}`);
|
||||||
@@ -59,7 +53,7 @@ export async function modelsDirect(args, log, context = {}) {
|
|||||||
if (args.listAvailableModels === true) {
|
if (args.listAvailableModels === true) {
|
||||||
return await getAvailableModelsList({
|
return await getAvailableModelsList({
|
||||||
session,
|
session,
|
||||||
mcpLog: logWrapper,
|
mcpLog,
|
||||||
projectRoot // Pass projectRoot to function
|
projectRoot // Pass projectRoot to function
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,7 +62,7 @@ export async function modelsDirect(args, log, context = {}) {
|
|||||||
if (args.setMain) {
|
if (args.setMain) {
|
||||||
return await setModel('main', args.setMain, {
|
return await setModel('main', args.setMain, {
|
||||||
session,
|
session,
|
||||||
mcpLog: logWrapper,
|
mcpLog,
|
||||||
projectRoot, // Pass projectRoot to function
|
projectRoot, // Pass projectRoot to function
|
||||||
providerHint: args.openrouter
|
providerHint: args.openrouter
|
||||||
? 'openrouter'
|
? 'openrouter'
|
||||||
@@ -81,7 +75,7 @@ export async function modelsDirect(args, log, context = {}) {
|
|||||||
if (args.setResearch) {
|
if (args.setResearch) {
|
||||||
return await setModel('research', args.setResearch, {
|
return await setModel('research', args.setResearch, {
|
||||||
session,
|
session,
|
||||||
mcpLog: logWrapper,
|
mcpLog,
|
||||||
projectRoot, // Pass projectRoot to function
|
projectRoot, // Pass projectRoot to function
|
||||||
providerHint: args.openrouter
|
providerHint: args.openrouter
|
||||||
? 'openrouter'
|
? 'openrouter'
|
||||||
@@ -94,7 +88,7 @@ export async function modelsDirect(args, log, context = {}) {
|
|||||||
if (args.setFallback) {
|
if (args.setFallback) {
|
||||||
return await setModel('fallback', args.setFallback, {
|
return await setModel('fallback', args.setFallback, {
|
||||||
session,
|
session,
|
||||||
mcpLog: logWrapper,
|
mcpLog,
|
||||||
projectRoot, // Pass projectRoot to function
|
projectRoot, // Pass projectRoot to function
|
||||||
providerHint: args.openrouter
|
providerHint: args.openrouter
|
||||||
? 'openrouter'
|
? 'openrouter'
|
||||||
@@ -107,7 +101,7 @@ export async function modelsDirect(args, log, context = {}) {
|
|||||||
// Default action: get current configuration
|
// Default action: get current configuration
|
||||||
return await getModelConfiguration({
|
return await getModelConfiguration({
|
||||||
session,
|
session,
|
||||||
mcpLog: logWrapper,
|
mcpLog,
|
||||||
projectRoot // Pass projectRoot to function
|
projectRoot // Pass projectRoot to function
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
enableSilentMode,
|
enableSilentMode,
|
||||||
disableSilentMode
|
disableSilentMode
|
||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
|
import { createLogWrapper } from '../../tools/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct function wrapper for parsing PRD documents and generating tasks.
|
* Direct function wrapper for parsing PRD documents and generating tasks.
|
||||||
@@ -104,23 +105,20 @@ export async function parsePRDDirect(args, log, context = {}) {
|
|||||||
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`
|
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the logger wrapper for proper logging in the core function
|
// --- Logger Wrapper ---
|
||||||
const logWrapper = {
|
const mcpLog = createLogWrapper(log);
|
||||||
info: (message, ...args) => log.info(message, ...args),
|
|
||||||
warn: (message, ...args) => log.warn(message, ...args),
|
// Prepare options for the core function
|
||||||
error: (message, ...args) => log.error(message, ...args),
|
const options = {
|
||||||
debug: (message, ...args) => log.debug && log.debug(message, ...args),
|
mcpLog,
|
||||||
success: (message, ...args) => log.info(message, ...args) // Map success to info
|
session
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||||
enableSilentMode();
|
enableSilentMode();
|
||||||
try {
|
try {
|
||||||
// Execute core parsePRD function - It now handles AI internally
|
// Execute core parsePRD function - It now handles AI internally
|
||||||
const tasksDataResult = await parsePRD(inputPath, outputPath, numTasks, {
|
const tasksDataResult = await parsePRD(inputPath, numTasks, options);
|
||||||
mcpLog: logWrapper,
|
|
||||||
session
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check the result from the core function (assuming it might return data or null/undefined)
|
// Check the result from the core function (assuming it might return data or null/undefined)
|
||||||
if (!tasksDataResult || !tasksDataResult.tasks) {
|
if (!tasksDataResult || !tasksDataResult.tasks) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
enableSilentMode,
|
enableSilentMode,
|
||||||
disableSilentMode
|
disableSilentMode
|
||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
|
import { createLogWrapper } from '../../tools/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct function wrapper for updateSubtaskById with error handling.
|
* Direct function wrapper for updateSubtaskById with error handling.
|
||||||
@@ -95,15 +96,8 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
|
|||||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||||
enableSilentMode();
|
enableSilentMode();
|
||||||
|
|
||||||
// Create a logger wrapper object to handle logging without breaking the mcpLog[level] calls
|
// Create the logger wrapper using the utility function
|
||||||
// This ensures outputFormat is set to 'json' while still supporting proper logging
|
const mcpLog = createLogWrapper(log);
|
||||||
const logWrapper = {
|
|
||||||
info: (message) => log.info(message),
|
|
||||||
warn: (message) => log.warn(message),
|
|
||||||
error: (message) => log.error(message),
|
|
||||||
debug: (message) => log.debug && log.debug(message),
|
|
||||||
success: (message) => log.info(message) // Map success to info if needed
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute core updateSubtaskById function
|
// Execute core updateSubtaskById function
|
||||||
// Pass both session and logWrapper as mcpLog to ensure outputFormat is 'json'
|
// Pass both session and logWrapper as mcpLog to ensure outputFormat is 'json'
|
||||||
@@ -114,7 +108,7 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
|
|||||||
useResearch,
|
useResearch,
|
||||||
{
|
{
|
||||||
session,
|
session,
|
||||||
mcpLog: logWrapper
|
mcpLog
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
enableSilentMode,
|
enableSilentMode,
|
||||||
disableSilentMode
|
disableSilentMode
|
||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
|
import { createLogWrapper } from '../../tools/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct function wrapper for updateTaskById with error handling.
|
* Direct function wrapper for updateTaskById with error handling.
|
||||||
@@ -96,14 +97,8 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
|||||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||||
enableSilentMode();
|
enableSilentMode();
|
||||||
|
|
||||||
// Create a logger wrapper that matches what updateTaskById expects
|
// Create the logger wrapper using the utility function
|
||||||
const logWrapper = {
|
const mcpLog = createLogWrapper(log);
|
||||||
info: (message) => log.info(message),
|
|
||||||
warn: (message) => log.warn(message),
|
|
||||||
error: (message) => log.error(message),
|
|
||||||
debug: (message) => log.debug && log.debug(message),
|
|
||||||
success: (message) => log.info(message) // Map success to info since many loggers don't have success
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute core updateTaskById function with proper parameters
|
// Execute core updateTaskById function with proper parameters
|
||||||
await updateTaskById(
|
await updateTaskById(
|
||||||
@@ -112,7 +107,7 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
|
|||||||
prompt,
|
prompt,
|
||||||
useResearch,
|
useResearch,
|
||||||
{
|
{
|
||||||
mcpLog: logWrapper, // Use our wrapper object that has the expected method structure
|
mcpLog, // Pass the wrapped logger
|
||||||
session
|
session
|
||||||
},
|
},
|
||||||
'json'
|
'json'
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
enableSilentMode,
|
enableSilentMode,
|
||||||
disableSilentMode
|
disableSilentMode
|
||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
|
import { createLogWrapper } from '../../tools/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct function wrapper for updating tasks based on new context/prompt.
|
* Direct function wrapper for updating tasks based on new context/prompt.
|
||||||
@@ -88,6 +89,9 @@ export async function updateTasksDirect(args, log, context = {}) {
|
|||||||
|
|
||||||
enableSilentMode(); // Enable silent mode
|
enableSilentMode(); // Enable silent mode
|
||||||
try {
|
try {
|
||||||
|
// Create logger wrapper using the utility
|
||||||
|
const mcpLog = createLogWrapper(log);
|
||||||
|
|
||||||
// Execute core updateTasks function, passing session context
|
// Execute core updateTasks function, passing session context
|
||||||
await updateTasks(
|
await updateTasks(
|
||||||
tasksJsonPath,
|
tasksJsonPath,
|
||||||
@@ -95,7 +99,7 @@ export async function updateTasksDirect(args, log, context = {}) {
|
|||||||
prompt,
|
prompt,
|
||||||
useResearch,
|
useResearch,
|
||||||
// Pass context with logger wrapper and session
|
// Pass context with logger wrapper and session
|
||||||
{ mcpLog: logWrapper, session },
|
{ mcpLog, session },
|
||||||
'json' // Explicitly request JSON format for MCP
|
'json' // Explicitly request JSON format for MCP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ export function registerAnalyzeTool(server) {
|
|||||||
.describe(
|
.describe(
|
||||||
'Output file path relative to project root (default: scripts/task-complexity-report.json)'
|
'Output file path relative to project root (default: scripts/task-complexity-report.json)'
|
||||||
),
|
),
|
||||||
model: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'Deprecated: LLM model override (model is determined by configured role)'
|
|
||||||
),
|
|
||||||
threshold: z.coerce
|
threshold: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.min(1)
|
.min(1)
|
||||||
@@ -44,7 +38,7 @@ export function registerAnalyzeTool(server) {
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
'Path to the tasks file relative to project root (default: tasks/tasks.json)'
|
'Absolute path to the tasks file in the /tasks folder inside the project root (default: tasks/tasks.json)'
|
||||||
),
|
),
|
||||||
research: z
|
research: z
|
||||||
.boolean()
|
.boolean()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function registerExpandAllTool(server) {
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
'Relative path to the tasks file from project root (default: tasks/tasks.json)'
|
'Absolute path to the tasks file in the /tasks folder inside the project root (default: tasks/tasks.json)'
|
||||||
),
|
),
|
||||||
projectRoot: z
|
projectRoot: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
createErrorResponse,
|
createErrorResponse,
|
||||||
getProjectRootFromSession
|
getProjectRootFromSession
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { expandTaskDirect } from '../core/direct-functions/expand-task.js';
|
import { expandTaskDirect } from '../core/task-master-core.js';
|
||||||
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
import { findTasksJsonPath } from '../core/utils/path-utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ function createContentResponse(content) {
|
|||||||
* @param {string} errorMessage - Error message to include in response
|
* @param {string} errorMessage - Error message to include in response
|
||||||
* @returns {Object} - Error content response object in FastMCP format
|
* @returns {Object} - Error content response object in FastMCP format
|
||||||
*/
|
*/
|
||||||
export function createErrorResponse(errorMessage) {
|
function createErrorResponse(errorMessage) {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -455,6 +455,25 @@ export function createErrorResponse(errorMessage) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a logger wrapper object compatible with core function expectations.
|
||||||
|
* Adapts the MCP logger to the { info, warn, error, debug, success } structure.
|
||||||
|
* @param {Object} log - The MCP logger instance.
|
||||||
|
* @returns {Object} - The logger wrapper object.
|
||||||
|
*/
|
||||||
|
function createLogWrapper(log) {
|
||||||
|
return {
|
||||||
|
info: (message, ...args) => log.info(message, ...args),
|
||||||
|
warn: (message, ...args) => log.warn(message, ...args),
|
||||||
|
error: (message, ...args) => log.error(message, ...args),
|
||||||
|
// Handle optional debug method
|
||||||
|
debug: (message, ...args) =>
|
||||||
|
log.debug ? log.debug(message, ...args) : null,
|
||||||
|
// Map success to info as a common fallback
|
||||||
|
success: (message, ...args) => log.info(message, ...args)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure all functions are exported
|
// Ensure all functions are exported
|
||||||
export {
|
export {
|
||||||
getProjectRoot,
|
getProjectRoot,
|
||||||
@@ -463,5 +482,7 @@ export {
|
|||||||
executeTaskMasterCommand,
|
executeTaskMasterCommand,
|
||||||
getCachedOrExecute,
|
getCachedOrExecute,
|
||||||
processMCPResponseData,
|
processMCPResponseData,
|
||||||
createContentResponse
|
createContentResponse,
|
||||||
|
createErrorResponse,
|
||||||
|
createLogWrapper
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
* It imports functionality from the modules directory and provides a CLI.
|
* It imports functionality from the modules directory and provides a CLI.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dotenv from 'dotenv'; // <-- ADD
|
import dotenv from 'dotenv';
|
||||||
dotenv.config(); // <-- ADD
|
dotenv.config();
|
||||||
|
|
||||||
// Add at the very beginning of the file
|
// Add at the very beginning of the file
|
||||||
if (process.env.DEBUG === '1') {
|
if (process.env.DEBUG === '1') {
|
||||||
|
|||||||
@@ -8,26 +8,22 @@
|
|||||||
|
|
||||||
// --- Core Dependencies ---
|
// --- Core Dependencies ---
|
||||||
import {
|
import {
|
||||||
// REMOVED: getProviderAndModelForRole, // This was incorrect
|
getMainProvider,
|
||||||
getMainProvider, // ADD individual getters
|
|
||||||
getMainModelId,
|
getMainModelId,
|
||||||
getResearchProvider,
|
getResearchProvider,
|
||||||
getResearchModelId,
|
getResearchModelId,
|
||||||
getFallbackProvider,
|
getFallbackProvider,
|
||||||
getFallbackModelId,
|
getFallbackModelId,
|
||||||
getParametersForRole
|
getParametersForRole
|
||||||
// ConfigurationError // Import if needed for specific handling
|
} from './config-manager.js';
|
||||||
} from './config-manager.js'; // Corrected: Removed getProviderAndModelForRole
|
|
||||||
import { log, resolveEnvVariable } from './utils.js';
|
import { log, resolveEnvVariable } from './utils.js';
|
||||||
|
|
||||||
// --- Provider Service Imports ---
|
|
||||||
// Corrected path from scripts/ai-providers/... to ../../src/ai-providers/...
|
|
||||||
import * as anthropic from '../../src/ai-providers/anthropic.js';
|
import * as anthropic from '../../src/ai-providers/anthropic.js';
|
||||||
import * as perplexity from '../../src/ai-providers/perplexity.js';
|
import * as perplexity from '../../src/ai-providers/perplexity.js';
|
||||||
import * as google from '../../src/ai-providers/google.js'; // Import Google provider
|
import * as google from '../../src/ai-providers/google.js';
|
||||||
import * as openai from '../../src/ai-providers/openai.js'; // ADD: Import OpenAI provider
|
import * as openai from '../../src/ai-providers/openai.js';
|
||||||
import * as xai from '../../src/ai-providers/xai.js'; // ADD: Import xAI provider
|
import * as xai from '../../src/ai-providers/xai.js';
|
||||||
import * as openrouter from '../../src/ai-providers/openrouter.js'; // ADD: Import OpenRouter provider
|
import * as openrouter from '../../src/ai-providers/openrouter.js';
|
||||||
// TODO: Import other provider modules when implemented (ollama, etc.)
|
// TODO: Import other provider modules when implemented (ollama, etc.)
|
||||||
|
|
||||||
// --- Provider Function Map ---
|
// --- Provider Function Map ---
|
||||||
@@ -37,13 +33,11 @@ const PROVIDER_FUNCTIONS = {
|
|||||||
generateText: anthropic.generateAnthropicText,
|
generateText: anthropic.generateAnthropicText,
|
||||||
streamText: anthropic.streamAnthropicText,
|
streamText: anthropic.streamAnthropicText,
|
||||||
generateObject: anthropic.generateAnthropicObject
|
generateObject: anthropic.generateAnthropicObject
|
||||||
// streamObject: anthropic.streamAnthropicObject, // Add when implemented
|
|
||||||
},
|
},
|
||||||
perplexity: {
|
perplexity: {
|
||||||
generateText: perplexity.generatePerplexityText,
|
generateText: perplexity.generatePerplexityText,
|
||||||
streamText: perplexity.streamPerplexityText,
|
streamText: perplexity.streamPerplexityText,
|
||||||
generateObject: perplexity.generatePerplexityObject
|
generateObject: perplexity.generatePerplexityObject
|
||||||
// streamObject: perplexity.streamPerplexityObject, // Add when implemented
|
|
||||||
},
|
},
|
||||||
google: {
|
google: {
|
||||||
// Add Google entry
|
// Add Google entry
|
||||||
@@ -73,22 +67,20 @@ const PROVIDER_FUNCTIONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- Configuration for Retries ---
|
// --- Configuration for Retries ---
|
||||||
const MAX_RETRIES = 2; // Total attempts = 1 + MAX_RETRIES
|
const MAX_RETRIES = 2;
|
||||||
const INITIAL_RETRY_DELAY_MS = 1000; // 1 second
|
const INITIAL_RETRY_DELAY_MS = 1000;
|
||||||
|
|
||||||
// Helper function to check if an error is retryable
|
// Helper function to check if an error is retryable
|
||||||
function isRetryableError(error) {
|
function isRetryableError(error) {
|
||||||
const errorMessage = error.message?.toLowerCase() || '';
|
const errorMessage = error.message?.toLowerCase() || '';
|
||||||
// Add common retryable error patterns
|
|
||||||
return (
|
return (
|
||||||
errorMessage.includes('rate limit') ||
|
errorMessage.includes('rate limit') ||
|
||||||
errorMessage.includes('overloaded') ||
|
errorMessage.includes('overloaded') ||
|
||||||
errorMessage.includes('service temporarily unavailable') ||
|
errorMessage.includes('service temporarily unavailable') ||
|
||||||
errorMessage.includes('timeout') ||
|
errorMessage.includes('timeout') ||
|
||||||
errorMessage.includes('network error') ||
|
errorMessage.includes('network error') ||
|
||||||
// Add specific status codes if available from the SDK errors
|
error.status === 429 ||
|
||||||
error.status === 429 || // Too Many Requests
|
error.status >= 500
|
||||||
error.status >= 500 // Server-side errors
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,11 +143,11 @@ function _resolveApiKey(providerName, session) {
|
|||||||
const keyMap = {
|
const keyMap = {
|
||||||
openai: 'OPENAI_API_KEY',
|
openai: 'OPENAI_API_KEY',
|
||||||
anthropic: 'ANTHROPIC_API_KEY',
|
anthropic: 'ANTHROPIC_API_KEY',
|
||||||
google: 'GOOGLE_API_KEY', // Add Google API Key
|
google: 'GOOGLE_API_KEY',
|
||||||
perplexity: 'PERPLEXITY_API_KEY',
|
perplexity: 'PERPLEXITY_API_KEY',
|
||||||
mistral: 'MISTRAL_API_KEY',
|
mistral: 'MISTRAL_API_KEY',
|
||||||
azure: 'AZURE_OPENAI_API_KEY',
|
azure: 'AZURE_OPENAI_API_KEY',
|
||||||
openrouter: 'OPENROUTER_API_KEY', // ADD OpenRouter key
|
openrouter: 'OPENROUTER_API_KEY',
|
||||||
xai: 'XAI_API_KEY'
|
xai: 'XAI_API_KEY'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,7 +191,7 @@ async function _attemptProviderCallWithRetries(
|
|||||||
attemptRole
|
attemptRole
|
||||||
) {
|
) {
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
const fnName = providerApiFn.name; // Get function name for logging
|
const fnName = providerApiFn.name;
|
||||||
|
|
||||||
while (retries <= MAX_RETRIES) {
|
while (retries <= MAX_RETRIES) {
|
||||||
try {
|
try {
|
||||||
@@ -215,7 +207,7 @@ async function _attemptProviderCallWithRetries(
|
|||||||
'info',
|
'info',
|
||||||
`${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}`
|
`${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}`
|
||||||
);
|
);
|
||||||
return result; // Success!
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(
|
log(
|
||||||
'warn',
|
'warn',
|
||||||
@@ -235,7 +227,7 @@ async function _attemptProviderCallWithRetries(
|
|||||||
'error',
|
'error',
|
||||||
`Non-retryable error or max retries reached for role ${attemptRole} (${fnName} / ${providerName}).`
|
`Non-retryable error or max retries reached for role ${attemptRole} (${fnName} / ${providerName}).`
|
||||||
);
|
);
|
||||||
throw error; // Final failure for this attempt chain
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,18 +280,17 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
try {
|
try {
|
||||||
log('info', `New AI service call with role: ${currentRole}`);
|
log('info', `New AI service call with role: ${currentRole}`);
|
||||||
|
|
||||||
// --- Corrected Config Fetching ---
|
|
||||||
// 1. Get Config: Provider, Model, Parameters for the current role
|
// 1. Get Config: Provider, Model, Parameters for the current role
|
||||||
// Call individual getters based on the current role
|
// Call individual getters based on the current role
|
||||||
if (currentRole === 'main') {
|
if (currentRole === 'main') {
|
||||||
providerName = getMainProvider(); // Use individual getter
|
providerName = getMainProvider();
|
||||||
modelId = getMainModelId(); // Use individual getter
|
modelId = getMainModelId();
|
||||||
} else if (currentRole === 'research') {
|
} else if (currentRole === 'research') {
|
||||||
providerName = getResearchProvider(); // Use individual getter
|
providerName = getResearchProvider();
|
||||||
modelId = getResearchModelId(); // Use individual getter
|
modelId = getResearchModelId();
|
||||||
} else if (currentRole === 'fallback') {
|
} else if (currentRole === 'fallback') {
|
||||||
providerName = getFallbackProvider(); // Use individual getter
|
providerName = getFallbackProvider();
|
||||||
modelId = getFallbackModelId(); // Use individual getter
|
modelId = getFallbackModelId();
|
||||||
} else {
|
} else {
|
||||||
log(
|
log(
|
||||||
'error',
|
'error',
|
||||||
@@ -307,9 +298,8 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
);
|
);
|
||||||
lastError =
|
lastError =
|
||||||
lastError || new Error(`Unknown AI role specified: ${currentRole}`);
|
lastError || new Error(`Unknown AI role specified: ${currentRole}`);
|
||||||
continue; // Skip to the next role attempt
|
continue;
|
||||||
}
|
}
|
||||||
// --- End Corrected Config Fetching ---
|
|
||||||
|
|
||||||
if (!providerName || !modelId) {
|
if (!providerName || !modelId) {
|
||||||
log(
|
log(
|
||||||
@@ -321,10 +311,10 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
new Error(
|
new Error(
|
||||||
`Configuration missing for role '${currentRole}'. Provider: ${providerName}, Model: ${modelId}`
|
`Configuration missing for role '${currentRole}'. Provider: ${providerName}, Model: ${modelId}`
|
||||||
);
|
);
|
||||||
continue; // Skip to the next role
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
roleParams = getParametersForRole(currentRole); // Get { maxTokens, temperature }
|
roleParams = getParametersForRole(currentRole);
|
||||||
|
|
||||||
// 2. Get Provider Function Set
|
// 2. Get Provider Function Set
|
||||||
providerFnSet = PROVIDER_FUNCTIONS[providerName?.toLowerCase()];
|
providerFnSet = PROVIDER_FUNCTIONS[providerName?.toLowerCase()];
|
||||||
@@ -355,7 +345,7 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Resolve API Key (will throw if required and missing)
|
// 3. Resolve API Key (will throw if required and missing)
|
||||||
apiKey = _resolveApiKey(providerName?.toLowerCase(), session); // Throws on failure
|
apiKey = _resolveApiKey(providerName?.toLowerCase(), session);
|
||||||
|
|
||||||
// 4. Construct Messages Array
|
// 4. Construct Messages Array
|
||||||
const messages = [];
|
const messages = [];
|
||||||
@@ -395,10 +385,9 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
modelId,
|
modelId,
|
||||||
maxTokens: roleParams.maxTokens,
|
maxTokens: roleParams.maxTokens,
|
||||||
temperature: roleParams.temperature,
|
temperature: roleParams.temperature,
|
||||||
messages, // *** Pass the constructed messages array ***
|
messages,
|
||||||
// Add specific params for generateObject if needed
|
|
||||||
...(serviceType === 'generateObject' && { schema, objectName }),
|
...(serviceType === 'generateObject' && { schema, objectName }),
|
||||||
...restApiParams // Include other params like maxRetries
|
...restApiParams
|
||||||
};
|
};
|
||||||
|
|
||||||
// 6. Attempt the call with retries
|
// 6. Attempt the call with retries
|
||||||
@@ -412,20 +401,18 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
|
|
||||||
log('info', `${serviceType}Service succeeded using role: ${currentRole}`);
|
log('info', `${serviceType}Service succeeded using role: ${currentRole}`);
|
||||||
|
|
||||||
return result; // Return original result for other cases
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const cleanMessage = _extractErrorMessage(error); // Extract clean message
|
const cleanMessage = _extractErrorMessage(error);
|
||||||
log(
|
log(
|
||||||
'error', // Log as error since this role attempt failed
|
'error',
|
||||||
`Service call failed for role ${currentRole} (Provider: ${providerName || 'unknown'}, Model: ${modelId || 'unknown'}): ${cleanMessage}` // Log the clean message
|
`Service call failed for role ${currentRole} (Provider: ${providerName || 'unknown'}, Model: ${modelId || 'unknown'}): ${cleanMessage}`
|
||||||
);
|
);
|
||||||
lastError = error; // Store the original error for potential debugging
|
lastError = error;
|
||||||
lastCleanErrorMessage = cleanMessage; // Store the clean message for final throw
|
lastCleanErrorMessage = cleanMessage;
|
||||||
|
|
||||||
// --- ADDED: Specific check for tool use error in generateObject ---
|
|
||||||
if (serviceType === 'generateObject') {
|
if (serviceType === 'generateObject') {
|
||||||
const lowerCaseMessage = cleanMessage.toLowerCase();
|
const lowerCaseMessage = cleanMessage.toLowerCase();
|
||||||
// Check for specific error messages indicating lack of tool support
|
|
||||||
if (
|
if (
|
||||||
lowerCaseMessage.includes(
|
lowerCaseMessage.includes(
|
||||||
'no endpoints found that support tool use'
|
'no endpoints found that support tool use'
|
||||||
@@ -437,14 +424,9 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
) {
|
) {
|
||||||
const specificErrorMsg = `Model '${modelId || 'unknown'}' via provider '${providerName || 'unknown'}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`;
|
const specificErrorMsg = `Model '${modelId || 'unknown'}' via provider '${providerName || 'unknown'}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`;
|
||||||
log('error', `[Tool Support Error] ${specificErrorMsg}`);
|
log('error', `[Tool Support Error] ${specificErrorMsg}`);
|
||||||
// Throw a more specific error immediately, breaking the fallback loop for this specific issue.
|
|
||||||
// Using a generic Error for simplicity, could use a custom ConfigurationError.
|
|
||||||
throw new Error(specificErrorMsg);
|
throw new Error(specificErrorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END ADDED ---
|
|
||||||
|
|
||||||
// Continue to the next role in the sequence if it wasn't a specific tool support error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +449,6 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
* @returns {Promise<string>} The generated text content.
|
* @returns {Promise<string>} The generated text content.
|
||||||
*/
|
*/
|
||||||
async function generateTextService(params) {
|
async function generateTextService(params) {
|
||||||
// Now directly returns the text string or throws error
|
|
||||||
return _unifiedServiceRunner('generateText', params);
|
return _unifiedServiceRunner('generateText', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +465,6 @@ async function generateTextService(params) {
|
|||||||
* @returns {Promise<ReadableStream<string>>} A readable stream of text deltas.
|
* @returns {Promise<ReadableStream<string>>} A readable stream of text deltas.
|
||||||
*/
|
*/
|
||||||
async function streamTextService(params) {
|
async function streamTextService(params) {
|
||||||
// Now directly returns the stream object or throws error
|
|
||||||
return _unifiedServiceRunner('streamText', params);
|
return _unifiedServiceRunner('streamText', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,7 +480,6 @@ async function streamTextService(params) {
|
|||||||
* @param {string} [params.systemPrompt] - Optional system prompt.
|
* @param {string} [params.systemPrompt] - Optional system prompt.
|
||||||
* @param {string} [params.objectName='generated_object'] - Name for object/tool.
|
* @param {string} [params.objectName='generated_object'] - Name for object/tool.
|
||||||
* @param {number} [params.maxRetries=3] - Max retries for object generation.
|
* @param {number} [params.maxRetries=3] - Max retries for object generation.
|
||||||
* // Other specific generateObject params can be included here.
|
|
||||||
* @returns {Promise<object>} The generated object matching the schema.
|
* @returns {Promise<object>} The generated object matching the schema.
|
||||||
*/
|
*/
|
||||||
async function generateObjectService(params) {
|
async function generateObjectService(params) {
|
||||||
@@ -509,7 +488,6 @@ async function generateObjectService(params) {
|
|||||||
maxRetries: 3
|
maxRetries: 3
|
||||||
};
|
};
|
||||||
const combinedParams = { ...defaults, ...params };
|
const combinedParams = { ...defaults, ...params };
|
||||||
// Now directly returns the generated object or throws error
|
|
||||||
return _unifiedServiceRunner('generateObject', combinedParams);
|
return _unifiedServiceRunner('generateObject', combinedParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import boxen from 'boxen';
|
import boxen from 'boxen';
|
||||||
// Remove Anthropic import if client is no longer initialized globally
|
|
||||||
// import { Anthropic } from '@anthropic-ai/sdk';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
log,
|
log,
|
||||||
@@ -23,11 +21,6 @@ import { displayBanner } from './ui.js';
|
|||||||
|
|
||||||
import { generateTaskFiles } from './task-manager.js';
|
import { generateTaskFiles } from './task-manager.js';
|
||||||
|
|
||||||
// Remove global Anthropic client initialization
|
|
||||||
// const anthropic = new Anthropic({
|
|
||||||
// apiKey: process.env.ANTHROPIC_API_KEY
|
|
||||||
// });
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a dependency to a task
|
* Add a dependency to a task
|
||||||
* @param {string} tasksPath - Path to the tasks.json file
|
* @param {string} tasksPath - Path to the tasks.json file
|
||||||
|
|||||||
@@ -265,7 +265,7 @@
|
|||||||
"max_tokens": 1048576
|
"max_tokens": 1048576
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "google/gemini-2.5-pro-exp-03-25:free",
|
"id": "google/gemini-2.5-pro-exp-03-25",
|
||||||
"swe_score": 0,
|
"swe_score": 0,
|
||||||
"cost_per_1m_tokens": { "input": 0, "output": 0 },
|
"cost_per_1m_tokens": { "input": 0, "output": 0 },
|
||||||
"allowed_roles": ["main", "fallback"],
|
"allowed_roles": ["main", "fallback"],
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ Do not include any explanatory text, markdown formatting, or code block markers
|
|||||||
* @param {Object} options Command options
|
* @param {Object} options Command options
|
||||||
* @param {string} options.file - Path to tasks file
|
* @param {string} options.file - Path to tasks file
|
||||||
* @param {string} options.output - Path to report output file
|
* @param {string} options.output - Path to report output file
|
||||||
* @param {string} [options.model] - Deprecated: Model override (ignored)
|
|
||||||
* @param {string|number} [options.threshold] - Complexity threshold
|
* @param {string|number} [options.threshold] - Complexity threshold
|
||||||
* @param {boolean} [options.research] - Use research role
|
* @param {boolean} [options.research] - Use research role
|
||||||
* @param {Object} [options._filteredTasksData] - Pre-filtered task data (internal use)
|
* @param {Object} [options._filteredTasksData] - Pre-filtered task data (internal use)
|
||||||
|
|||||||
@@ -48,9 +48,7 @@ export async function generateXaiText({
|
|||||||
model: client(modelId), // Correct model invocation
|
model: client(modelId), // Correct model invocation
|
||||||
messages: messages,
|
messages: messages,
|
||||||
maxTokens: maxTokens,
|
maxTokens: maxTokens,
|
||||||
temperature: temperature,
|
temperature: temperature
|
||||||
// Add reasoningEffort or other xAI specific options via providerOptions if needed
|
|
||||||
providerOptions: { xai: { reasoningEffort: 'high' } }
|
|
||||||
});
|
});
|
||||||
log(
|
log(
|
||||||
'debug',
|
'debug',
|
||||||
|
|||||||
19
tasks/task_074.txt
Normal file
19
tasks/task_074.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Task ID: 74
|
||||||
|
# Title: PR Review: better-model-management
|
||||||
|
# Status: done
|
||||||
|
# Dependencies: None
|
||||||
|
# Priority: medium
|
||||||
|
# Description: will add subtasks
|
||||||
|
# Details:
|
||||||
|
|
||||||
|
|
||||||
|
# Test Strategy:
|
||||||
|
|
||||||
|
|
||||||
|
# Subtasks:
|
||||||
|
## 1. pull out logWrapper into utils [done]
|
||||||
|
### Dependencies: None
|
||||||
|
### Description: its being used a lot across direct functions and repeated right now
|
||||||
|
### Details:
|
||||||
|
|
||||||
|
|
||||||
@@ -3916,6 +3916,27 @@
|
|||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"subtasks": []
|
"subtasks": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"title": "PR Review: better-model-management",
|
||||||
|
"description": "will add subtasks",
|
||||||
|
"details": "",
|
||||||
|
"testStrategy": "",
|
||||||
|
"status": "done",
|
||||||
|
"dependencies": [],
|
||||||
|
"priority": "medium",
|
||||||
|
"subtasks": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "pull out logWrapper into utils",
|
||||||
|
"description": "its being used a lot across direct functions and repeated right now",
|
||||||
|
"details": "",
|
||||||
|
"status": "done",
|
||||||
|
"dependencies": [],
|
||||||
|
"parentTaskId": 74
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Exit immediately if a command exits with a non-zero status.
|
|
||||||
set -e
|
|
||||||
# Treat unset variables as an error when substituting.
|
# Treat unset variables as an error when substituting.
|
||||||
set -u
|
set -u
|
||||||
# Prevent errors in pipelines from being masked.
|
# Prevent errors in pipelines from being masked.
|
||||||
@@ -33,6 +31,11 @@ mkdir -p "$LOG_DIR"
|
|||||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
LOG_FILE="$LOG_DIR/e2e_run_$TIMESTAMP.log"
|
LOG_FILE="$LOG_DIR/e2e_run_$TIMESTAMP.log"
|
||||||
|
|
||||||
|
# Define and create the test run directory *before* the main pipe
|
||||||
|
mkdir -p "$BASE_TEST_DIR" # Ensure base exists first
|
||||||
|
TEST_RUN_DIR="$BASE_TEST_DIR/run_$TIMESTAMP"
|
||||||
|
mkdir -p "$TEST_RUN_DIR"
|
||||||
|
|
||||||
# Echo starting message to the original terminal BEFORE the main piped block
|
# Echo starting message to the original terminal BEFORE the main piped block
|
||||||
echo "Starting E2E test. Output will be shown here and saved to: $LOG_FILE"
|
echo "Starting E2E test. Output will be shown here and saved to: $LOG_FILE"
|
||||||
echo "Running from directory: $(pwd)"
|
echo "Running from directory: $(pwd)"
|
||||||
@@ -82,6 +85,125 @@ overall_start_time=$(date +%s)
|
|||||||
echo " STEP ${test_step_count}: [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
|
echo " STEP ${test_step_count}: [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
|
||||||
echo "============================================="
|
echo "============================================="
|
||||||
}
|
}
|
||||||
|
|
||||||
|
analyze_log_with_llm() {
|
||||||
|
local log_file="$1"
|
||||||
|
local provider_summary_log="provider_add_task_summary.log" # File summarizing provider test outcomes
|
||||||
|
local api_key=""
|
||||||
|
local api_endpoint="https://api.anthropic.com/v1/messages"
|
||||||
|
local api_key_name="CLAUDE_API_KEY"
|
||||||
|
|
||||||
|
echo "" # Add a newline before analysis starts
|
||||||
|
log_info "Attempting LLM analysis of log: $log_file"
|
||||||
|
|
||||||
|
# Check for jq and curl
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
log_error "LLM Analysis requires 'jq'. Skipping analysis."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
log_error "LLM Analysis requires 'curl'. Skipping analysis."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for API Key in the TEST_RUN_DIR/.env (copied earlier)
|
||||||
|
if [ -f ".env" ]; then
|
||||||
|
# Using grep and sed for better handling of potential quotes/spaces
|
||||||
|
api_key=$(grep "^${api_key_name}=" .env | sed -e "s/^${api_key_name}=//" -e 's/^[[:space:]"]*//' -e 's/[[:space:]"]*$//')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$api_key" ]; then
|
||||||
|
log_error "${api_key_name} not found or empty in .env file in the test run directory ($(pwd)/.env). Skipping LLM analysis."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$log_file" ]; then
|
||||||
|
log_error "Log file not found: $log_file. Skipping LLM analysis."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Reading log file content..."
|
||||||
|
local log_content
|
||||||
|
# Read entire file, handle potential errors
|
||||||
|
log_content=$(cat "$log_file") || {
|
||||||
|
log_error "Failed to read log file: $log_file. Skipping LLM analysis."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare the prompt
|
||||||
|
# Using printf with %s for the log content is generally safer than direct variable expansion
|
||||||
|
local prompt_template='Analyze the following E2E test log for the task-master tool. The log contains output from various '\''task-master'\'' commands executed sequentially.\n\nYour goal is to:\n1. Verify if the key E2E steps completed successfully based on the log messages (e.g., init, parse PRD, list tasks, analyze complexity, expand task, set status, manage models, add/remove dependencies, add/update/remove tasks/subtasks, generate files).\n2. **Specifically analyze the Multi-Provider Add-Task Test Sequence:**\n a. Identify which providers were tested for `add-task`. Look for log steps like "Testing Add-Task with Provider: ..." and the summary log `'"$provider_summary_log"'`.\n b. For each tested provider, determine if `add-task` succeeded or failed. Note the created task ID if successful.\n c. Review the corresponding `add_task_show_output_<provider>_id_<id>.log` file (if created) for each successful `add-task` execution.\n d. **Compare the quality and completeness** of the task generated by each successful provider based on their `show` output. Assign a score (e.g., 1-10, 10 being best) based on relevance to the prompt, detail level, and correctness.\n e. Note any providers where `add-task` failed or where the task ID could not be extracted.\n3. Identify any general explicit "[ERROR]" messages or stack traces throughout the *entire* log.\n4. Identify any potential warnings or unusual output that might indicate a problem even if not marked as an explicit error.\n5. Provide an overall assessment of the test run'\''s health based *only* on the log content.\n\nReturn your analysis **strictly** in the following JSON format. Do not include any text outside of the JSON structure:\n\n{\n "overall_status": "Success|Failure|Warning",\n "verified_steps": [ "Initialization", "PRD Parsing", /* ...other general steps observed... */ ],\n "provider_add_task_comparison": {\n "prompt_used": "... (extract from log if possible or state 'standard auth prompt') ...",\n "provider_results": {\n "anthropic": { "status": "Success|Failure|ID_Extraction_Failed|Set_Model_Failed", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },\n "openai": { "status": "Success|Failure|...", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },\n /* ... include all tested providers ... */\n },\n "comparison_summary": "Brief overall comparison of generated tasks..."\n },\n "detected_issues": [ { "severity": "Error|Warning|Anomaly", "description": "...", "log_context": "[Optional, short snippet from log near the issue]" } ],\n "llm_summary_points": [ "Overall summary point 1", "Provider comparison highlight", "Any major issues noted" ]\n}\n\nHere is the main log content:\n\n%s'
|
||||||
|
|
||||||
|
local full_prompt
|
||||||
|
printf -v full_prompt "$prompt_template" "$log_content"
|
||||||
|
|
||||||
|
# Construct the JSON payload for Claude Messages API
|
||||||
|
# Using jq for robust JSON construction
|
||||||
|
local payload
|
||||||
|
payload=$(jq -n --arg prompt "$full_prompt" '{
|
||||||
|
"model": "claude-3-7-sonnet-20250219",
|
||||||
|
"max_tokens": 10000,
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": $prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.0
|
||||||
|
}') || {
|
||||||
|
log_error "Failed to create JSON payload using jq."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info "Sending request to LLM API endpoint: $api_endpoint ..."
|
||||||
|
local response_raw response_http_code response_body
|
||||||
|
# Capture body and HTTP status code separately
|
||||||
|
response_raw=$(curl -s -w "\nHTTP_STATUS_CODE:%{http_code}" -X POST "$api_endpoint" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: $api_key" \
|
||||||
|
-H "anthropic-version: 2023-06-01" \
|
||||||
|
--data "$payload")
|
||||||
|
|
||||||
|
# Extract status code and body
|
||||||
|
response_http_code=$(echo "$response_raw" | grep '^HTTP_STATUS_CODE:' | sed 's/HTTP_STATUS_CODE://')
|
||||||
|
response_body=$(echo "$response_raw" | sed '$d') # Remove last line (status code)
|
||||||
|
|
||||||
|
if [ "$response_http_code" != "200" ]; then
|
||||||
|
log_error "LLM API call failed with HTTP status $response_http_code."
|
||||||
|
log_error "Response Body: $response_body"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$response_body" ]; then
|
||||||
|
log_error "LLM API call returned empty response body."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Received LLM response (HTTP 200). Parsing analysis JSON..."
|
||||||
|
|
||||||
|
# Extract the analysis JSON string from the API response (adjust jq path if needed)
|
||||||
|
local analysis_json_string
|
||||||
|
analysis_json_string=$(echo "$response_body" | jq -r '.content[0].text' 2>/dev/null) # Assumes Messages API structure
|
||||||
|
|
||||||
|
if [ -z "$analysis_json_string" ]; then
|
||||||
|
log_error "Failed to extract 'content[0].text' from LLM response JSON."
|
||||||
|
log_error "Full API response body: $response_body"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate and pretty-print the extracted JSON
|
||||||
|
if ! echo "$analysis_json_string" | jq -e . > /dev/null 2>&1; then
|
||||||
|
log_error "Extracted content from LLM is not valid JSON."
|
||||||
|
log_error "Raw extracted content: $analysis_json_string"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "LLM analysis completed successfully."
|
||||||
|
echo ""
|
||||||
|
echo "--- LLM Analysis ---"
|
||||||
|
# Pretty print the JSON analysis
|
||||||
|
echo "$analysis_json_string" | jq '.'
|
||||||
|
echo "--------------------"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
# --- Test Setup (Output to tee) ---
|
# --- Test Setup (Output to tee) ---
|
||||||
@@ -95,12 +217,9 @@ overall_start_time=$(date +%s)
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$BASE_TEST_DIR"
|
|
||||||
log_info "Ensured base test directory exists: $BASE_TEST_DIR"
|
log_info "Ensured base test directory exists: $BASE_TEST_DIR"
|
||||||
|
|
||||||
TEST_RUN_DIR="$BASE_TEST_DIR/run_$TIMESTAMP"
|
log_info "Using test run directory (created earlier): $TEST_RUN_DIR"
|
||||||
mkdir -p "$TEST_RUN_DIR"
|
|
||||||
log_info "Created test run directory: $TEST_RUN_DIR"
|
|
||||||
|
|
||||||
# Check if source .env file exists
|
# Check if source .env file exists
|
||||||
if [ ! -f "$MAIN_ENV_FILE" ]; then
|
if [ ! -f "$MAIN_ENV_FILE" ]; then
|
||||||
@@ -209,8 +328,103 @@ overall_start_time=$(date +%s)
|
|||||||
log_step "Checking final model configuration"
|
log_step "Checking final model configuration"
|
||||||
task-master models > models_final_config.log
|
task-master models > models_final_config.log
|
||||||
log_success "Final model config saved to models_final_config.log"
|
log_success "Final model config saved to models_final_config.log"
|
||||||
|
|
||||||
|
log_step "Resetting main model to default (Claude Sonnet) before provider tests"
|
||||||
|
task-master models --set-main claude-3-7-sonnet-20250219
|
||||||
|
log_success "Main model reset to claude-3-7-sonnet-20250219."
|
||||||
|
|
||||||
# === End Model Commands Test ===
|
# === End Model Commands Test ===
|
||||||
|
|
||||||
|
# === Multi-Provider Add-Task Test ===
|
||||||
|
log_step "Starting Multi-Provider Add-Task Test Sequence"
|
||||||
|
|
||||||
|
# Define providers, models, and flags
|
||||||
|
# Array order matters: providers[i] corresponds to models[i] and flags[i]
|
||||||
|
declare -a providers=("anthropic" "openai" "google" "perplexity" "xai" "openrouter")
|
||||||
|
declare -a models=(
|
||||||
|
"claude-3-7-sonnet-20250219"
|
||||||
|
"gpt-4o"
|
||||||
|
"gemini-2.5-pro-exp-03-25"
|
||||||
|
"sonar-pro"
|
||||||
|
"grok-3"
|
||||||
|
"anthropic/claude-3.7-sonnet" # OpenRouter uses Claude 3.7
|
||||||
|
)
|
||||||
|
# Flags: Add provider-specific flags here, e.g., --openrouter. Use empty string if none.
|
||||||
|
declare -a flags=("" "" "" "" "" "--openrouter")
|
||||||
|
|
||||||
|
# Consistent prompt for all providers
|
||||||
|
add_task_prompt="Create a task to implement user authentication using OAuth 2.0 with Google as the provider. Include steps for registering the app, handling the callback, and storing user sessions."
|
||||||
|
log_info "Using consistent prompt for add-task tests: \"$add_task_prompt\""
|
||||||
|
|
||||||
|
for i in "${!providers[@]}"; do
|
||||||
|
provider="${providers[$i]}"
|
||||||
|
model="${models[$i]}"
|
||||||
|
flag="${flags[$i]}"
|
||||||
|
|
||||||
|
log_step "Testing Add-Task with Provider: $provider (Model: $model)"
|
||||||
|
|
||||||
|
# 1. Set the main model for this provider
|
||||||
|
log_info "Setting main model to $model for $provider ${flag:+using flag $flag}..."
|
||||||
|
set_model_cmd="task-master models --set-main \"$model\" $flag"
|
||||||
|
echo "Executing: $set_model_cmd"
|
||||||
|
if eval $set_model_cmd; then
|
||||||
|
log_success "Successfully set main model for $provider."
|
||||||
|
else
|
||||||
|
log_error "Failed to set main model for $provider. Skipping add-task for this provider."
|
||||||
|
# Optionally save failure info here if needed for LLM analysis
|
||||||
|
echo "Provider $provider set-main FAILED" >> provider_add_task_summary.log
|
||||||
|
continue # Skip to the next provider
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Run add-task
|
||||||
|
log_info "Running add-task with prompt..."
|
||||||
|
add_task_output_file="add_task_raw_output_${provider}.log"
|
||||||
|
# Run add-task and capture ALL output (stdout & stderr) to a file AND a variable
|
||||||
|
add_task_cmd_output=$(task-master add-task --prompt "$add_task_prompt" 2>&1 | tee "$add_task_output_file")
|
||||||
|
add_task_exit_code=${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
# 3. Check for success and extract task ID
|
||||||
|
new_task_id=""
|
||||||
|
if [ $add_task_exit_code -eq 0 ] && echo "$add_task_cmd_output" | grep -q "Successfully added task with ID:"; then
|
||||||
|
# Attempt to extract the ID (adjust grep/sed/awk as needed based on actual output format)
|
||||||
|
new_task_id=$(echo "$add_task_cmd_output" | grep "Successfully added task with ID:" | sed 's/.*Successfully added task with ID: \([0-9.]\+\).*/\1/')
|
||||||
|
if [ -n "$new_task_id" ]; then
|
||||||
|
log_success "Add-task succeeded for $provider. New task ID: $new_task_id"
|
||||||
|
echo "Provider $provider add-task SUCCESS (ID: $new_task_id)" >> provider_add_task_summary.log
|
||||||
|
else
|
||||||
|
# Succeeded but couldn't parse ID - treat as warning/anomaly
|
||||||
|
log_error "Add-task command succeeded for $provider, but failed to extract task ID from output."
|
||||||
|
echo "Provider $provider add-task SUCCESS (ID extraction FAILED)" >> provider_add_task_summary.log
|
||||||
|
new_task_id="UNKNOWN_ID_EXTRACTION_FAILED"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "Add-task command failed for $provider (Exit Code: $add_task_exit_code). See $add_task_output_file for details."
|
||||||
|
echo "Provider $provider add-task FAILED (Exit Code: $add_task_exit_code)" >> provider_add_task_summary.log
|
||||||
|
new_task_id="FAILED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Run task show if ID was obtained (even if extraction failed, use placeholder)
|
||||||
|
if [ "$new_task_id" != "FAILED" ] && [ "$new_task_id" != "UNKNOWN_ID_EXTRACTION_FAILED" ]; then
|
||||||
|
log_info "Running task show for new task ID: $new_task_id"
|
||||||
|
show_output_file="add_task_show_output_${provider}_id_${new_task_id}.log"
|
||||||
|
if task-master show "$new_task_id" > "$show_output_file"; then
|
||||||
|
log_success "Task show output saved to $show_output_file"
|
||||||
|
else
|
||||||
|
log_error "task show command failed for ID $new_task_id. Check log."
|
||||||
|
# Still keep the file, it might contain error output
|
||||||
|
fi
|
||||||
|
elif [ "$new_task_id" == "UNKNOWN_ID_EXTRACTION_FAILED" ]; then
|
||||||
|
log_info "Skipping task show for $provider due to ID extraction failure."
|
||||||
|
else
|
||||||
|
log_info "Skipping task show for $provider due to add-task failure."
|
||||||
|
fi
|
||||||
|
|
||||||
|
done # End of provider loop
|
||||||
|
|
||||||
|
log_step "Finished Multi-Provider Add-Task Test Sequence"
|
||||||
|
echo "Provider add-task summary log available at: provider_add_task_summary.log"
|
||||||
|
# === End Multi-Provider Add-Task Test ===
|
||||||
|
|
||||||
log_step "Listing tasks again (final)"
|
log_step "Listing tasks again (final)"
|
||||||
task-master list --with-subtasks > task_list_final.log
|
task-master list --with-subtasks > task_list_final.log
|
||||||
log_success "Final task list saved to task_list_final.log"
|
log_success "Final task list saved to task_list_final.log"
|
||||||
@@ -386,4 +600,26 @@ else
|
|||||||
fi
|
fi
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
|
|
||||||
|
# --- Attempt LLM Analysis ---
|
||||||
|
echo "DEBUG: Entering LLM Analysis section..."
|
||||||
|
# Run this *after* the main execution block and tee pipe finish writing the log file
|
||||||
|
# It will read the completed log file and append its output to the terminal (and the log via subsequent writes if tee is still active, though it shouldn't be)
|
||||||
|
# Change directory back into the test run dir where .env is located
|
||||||
|
if [ -d "$TEST_RUN_DIR" ]; then
|
||||||
|
echo "DEBUG: Found TEST_RUN_DIR: $TEST_RUN_DIR. Attempting cd..."
|
||||||
|
cd "$TEST_RUN_DIR"
|
||||||
|
echo "DEBUG: Changed directory to $(pwd). Calling analyze_log_with_llm..."
|
||||||
|
analyze_log_with_llm "$LOG_FILE"
|
||||||
|
echo "DEBUG: analyze_log_with_llm function call finished."
|
||||||
|
# Optional: cd back again if needed, though script is ending
|
||||||
|
# cd "$ORIGINAL_DIR"
|
||||||
|
else
|
||||||
|
# Use log_error format even outside the pipe for consistency
|
||||||
|
current_time_for_error=$(date +%s)
|
||||||
|
elapsed_seconds_for_error=$((current_time_for_error - overall_start_time)) # Use overall start time
|
||||||
|
formatted_duration_for_error=$(_format_duration "$elapsed_seconds_for_error")
|
||||||
|
echo "[ERROR] [$formatted_duration_for_error] $(date +"%Y-%m-%d %H:%M:%S") Test run directory $TEST_RUN_DIR not found. Cannot perform LLM analysis." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DEBUG: Reached end of script before final exit."
|
||||||
exit $EXIT_CODE # Exit with the status of the main script block
|
exit $EXIT_CODE # Exit with the status of the main script block
|
||||||
@@ -1,550 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
import path from 'path'; // Needed for mocking fs
|
|
||||||
|
|
||||||
// --- Mock Vercel AI SDK Modules ---
|
|
||||||
// Mock implementations - they just need to be callable and return a basic object
|
|
||||||
const mockCreateOpenAI = jest.fn(() => ({ provider: 'openai', type: 'mock' }));
|
|
||||||
const mockCreateAnthropic = jest.fn(() => ({
|
|
||||||
provider: 'anthropic',
|
|
||||||
type: 'mock'
|
|
||||||
}));
|
|
||||||
const mockCreateGoogle = jest.fn(() => ({ provider: 'google', type: 'mock' }));
|
|
||||||
const mockCreatePerplexity = jest.fn(() => ({
|
|
||||||
provider: 'perplexity',
|
|
||||||
type: 'mock'
|
|
||||||
}));
|
|
||||||
const mockCreateOllama = jest.fn(() => ({ provider: 'ollama', type: 'mock' }));
|
|
||||||
const mockCreateMistral = jest.fn(() => ({
|
|
||||||
provider: 'mistral',
|
|
||||||
type: 'mock'
|
|
||||||
}));
|
|
||||||
const mockCreateAzure = jest.fn(() => ({ provider: 'azure', type: 'mock' }));
|
|
||||||
const mockCreateXai = jest.fn(() => ({ provider: 'xai', type: 'mock' }));
|
|
||||||
// jest.unstable_mockModule('@ai-sdk/grok', () => ({
|
|
||||||
// createGrok: mockCreateGrok
|
|
||||||
// }));
|
|
||||||
const mockCreateOpenRouter = jest.fn(() => ({
|
|
||||||
provider: 'openrouter',
|
|
||||||
type: 'mock'
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('@ai-sdk/openai', () => ({
|
|
||||||
createOpenAI: mockCreateOpenAI
|
|
||||||
}));
|
|
||||||
jest.unstable_mockModule('@ai-sdk/anthropic', () => ({
|
|
||||||
createAnthropic: mockCreateAnthropic
|
|
||||||
}));
|
|
||||||
jest.unstable_mockModule('@ai-sdk/google', () => ({
|
|
||||||
createGoogle: mockCreateGoogle
|
|
||||||
}));
|
|
||||||
jest.unstable_mockModule('@ai-sdk/perplexity', () => ({
|
|
||||||
createPerplexity: mockCreatePerplexity
|
|
||||||
}));
|
|
||||||
jest.unstable_mockModule('ollama-ai-provider', () => ({
|
|
||||||
createOllama: mockCreateOllama
|
|
||||||
}));
|
|
||||||
jest.unstable_mockModule('@ai-sdk/mistral', () => ({
|
|
||||||
createMistral: mockCreateMistral
|
|
||||||
}));
|
|
||||||
jest.unstable_mockModule('@ai-sdk/azure', () => ({
|
|
||||||
createAzure: mockCreateAzure
|
|
||||||
}));
|
|
||||||
jest.unstable_mockModule('@ai-sdk/xai', () => ({
|
|
||||||
createXai: mockCreateXai
|
|
||||||
}));
|
|
||||||
// jest.unstable_mockModule('@ai-sdk/openrouter', () => ({
|
|
||||||
// createOpenRouter: mockCreateOpenRouter
|
|
||||||
// }));
|
|
||||||
jest.unstable_mockModule('@openrouter/ai-sdk-provider', () => ({
|
|
||||||
createOpenRouter: mockCreateOpenRouter
|
|
||||||
}));
|
|
||||||
// TODO: Mock other providers (OpenRouter, Grok) when added
|
|
||||||
|
|
||||||
// --- Mock Config Manager ---
|
|
||||||
const mockGetProviderAndModelForRole = jest.fn();
|
|
||||||
const mockFindProjectRoot = jest.fn();
|
|
||||||
jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
|
|
||||||
getProviderAndModelForRole: mockGetProviderAndModelForRole,
|
|
||||||
findProjectRoot: mockFindProjectRoot
|
|
||||||
}));
|
|
||||||
|
|
||||||
// --- Mock File System (for supported-models.json loading) ---
|
|
||||||
const mockFsExistsSync = jest.fn();
|
|
||||||
const mockFsReadFileSync = jest.fn();
|
|
||||||
jest.unstable_mockModule('fs', () => ({
|
|
||||||
__esModule: true, // Important for ES modules with default exports
|
|
||||||
default: {
|
|
||||||
// Provide the default export expected by `import fs from 'fs'`
|
|
||||||
existsSync: mockFsExistsSync,
|
|
||||||
readFileSync: mockFsReadFileSync
|
|
||||||
},
|
|
||||||
// Also provide named exports if they were directly imported elsewhere, though not needed here
|
|
||||||
existsSync: mockFsExistsSync,
|
|
||||||
readFileSync: mockFsReadFileSync
|
|
||||||
}));
|
|
||||||
|
|
||||||
// --- Mock path (specifically path.join used for supported-models.json) ---
|
|
||||||
const mockPathJoin = jest.fn((...args) => args.join(path.sep)); // Simple mock
|
|
||||||
const actualPath = jest.requireActual('path'); // Get the actual path module
|
|
||||||
jest.unstable_mockModule('path', () => ({
|
|
||||||
__esModule: true, // Indicate ES module mock
|
|
||||||
default: {
|
|
||||||
// Provide the default export
|
|
||||||
...actualPath, // Spread actual functions
|
|
||||||
join: mockPathJoin // Override join
|
|
||||||
},
|
|
||||||
// Also provide named exports for consistency
|
|
||||||
...actualPath,
|
|
||||||
join: mockPathJoin
|
|
||||||
}));
|
|
||||||
|
|
||||||
// --- Define Mock Data ---
|
|
||||||
const mockSupportedModels = {
|
|
||||||
openai: [
|
|
||||||
{ id: 'gpt-4o', allowed_roles: ['main', 'fallback'] },
|
|
||||||
{ id: 'gpt-3.5-turbo', allowed_roles: ['main', 'fallback'] }
|
|
||||||
],
|
|
||||||
anthropic: [
|
|
||||||
{ id: 'claude-3.5-sonnet-20240620', allowed_roles: ['main'] },
|
|
||||||
{ id: 'claude-3-haiku-20240307', allowed_roles: ['fallback'] }
|
|
||||||
],
|
|
||||||
perplexity: [{ id: 'sonar-pro', allowed_roles: ['research'] }],
|
|
||||||
ollama: [{ id: 'llama3', allowed_roles: ['main', 'fallback'] }],
|
|
||||||
google: [{ id: 'gemini-pro', allowed_roles: ['main'] }],
|
|
||||||
mistral: [{ id: 'mistral-large-latest', allowed_roles: ['main'] }],
|
|
||||||
azure: [{ id: 'azure-gpt4o', allowed_roles: ['main'] }],
|
|
||||||
xai: [{ id: 'grok-basic', allowed_roles: ['main'] }],
|
|
||||||
openrouter: [{ id: 'openrouter-model', allowed_roles: ['main'] }]
|
|
||||||
// Add other providers as needed for tests
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Import the module AFTER mocks ---
|
|
||||||
const { getClient, clearClientCache, _resetSupportedModelsCache } =
|
|
||||||
await import('../../scripts/modules/ai-client-factory.js');
|
|
||||||
|
|
||||||
describe('AI Client Factory (Role-Based)', () => {
|
|
||||||
const OLD_ENV = process.env;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Reset state before each test
|
|
||||||
clearClientCache(); // Use the correct function name
|
|
||||||
_resetSupportedModelsCache(); // Reset the models cache
|
|
||||||
mockFsExistsSync.mockClear();
|
|
||||||
mockFsReadFileSync.mockClear();
|
|
||||||
mockGetProviderAndModelForRole.mockClear(); // Reset this mock too
|
|
||||||
|
|
||||||
// Reset environment to avoid test pollution
|
|
||||||
process.env = { ...OLD_ENV };
|
|
||||||
|
|
||||||
// Default mock implementations (can be overridden)
|
|
||||||
mockFindProjectRoot.mockReturnValue('/fake/project/root');
|
|
||||||
mockPathJoin.mockImplementation((...args) => args.join(actualPath.sep)); // Use actualPath.sep
|
|
||||||
|
|
||||||
// Default FS mocks for model/config loading
|
|
||||||
mockFsExistsSync.mockImplementation((filePath) => {
|
|
||||||
// Default to true for the files we expect to load
|
|
||||||
if (filePath.endsWith('supported-models.json')) return true;
|
|
||||||
// Add other expected files if necessary
|
|
||||||
return false; // Default to false for others
|
|
||||||
});
|
|
||||||
mockFsReadFileSync.mockImplementation((filePath) => {
|
|
||||||
if (filePath.endsWith('supported-models.json')) {
|
|
||||||
return JSON.stringify(mockSupportedModels);
|
|
||||||
}
|
|
||||||
// Throw if an unexpected file is read
|
|
||||||
throw new Error(`Unexpected readFileSync call in test: ${filePath}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Default config mock
|
|
||||||
mockGetProviderAndModelForRole.mockImplementation((role) => {
|
|
||||||
if (role === 'main') return { provider: 'openai', modelId: 'gpt-4o' };
|
|
||||||
if (role === 'research')
|
|
||||||
return { provider: 'perplexity', modelId: 'sonar-pro' };
|
|
||||||
if (role === 'fallback')
|
|
||||||
return { provider: 'anthropic', modelId: 'claude-3-haiku-20240307' };
|
|
||||||
return {}; // Default empty for unconfigured roles
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set default required env vars (can be overridden in tests)
|
|
||||||
process.env.OPENAI_API_KEY = 'test-openai-key';
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
|
|
||||||
process.env.PERPLEXITY_API_KEY = 'test-perplexity-key';
|
|
||||||
process.env.GOOGLE_API_KEY = 'test-google-key';
|
|
||||||
process.env.MISTRAL_API_KEY = 'test-mistral-key';
|
|
||||||
process.env.AZURE_OPENAI_API_KEY = 'test-azure-key';
|
|
||||||
process.env.AZURE_OPENAI_ENDPOINT = 'test-azure-endpoint';
|
|
||||||
process.env.XAI_API_KEY = 'test-xai-key';
|
|
||||||
process.env.OPENROUTER_API_KEY = 'test-openrouter-key';
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
process.env = OLD_ENV;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error if role is missing', () => {
|
|
||||||
expect(() => getClient()).toThrow(
|
|
||||||
"Client role ('main', 'research', 'fallback') must be specified."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error if config manager fails to get role config', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockImplementation((role) => {
|
|
||||||
if (role === 'main') throw new Error('Config file not found');
|
|
||||||
});
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
"Failed to get configuration for role 'main': Config file not found"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error if config manager returns undefined provider/model', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({}); // Empty object
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
"Could not determine provider or modelId for role 'main'"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error if configured model is not supported for the role', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'anthropic',
|
|
||||||
modelId: 'claude-3.5-sonnet-20240620' // Only allowed for 'main' in mock data
|
|
||||||
});
|
|
||||||
expect(() => getClient('research')).toThrow(
|
|
||||||
/Model 'claude-3.5-sonnet-20240620' from provider 'anthropic' is either not supported or not allowed for the 'research' role/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error if configured model is not found in supported list', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'openai',
|
|
||||||
modelId: 'gpt-unknown'
|
|
||||||
});
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
/Model 'gpt-unknown' from provider 'openai' is either not supported or not allowed for the 'main' role/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error if configured provider is not found in supported list', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'unknown-provider',
|
|
||||||
modelId: 'some-model'
|
|
||||||
});
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
/Model 'some-model' from provider 'unknown-provider' is either not supported or not allowed for the 'main' role/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should skip model validation if supported-models.json is not found', () => {
|
|
||||||
mockFsExistsSync.mockReturnValue(false); // Simulate file not found
|
|
||||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); // Suppress warning
|
|
||||||
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'openai',
|
|
||||||
modelId: 'gpt-any' // Doesn't matter, validation skipped
|
|
||||||
});
|
|
||||||
process.env.OPENAI_API_KEY = 'test-key';
|
|
||||||
|
|
||||||
expect(() => getClient('main')).not.toThrow(); // Should not throw validation error
|
|
||||||
expect(mockCreateOpenAI).toHaveBeenCalled();
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('Skipping model validation')
|
|
||||||
);
|
|
||||||
consoleWarnSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw environment validation error', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'openai',
|
|
||||||
modelId: 'gpt-4o'
|
|
||||||
});
|
|
||||||
delete process.env.OPENAI_API_KEY; // Trigger missing env var
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
// Expect the original error message from validateEnvironment
|
|
||||||
/Missing environment variables for provider 'openai': OPENAI_API_KEY\. Please check your \.env file or session configuration\./
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should successfully create client using config and process.env', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'openai',
|
|
||||||
modelId: 'gpt-4o'
|
|
||||||
});
|
|
||||||
process.env.OPENAI_API_KEY = 'env-key';
|
|
||||||
|
|
||||||
const client = getClient('main');
|
|
||||||
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockGetProviderAndModelForRole).toHaveBeenCalledWith('main');
|
|
||||||
expect(mockCreateOpenAI).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ apiKey: 'env-key', model: 'gpt-4o' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should successfully create client using config and session.env', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'anthropic',
|
|
||||||
modelId: 'claude-3.5-sonnet-20240620'
|
|
||||||
});
|
|
||||||
delete process.env.ANTHROPIC_API_KEY;
|
|
||||||
const session = { env: { ANTHROPIC_API_KEY: 'session-key' } };
|
|
||||||
|
|
||||||
const client = getClient('main', session);
|
|
||||||
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockGetProviderAndModelForRole).toHaveBeenCalledWith('main');
|
|
||||||
expect(mockCreateAnthropic).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
apiKey: 'session-key',
|
|
||||||
model: 'claude-3.5-sonnet-20240620'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should use overrideOptions when provided', () => {
|
|
||||||
process.env.PERPLEXITY_API_KEY = 'env-key';
|
|
||||||
const override = { provider: 'perplexity', modelId: 'sonar-pro' };
|
|
||||||
|
|
||||||
const client = getClient('research', null, override);
|
|
||||||
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockGetProviderAndModelForRole).not.toHaveBeenCalled(); // Config shouldn't be called
|
|
||||||
expect(mockCreatePerplexity).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ apiKey: 'env-key', model: 'sonar-pro' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw validation error even with override if role is disallowed', () => {
|
|
||||||
process.env.OPENAI_API_KEY = 'env-key';
|
|
||||||
// gpt-4o is not allowed for 'research' in mock data
|
|
||||||
const override = { provider: 'openai', modelId: 'gpt-4o' };
|
|
||||||
|
|
||||||
expect(() => getClient('research', null, override)).toThrow(
|
|
||||||
/Model 'gpt-4o' from provider 'openai' is either not supported or not allowed for the 'research' role/
|
|
||||||
);
|
|
||||||
expect(mockGetProviderAndModelForRole).not.toHaveBeenCalled();
|
|
||||||
expect(mockCreateOpenAI).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Caching Behavior (Role-Based)', () => {
|
|
||||||
test('should return cached client instance for the same provider/model derived from role', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'openai',
|
|
||||||
modelId: 'gpt-4o'
|
|
||||||
});
|
|
||||||
process.env.OPENAI_API_KEY = 'test-key';
|
|
||||||
|
|
||||||
const client1 = getClient('main');
|
|
||||||
const client2 = getClient('main'); // Same role, same config result
|
|
||||||
|
|
||||||
expect(client1).toBe(client2); // Should be the exact same instance
|
|
||||||
expect(mockGetProviderAndModelForRole).toHaveBeenCalledTimes(2); // Config lookup happens each time
|
|
||||||
expect(mockCreateOpenAI).toHaveBeenCalledTimes(1); // Instance created only once
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return different client instances for different roles if config differs', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockImplementation((role) => {
|
|
||||||
if (role === 'main') return { provider: 'openai', modelId: 'gpt-4o' };
|
|
||||||
if (role === 'research')
|
|
||||||
return { provider: 'perplexity', modelId: 'sonar-pro' };
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
process.env.OPENAI_API_KEY = 'test-key-1';
|
|
||||||
process.env.PERPLEXITY_API_KEY = 'test-key-2';
|
|
||||||
|
|
||||||
const client1 = getClient('main');
|
|
||||||
const client2 = getClient('research');
|
|
||||||
|
|
||||||
expect(client1).not.toBe(client2);
|
|
||||||
expect(mockCreateOpenAI).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockCreatePerplexity).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return same client instance if different roles resolve to same provider/model', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockImplementation((role) => {
|
|
||||||
// Both roles point to the same model
|
|
||||||
return { provider: 'openai', modelId: 'gpt-4o' };
|
|
||||||
});
|
|
||||||
process.env.OPENAI_API_KEY = 'test-key';
|
|
||||||
|
|
||||||
const client1 = getClient('main');
|
|
||||||
const client2 = getClient('fallback'); // Different role, same config result
|
|
||||||
|
|
||||||
expect(client1).toBe(client2); // Should be the exact same instance
|
|
||||||
expect(mockCreateOpenAI).toHaveBeenCalledTimes(1); // Instance created only once
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add tests for specific providers
|
|
||||||
describe('Specific Provider Instantiation', () => {
|
|
||||||
test('should successfully create Google client with GOOGLE_API_KEY', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'google',
|
|
||||||
modelId: 'gemini-pro'
|
|
||||||
}); // Assume gemini-pro is supported
|
|
||||||
process.env.GOOGLE_API_KEY = 'test-google-key';
|
|
||||||
const client = getClient('main');
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockCreateGoogle).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ apiKey: 'test-google-key' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw environment error if GOOGLE_API_KEY is missing', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'google',
|
|
||||||
modelId: 'gemini-pro'
|
|
||||||
});
|
|
||||||
delete process.env.GOOGLE_API_KEY;
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
/Missing environment variables for provider 'google': GOOGLE_API_KEY/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should successfully create Ollama client with OLLAMA_BASE_URL', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'ollama',
|
|
||||||
modelId: 'llama3'
|
|
||||||
}); // Use supported llama3
|
|
||||||
process.env.OLLAMA_BASE_URL = 'http://test-ollama:11434';
|
|
||||||
const client = getClient('main');
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockCreateOllama).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ baseURL: 'http://test-ollama:11434' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw environment error if OLLAMA_BASE_URL is missing', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'ollama',
|
|
||||||
modelId: 'llama3'
|
|
||||||
});
|
|
||||||
delete process.env.OLLAMA_BASE_URL;
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
/Missing environment variables for provider 'ollama': OLLAMA_BASE_URL/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should successfully create Mistral client with MISTRAL_API_KEY', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'mistral',
|
|
||||||
modelId: 'mistral-large-latest'
|
|
||||||
}); // Assume supported
|
|
||||||
process.env.MISTRAL_API_KEY = 'test-mistral-key';
|
|
||||||
const client = getClient('main');
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockCreateMistral).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ apiKey: 'test-mistral-key' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw environment error if MISTRAL_API_KEY is missing', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'mistral',
|
|
||||||
modelId: 'mistral-large-latest'
|
|
||||||
});
|
|
||||||
delete process.env.MISTRAL_API_KEY;
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
/Missing environment variables for provider 'mistral': MISTRAL_API_KEY/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should successfully create Azure client with AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'azure',
|
|
||||||
modelId: 'azure-gpt4o'
|
|
||||||
}); // Assume supported
|
|
||||||
process.env.AZURE_OPENAI_API_KEY = 'test-azure-key';
|
|
||||||
process.env.AZURE_OPENAI_ENDPOINT = 'https://test-azure.openai.azure.com';
|
|
||||||
const client = getClient('main');
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockCreateAzure).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
apiKey: 'test-azure-key',
|
|
||||||
endpoint: 'https://test-azure.openai.azure.com'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw environment error if AZURE_OPENAI_API_KEY or AZURE_OPENAI_ENDPOINT is missing', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'azure',
|
|
||||||
modelId: 'azure-gpt4o'
|
|
||||||
});
|
|
||||||
process.env.AZURE_OPENAI_API_KEY = 'test-azure-key';
|
|
||||||
delete process.env.AZURE_OPENAI_ENDPOINT;
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
/Missing environment variables for provider 'azure': AZURE_OPENAI_ENDPOINT/
|
|
||||||
);
|
|
||||||
|
|
||||||
process.env.AZURE_OPENAI_ENDPOINT = 'https://test-azure.openai.azure.com';
|
|
||||||
delete process.env.AZURE_OPENAI_API_KEY;
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
/Missing environment variables for provider 'azure': AZURE_OPENAI_API_KEY/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should successfully create xAI (Grok) client with XAI_API_KEY', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'xai',
|
|
||||||
modelId: 'grok-basic'
|
|
||||||
});
|
|
||||||
process.env.XAI_API_KEY = 'test-xai-key-specific';
|
|
||||||
const client = getClient('main');
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockCreateXai).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ apiKey: 'test-xai-key-specific' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw environment error if XAI_API_KEY is missing', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'xai',
|
|
||||||
modelId: 'grok-basic'
|
|
||||||
});
|
|
||||||
delete process.env.XAI_API_KEY;
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
/Missing environment variables for provider 'xai': XAI_API_KEY/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should successfully create OpenRouter client with OPENROUTER_API_KEY', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'openrouter',
|
|
||||||
modelId: 'openrouter-model'
|
|
||||||
});
|
|
||||||
process.env.OPENROUTER_API_KEY = 'test-openrouter-key-specific';
|
|
||||||
const client = getClient('main');
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ apiKey: 'test-openrouter-key-specific' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw environment error if OPENROUTER_API_KEY is missing', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'openrouter',
|
|
||||||
modelId: 'openrouter-model'
|
|
||||||
});
|
|
||||||
delete process.env.OPENROUTER_API_KEY;
|
|
||||||
expect(() => getClient('main')).toThrow(
|
|
||||||
/Missing environment variables for provider 'openrouter': OPENROUTER_API_KEY/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Environment Variable Precedence', () => {
|
|
||||||
test('should prioritize process.env over session.env for API keys', () => {
|
|
||||||
mockGetProviderAndModelForRole.mockReturnValue({
|
|
||||||
provider: 'openai',
|
|
||||||
modelId: 'gpt-4o'
|
|
||||||
});
|
|
||||||
process.env.OPENAI_API_KEY = 'process-env-key'; // This should be used
|
|
||||||
const session = { env: { OPENAI_API_KEY: 'session-env-key' } };
|
|
||||||
|
|
||||||
const client = getClient('main', session);
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockCreateOpenAI).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ apiKey: 'process-env-key', model: 'gpt-4o' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
/**
|
|
||||||
* ai-client-utils.test.js
|
|
||||||
* Tests for AI client utility functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { jest } from '@jest/globals';
|
|
||||||
import {
|
|
||||||
getAnthropicClientForMCP,
|
|
||||||
getPerplexityClientForMCP,
|
|
||||||
getModelConfig,
|
|
||||||
getBestAvailableAIModel,
|
|
||||||
handleClaudeError
|
|
||||||
} from '../../mcp-server/src/core/utils/ai-client-utils.js';
|
|
||||||
|
|
||||||
// Mock the Anthropic constructor
|
|
||||||
jest.mock('@anthropic-ai/sdk', () => {
|
|
||||||
return {
|
|
||||||
Anthropic: jest.fn().mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
messages: {
|
|
||||||
create: jest.fn().mockResolvedValue({})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock the OpenAI dynamic import
|
|
||||||
jest.mock('openai', () => {
|
|
||||||
return {
|
|
||||||
default: jest.fn().mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
chat: {
|
|
||||||
completions: {
|
|
||||||
create: jest.fn().mockResolvedValue({})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('AI Client Utilities', () => {
|
|
||||||
const originalEnv = process.env;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Reset process.env before each test
|
|
||||||
process.env = { ...originalEnv };
|
|
||||||
|
|
||||||
// Clear all mocks
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
// Restore process.env
|
|
||||||
process.env = originalEnv;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAnthropicClientForMCP', () => {
|
|
||||||
it('should initialize client with API key from session', () => {
|
|
||||||
// Setup
|
|
||||||
const session = {
|
|
||||||
env: {
|
|
||||||
ANTHROPIC_API_KEY: 'test-key-from-session'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mockLog = { error: jest.fn() };
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const client = getAnthropicClientForMCP(session, mockLog);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(client.messages.create).toBeDefined();
|
|
||||||
expect(mockLog.error).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fall back to process.env when session key is missing', () => {
|
|
||||||
// Setup
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key-from-env';
|
|
||||||
const session = { env: {} };
|
|
||||||
const mockLog = { error: jest.fn() };
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const client = getAnthropicClientForMCP(session, mockLog);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(mockLog.error).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error when API key is missing', () => {
|
|
||||||
// Setup
|
|
||||||
delete process.env.ANTHROPIC_API_KEY;
|
|
||||||
const session = { env: {} };
|
|
||||||
const mockLog = { error: jest.fn() };
|
|
||||||
|
|
||||||
// Execute & Verify
|
|
||||||
expect(() => getAnthropicClientForMCP(session, mockLog)).toThrow();
|
|
||||||
expect(mockLog.error).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getPerplexityClientForMCP', () => {
|
|
||||||
it('should initialize client with API key from session', async () => {
|
|
||||||
// Setup
|
|
||||||
const session = {
|
|
||||||
env: {
|
|
||||||
PERPLEXITY_API_KEY: 'test-perplexity-key'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mockLog = { error: jest.fn() };
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const client = await getPerplexityClientForMCP(session, mockLog);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(client).toBeDefined();
|
|
||||||
expect(client.chat.completions.create).toBeDefined();
|
|
||||||
expect(mockLog.error).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error when API key is missing', async () => {
|
|
||||||
// Setup
|
|
||||||
delete process.env.PERPLEXITY_API_KEY;
|
|
||||||
const session = { env: {} };
|
|
||||||
const mockLog = { error: jest.fn() };
|
|
||||||
|
|
||||||
// Execute & Verify
|
|
||||||
await expect(
|
|
||||||
getPerplexityClientForMCP(session, mockLog)
|
|
||||||
).rejects.toThrow();
|
|
||||||
expect(mockLog.error).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getModelConfig', () => {
|
|
||||||
it('should get model config from session', () => {
|
|
||||||
// Setup
|
|
||||||
const session = {
|
|
||||||
env: {
|
|
||||||
MODEL: 'claude-3-opus',
|
|
||||||
MAX_TOKENS: '8000',
|
|
||||||
TEMPERATURE: '0.5'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const config = getModelConfig(session);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(config).toEqual({
|
|
||||||
model: 'claude-3-opus',
|
|
||||||
maxTokens: 8000,
|
|
||||||
temperature: 0.5
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use default values when session values are missing', () => {
|
|
||||||
// Setup
|
|
||||||
const session = {
|
|
||||||
env: {
|
|
||||||
// No values
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const config = getModelConfig(session);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(config).toEqual({
|
|
||||||
model: 'claude-3-7-sonnet-20250219',
|
|
||||||
maxTokens: 64000,
|
|
||||||
temperature: 0.2
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow custom defaults', () => {
|
|
||||||
// Setup
|
|
||||||
const session = { env: {} };
|
|
||||||
const customDefaults = {
|
|
||||||
model: 'custom-model',
|
|
||||||
maxTokens: 2000,
|
|
||||||
temperature: 0.3
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const config = getModelConfig(session, customDefaults);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(config).toEqual(customDefaults);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getBestAvailableAIModel', () => {
|
|
||||||
it('should return Perplexity for research when available', async () => {
|
|
||||||
// Setup
|
|
||||||
const session = {
|
|
||||||
env: {
|
|
||||||
PERPLEXITY_API_KEY: 'test-perplexity-key',
|
|
||||||
ANTHROPIC_API_KEY: 'test-anthropic-key'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const result = await getBestAvailableAIModel(
|
|
||||||
session,
|
|
||||||
{ requiresResearch: true },
|
|
||||||
mockLog
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(result.type).toBe('perplexity');
|
|
||||||
expect(result.client).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return Claude when Perplexity is not available and Claude is not overloaded', async () => {
|
|
||||||
// Setup
|
|
||||||
const originalPerplexityKey = process.env.PERPLEXITY_API_KEY;
|
|
||||||
delete process.env.PERPLEXITY_API_KEY; // Make sure Perplexity is not available in process.env
|
|
||||||
|
|
||||||
const session = {
|
|
||||||
env: {
|
|
||||||
ANTHROPIC_API_KEY: 'test-anthropic-key'
|
|
||||||
// Purposely not including PERPLEXITY_API_KEY
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Execute
|
|
||||||
const result = await getBestAvailableAIModel(
|
|
||||||
session,
|
|
||||||
{ requiresResearch: true },
|
|
||||||
mockLog
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
// In our implementation, we prioritize research capability through Perplexity
|
|
||||||
// so if we're testing research but Perplexity isn't available, Claude is used
|
|
||||||
expect(result.type).toBe('claude');
|
|
||||||
expect(result.client).toBeDefined();
|
|
||||||
expect(mockLog.warn).toHaveBeenCalled(); // Warning about using Claude instead of Perplexity
|
|
||||||
} finally {
|
|
||||||
// Restore original env variables
|
|
||||||
if (originalPerplexityKey) {
|
|
||||||
process.env.PERPLEXITY_API_KEY = originalPerplexityKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fall back to Claude as last resort when overloaded', async () => {
|
|
||||||
// Setup
|
|
||||||
const session = {
|
|
||||||
env: {
|
|
||||||
ANTHROPIC_API_KEY: 'test-anthropic-key'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const result = await getBestAvailableAIModel(
|
|
||||||
session,
|
|
||||||
{ claudeOverloaded: true },
|
|
||||||
mockLog
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(result.type).toBe('claude');
|
|
||||||
expect(result.client).toBeDefined();
|
|
||||||
expect(mockLog.warn).toHaveBeenCalled(); // Warning about Claude overloaded
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error when no models are available', async () => {
|
|
||||||
// Setup
|
|
||||||
delete process.env.ANTHROPIC_API_KEY;
|
|
||||||
delete process.env.PERPLEXITY_API_KEY;
|
|
||||||
const session = { env: {} };
|
|
||||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
|
||||||
|
|
||||||
// Execute & Verify
|
|
||||||
await expect(
|
|
||||||
getBestAvailableAIModel(session, {}, mockLog)
|
|
||||||
).rejects.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleClaudeError', () => {
|
|
||||||
it('should handle overloaded error', () => {
|
|
||||||
// Setup
|
|
||||||
const error = {
|
|
||||||
type: 'error',
|
|
||||||
error: {
|
|
||||||
type: 'overloaded_error',
|
|
||||||
message: 'Claude is overloaded'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const message = handleClaudeError(error);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(message).toContain('overloaded');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle rate limit error', () => {
|
|
||||||
// Setup
|
|
||||||
const error = {
|
|
||||||
type: 'error',
|
|
||||||
error: {
|
|
||||||
type: 'rate_limit_error',
|
|
||||||
message: 'Rate limit exceeded'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const message = handleClaudeError(error);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(message).toContain('rate limit');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle timeout error', () => {
|
|
||||||
// Setup
|
|
||||||
const error = {
|
|
||||||
message: 'Request timed out after 60 seconds'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const message = handleClaudeError(error);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(message).toContain('timed out');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle generic errors', () => {
|
|
||||||
// Setup
|
|
||||||
const error = {
|
|
||||||
message: 'Something went wrong'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
const message = handleClaudeError(error);
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
expect(message).toContain('Error communicating with Claude');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
/**
|
|
||||||
* AI Services module tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { jest } from '@jest/globals';
|
|
||||||
import { parseSubtasksFromText } from '../../scripts/modules/ai-services.js';
|
|
||||||
|
|
||||||
// Create a mock log function we can check later
|
|
||||||
const mockLog = jest.fn();
|
|
||||||
|
|
||||||
// Mock dependencies
|
|
||||||
jest.mock('@anthropic-ai/sdk', () => {
|
|
||||||
const mockCreate = jest.fn().mockResolvedValue({
|
|
||||||
content: [{ text: 'AI response' }]
|
|
||||||
});
|
|
||||||
const mockAnthropicInstance = {
|
|
||||||
messages: {
|
|
||||||
create: mockCreate
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mockAnthropicConstructor = jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementation(() => mockAnthropicInstance);
|
|
||||||
return {
|
|
||||||
Anthropic: mockAnthropicConstructor
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use jest.fn() directly for OpenAI mock
|
|
||||||
const mockOpenAIInstance = {
|
|
||||||
chat: {
|
|
||||||
completions: {
|
|
||||||
create: jest.fn().mockResolvedValue({
|
|
||||||
choices: [{ message: { content: 'Perplexity response' } }]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const mockOpenAI = jest.fn().mockImplementation(() => mockOpenAIInstance);
|
|
||||||
|
|
||||||
jest.mock('openai', () => {
|
|
||||||
return { default: mockOpenAI };
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('dotenv', () => ({
|
|
||||||
config: jest.fn()
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('../../scripts/modules/utils.js', () => ({
|
|
||||||
CONFIG: {
|
|
||||||
model: 'claude-3-sonnet-20240229',
|
|
||||||
temperature: 0.7,
|
|
||||||
maxTokens: 4000
|
|
||||||
},
|
|
||||||
log: mockLog,
|
|
||||||
sanitizePrompt: jest.fn((text) => text)
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('../../scripts/modules/ui.js', () => ({
|
|
||||||
startLoadingIndicator: jest.fn().mockReturnValue('mockLoader'),
|
|
||||||
stopLoadingIndicator: jest.fn()
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock anthropic global object
|
|
||||||
global.anthropic = {
|
|
||||||
messages: {
|
|
||||||
create: jest.fn().mockResolvedValue({
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
text: '[{"id": 1, "title": "Test", "description": "Test", "dependencies": [], "details": "Test"}]'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock process.env
|
|
||||||
const originalEnv = process.env;
|
|
||||||
|
|
||||||
// Import Anthropic for testing constructor arguments
|
|
||||||
import { Anthropic } from '@anthropic-ai/sdk';
|
|
||||||
|
|
||||||
describe('AI Services Module', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
process.env = { ...originalEnv };
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
|
|
||||||
process.env.PERPLEXITY_API_KEY = 'test-perplexity-key';
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
process.env = originalEnv;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseSubtasksFromText function', () => {
|
|
||||||
test('should parse subtasks from JSON text', () => {
|
|
||||||
const text = `Here's your list of subtasks:
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Implement database schema",
|
|
||||||
"description": "Design and implement the database schema for user data",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Create tables for users, preferences, and settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Create API endpoints",
|
|
||||||
"description": "Develop RESTful API endpoints for user operations",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Implement CRUD operations for user management"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
These subtasks will help you implement the parent task efficiently.`;
|
|
||||||
|
|
||||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
expect(result[0]).toEqual({
|
|
||||||
id: 1,
|
|
||||||
title: 'Implement database schema',
|
|
||||||
description: 'Design and implement the database schema for user data',
|
|
||||||
status: 'pending',
|
|
||||||
dependencies: [],
|
|
||||||
details: 'Create tables for users, preferences, and settings',
|
|
||||||
parentTaskId: 5
|
|
||||||
});
|
|
||||||
expect(result[1]).toEqual({
|
|
||||||
id: 2,
|
|
||||||
title: 'Create API endpoints',
|
|
||||||
description: 'Develop RESTful API endpoints for user operations',
|
|
||||||
status: 'pending',
|
|
||||||
dependencies: [],
|
|
||||||
details: 'Implement CRUD operations for user management',
|
|
||||||
parentTaskId: 5
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle subtasks with dependencies', () => {
|
|
||||||
const text = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Setup React environment",
|
|
||||||
"description": "Initialize React app with necessary dependencies",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Use Create React App or Vite to set up a new project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Create component structure",
|
|
||||||
"description": "Design and implement component hierarchy",
|
|
||||||
"dependencies": [1],
|
|
||||||
"details": "Organize components by feature and reusability"
|
|
||||||
}
|
|
||||||
]`;
|
|
||||||
|
|
||||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
expect(result[0].dependencies).toEqual([]);
|
|
||||||
expect(result[1].dependencies).toEqual([1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle complex dependency lists', () => {
|
|
||||||
const text = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Setup database",
|
|
||||||
"description": "Initialize database structure",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Set up PostgreSQL database"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Create models",
|
|
||||||
"description": "Implement data models",
|
|
||||||
"dependencies": [1],
|
|
||||||
"details": "Define Prisma models"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Implement controllers",
|
|
||||||
"description": "Create API controllers",
|
|
||||||
"dependencies": [1, 2],
|
|
||||||
"details": "Build controllers for all endpoints"
|
|
||||||
}
|
|
||||||
]`;
|
|
||||||
|
|
||||||
const result = parseSubtasksFromText(text, 1, 3, 5);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(3);
|
|
||||||
expect(result[2].dependencies).toEqual([1, 2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw an error for empty text', () => {
|
|
||||||
const emptyText = '';
|
|
||||||
|
|
||||||
expect(() => parseSubtasksFromText(emptyText, 1, 2, 5)).toThrow(
|
|
||||||
'Empty text provided, cannot parse subtasks'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should normalize subtask IDs', () => {
|
|
||||||
const text = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"title": "First task with incorrect ID",
|
|
||||||
"description": "First description",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "First details"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 20,
|
|
||||||
"title": "Second task with incorrect ID",
|
|
||||||
"description": "Second description",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "Second details"
|
|
||||||
}
|
|
||||||
]`;
|
|
||||||
|
|
||||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
expect(result[0].id).toBe(1); // Should normalize to starting ID
|
|
||||||
expect(result[1].id).toBe(2); // Should normalize to starting ID + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should convert string dependencies to numbers', () => {
|
|
||||||
const text = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "First task",
|
|
||||||
"description": "First description",
|
|
||||||
"dependencies": [],
|
|
||||||
"details": "First details"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Second task",
|
|
||||||
"description": "Second description",
|
|
||||||
"dependencies": ["1"],
|
|
||||||
"details": "Second details"
|
|
||||||
}
|
|
||||||
]`;
|
|
||||||
|
|
||||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
|
||||||
|
|
||||||
expect(result[1].dependencies).toEqual([1]);
|
|
||||||
expect(typeof result[1].dependencies[0]).toBe('number');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw an error for invalid JSON', () => {
|
|
||||||
const text = `This is not valid JSON and cannot be parsed`;
|
|
||||||
|
|
||||||
expect(() => parseSubtasksFromText(text, 1, 2, 5)).toThrow(
|
|
||||||
'Could not locate valid JSON array in the response'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleClaudeError function', () => {
|
|
||||||
// Import the function directly for testing
|
|
||||||
let handleClaudeError;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
// Dynamic import to get the actual function
|
|
||||||
const module = await import('../../scripts/modules/ai-services.js');
|
|
||||||
handleClaudeError = module.handleClaudeError;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle overloaded_error type', () => {
|
|
||||||
const error = {
|
|
||||||
type: 'error',
|
|
||||||
error: {
|
|
||||||
type: 'overloaded_error',
|
|
||||||
message: 'Claude is experiencing high volume'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock process.env to include PERPLEXITY_API_KEY
|
|
||||||
const originalEnv = process.env;
|
|
||||||
process.env = { ...originalEnv, PERPLEXITY_API_KEY: 'test-key' };
|
|
||||||
|
|
||||||
const result = handleClaudeError(error);
|
|
||||||
|
|
||||||
// Restore original env
|
|
||||||
process.env = originalEnv;
|
|
||||||
|
|
||||||
expect(result).toContain('Claude is currently overloaded');
|
|
||||||
expect(result).toContain('fall back to Perplexity AI');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle rate_limit_error type', () => {
|
|
||||||
const error = {
|
|
||||||
type: 'error',
|
|
||||||
error: {
|
|
||||||
type: 'rate_limit_error',
|
|
||||||
message: 'Rate limit exceeded'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = handleClaudeError(error);
|
|
||||||
|
|
||||||
expect(result).toContain('exceeded the rate limit');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle invalid_request_error type', () => {
|
|
||||||
const error = {
|
|
||||||
type: 'error',
|
|
||||||
error: {
|
|
||||||
type: 'invalid_request_error',
|
|
||||||
message: 'Invalid request parameters'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = handleClaudeError(error);
|
|
||||||
|
|
||||||
expect(result).toContain('issue with the request format');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle timeout errors', () => {
|
|
||||||
const error = {
|
|
||||||
message: 'Request timed out after 60000ms'
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = handleClaudeError(error);
|
|
||||||
|
|
||||||
expect(result).toContain('timed out');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle network errors', () => {
|
|
||||||
const error = {
|
|
||||||
message: 'Network error occurred'
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = handleClaudeError(error);
|
|
||||||
|
|
||||||
expect(result).toContain('network error');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle generic errors', () => {
|
|
||||||
const error = {
|
|
||||||
message: 'Something unexpected happened'
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = handleClaudeError(error);
|
|
||||||
|
|
||||||
expect(result).toContain('Error communicating with Claude');
|
|
||||||
expect(result).toContain('Something unexpected happened');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Anthropic client configuration', () => {
|
|
||||||
test('should include output-128k beta header in client configuration', async () => {
|
|
||||||
// Read the file content to verify the change is present
|
|
||||||
const fs = await import('fs');
|
|
||||||
const path = await import('path');
|
|
||||||
const filePath = path.resolve('./scripts/modules/ai-services.js');
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Check if the beta header is in the file
|
|
||||||
expect(fileContent).toContain(
|
|
||||||
"'anthropic-beta': 'output-128k-2025-02-19'"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user