Revert "Release 0.13.0"
This commit is contained in:
@@ -1,162 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# --- LLM Analysis Helper Function ---
|
||||
# This function should be sourced by the main E2E script or test scripts.
|
||||
# It requires curl and jq to be installed.
|
||||
# It expects the project root path to be passed as the second argument.
|
||||
|
||||
analyze_log_with_llm() {
|
||||
local log_file="$1"
|
||||
local project_root="$2" # Expect project root as the second argument
|
||||
|
||||
if [ -z "$project_root" ]; then
|
||||
echo "[HELPER_ERROR] Project root argument is missing. Skipping LLM analysis." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local env_file="${project_root}/.env" # Path to .env in project root
|
||||
|
||||
local provider_summary_log="provider_add_task_summary.log" # File summarizing provider test outcomes
|
||||
local api_key=""
|
||||
# !!! IMPORTANT: Replace with your actual Claude API endpoint if different !!!
|
||||
local api_endpoint="https://api.anthropic.com/v1/messages"
|
||||
# !!! IMPORTANT: Ensure this matches the variable name in your .env file !!!
|
||||
local api_key_name="ANTHROPIC_API_KEY"
|
||||
|
||||
echo "" # Add a newline before analysis starts
|
||||
|
||||
# Check for jq and curl
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "[HELPER_ERROR] LLM Analysis requires 'jq'. Skipping analysis." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! command -v curl &> /dev/null; then
|
||||
echo "[HELPER_ERROR] LLM Analysis requires 'curl'. Skipping analysis." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for API Key in the PROJECT ROOT's .env file
|
||||
if [ -f "$env_file" ]; then
|
||||
# Original assignment - Reading from project root .env
|
||||
api_key=$(grep "^${api_key_name}=" "$env_file" | sed -e "s/^${api_key_name}=//" -e 's/^[[:space:]"]*//' -e 's/[[:space:]"]*$//')
|
||||
fi
|
||||
|
||||
if [ -z "$api_key" ]; then
|
||||
echo "[HELPER_ERROR] ${api_key_name} not found or empty in project root .env file ($env_file). Skipping LLM analysis." >&2 # Updated error message
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Log file path is passed as argument, need to ensure it exists relative to where the script *calling* this function is, OR use absolute path.
|
||||
# Assuming absolute path or path relative to the initial PWD for simplicity here.
|
||||
# The calling script passes the correct path relative to the original PWD.
|
||||
if [ ! -f "$log_file" ]; then
|
||||
echo "[HELPER_ERROR] Log file not found: $log_file (PWD: $(pwd)). Check path passed to function. Skipping LLM analysis." >&2 # Updated error
|
||||
return 1
|
||||
fi
|
||||
|
||||
local log_content
|
||||
# Read entire file, handle potential errors
|
||||
log_content=$(cat "$log_file") || {
|
||||
echo "[HELPER_ERROR] Failed to read log file: $log_file. Skipping LLM analysis." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Prepare the prompt using a quoted heredoc for literal interpretation
|
||||
read -r -d '' prompt_template <<'EOF'
|
||||
Analyze the following E2E test log for the task-master tool. The log contains output from various 'task-master' commands executed sequentially.
|
||||
|
||||
Your goal is to:
|
||||
1. 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).
|
||||
2. **Specifically analyze the Multi-Provider Add-Task Test Sequence:**
|
||||
a. Identify which providers were tested for `add-task`. Look for log steps like "Testing Add-Task with Provider: ..." and the summary log 'provider_add_task_summary.log'.
|
||||
b. For each tested provider, determine if `add-task` succeeded or failed. Note the created task ID if successful.
|
||||
c. Review the corresponding `add_task_show_output_<provider>_id_<id>.log` file (if created) for each successful `add-task` execution.
|
||||
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.
|
||||
e. Note any providers where `add-task` failed or where the task ID could not be extracted.
|
||||
3. Identify any general explicit "[ERROR]" messages or stack traces throughout the *entire* log.
|
||||
4. Identify any potential warnings or unusual output that might indicate a problem even if not marked as an explicit error.
|
||||
5. Provide an overall assessment of the test run's health based *only* on the log content.
|
||||
|
||||
Return your analysis **strictly** in the following JSON format. Do not include any text outside of the JSON structure:
|
||||
|
||||
{
|
||||
"overall_status": "Success|Failure|Warning",
|
||||
"verified_steps": [ "Initialization", "PRD Parsing", /* ...other general steps observed... */ ],
|
||||
"provider_add_task_comparison": {
|
||||
"prompt_used": "... (extract from log if possible or state 'standard auth prompt') ...",
|
||||
"provider_results": {
|
||||
"anthropic": { "status": "Success|Failure|ID_Extraction_Failed|Set_Model_Failed", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },
|
||||
"openai": { "status": "Success|Failure|...", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },
|
||||
/* ... include all tested providers ... */
|
||||
},
|
||||
"comparison_summary": "Brief overall comparison of generated tasks..."
|
||||
},
|
||||
"detected_issues": [ { "severity": "Error|Warning|Anomaly", "description": "...", "log_context": "[Optional, short snippet from log near the issue]" } ],
|
||||
"llm_summary_points": [ "Overall summary point 1", "Provider comparison highlight", "Any major issues noted" ]
|
||||
}
|
||||
|
||||
Here is the main log content:
|
||||
|
||||
%s
|
||||
EOF
|
||||
# Note: The final %s is a placeholder for printf later
|
||||
|
||||
local full_prompt
|
||||
# Use printf to substitute the log content into the %s placeholder
|
||||
if ! printf -v full_prompt "$prompt_template" "$log_content"; then
|
||||
echo "[HELPER_ERROR] Failed to format prompt using printf." >&2
|
||||
# It's unlikely printf itself fails, but good practice
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Construct the JSON payload for Claude Messages API
|
||||
local payload
|
||||
payload=$(jq -n --arg prompt "$full_prompt" '{
|
||||
"model": "claude-3-haiku-20240307", # Using Haiku for faster/cheaper testing
|
||||
"max_tokens": 3072, # Increased slightly
|
||||
"messages": [
|
||||
{"role": "user", "content": $prompt}
|
||||
]
|
||||
# "temperature": 0.0 # Optional: Lower temperature for more deterministic JSON output
|
||||
}') || {
|
||||
echo "[HELPER_ERROR] Failed to create JSON payload using jq." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
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
|
||||
echo "[HELPER_ERROR] LLM API call failed with HTTP status $response_http_code." >&2
|
||||
echo "[HELPER_ERROR] Response Body: $response_body" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$response_body" ]; then
|
||||
echo "[HELPER_ERROR] LLM API call returned empty response body." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Pipe the raw response body directly to the Node.js parser script
|
||||
if echo "$response_body" | node "${project_root}/tests/e2e/parse_llm_output.cjs" "$log_file"; then
|
||||
echo "[HELPER_SUCCESS] LLM analysis parsed and printed successfully by Node.js script."
|
||||
return 0 # Success
|
||||
else
|
||||
local node_exit_code=$?
|
||||
echo "[HELPER_ERROR] Node.js parsing script failed with exit code ${node_exit_code}."
|
||||
echo "[HELPER_ERROR] Raw API response body (first 500 chars): $(echo "$response_body" | head -c 500)"
|
||||
return 1 # Failure
|
||||
fi
|
||||
}
|
||||
|
||||
# Export the function so it might be available to subshells if sourced
|
||||
export -f analyze_log_with_llm
|
||||
@@ -1,266 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Note: We will use dynamic import() inside the async callback due to project being type: module
|
||||
|
||||
const readline = require('readline');
|
||||
const path = require('path'); // Import path module
|
||||
|
||||
let inputData = '';
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: false
|
||||
});
|
||||
|
||||
rl.on('line', (line) => {
|
||||
inputData += line;
|
||||
});
|
||||
|
||||
// Make the callback async to allow await for dynamic imports
|
||||
rl.on('close', async () => {
|
||||
let chalk, boxen, Table;
|
||||
try {
|
||||
// Dynamically import libraries
|
||||
chalk = (await import('chalk')).default;
|
||||
boxen = (await import('boxen')).default;
|
||||
Table = (await import('cli-table3')).default;
|
||||
|
||||
// 1. Parse the initial API response body
|
||||
const apiResponse = JSON.parse(inputData);
|
||||
|
||||
// 2. Extract the text content containing the nested JSON
|
||||
// Robust check for content structure
|
||||
const textContent = apiResponse?.content?.[0]?.text;
|
||||
if (!textContent) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
"Error: Could not find '.content[0].text' in the API response JSON."
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 3. Find the start of the actual JSON block
|
||||
const jsonStart = textContent.indexOf('{');
|
||||
const jsonEnd = textContent.lastIndexOf('}');
|
||||
|
||||
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: Could not find JSON block starting with { and ending with } in the extracted text content.'
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const jsonString = textContent.substring(jsonStart, jsonEnd + 1);
|
||||
|
||||
// 4. Parse the extracted JSON string
|
||||
let reportData;
|
||||
try {
|
||||
reportData = JSON.parse(jsonString);
|
||||
} catch (parseError) {
|
||||
console.error(
|
||||
chalk.red('Error: Failed to parse the extracted JSON block.')
|
||||
);
|
||||
console.error(chalk.red('Parse Error:'), parseError.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Ensure reportData is an object
|
||||
if (typeof reportData !== 'object' || reportData === null) {
|
||||
console.error(
|
||||
chalk.red('Error: Parsed report data is not a valid object.')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// --- Get Log File Path and Format Timestamp ---
|
||||
const logFilePath = process.argv[2]; // Get the log file path argument
|
||||
let formattedTime = 'Unknown';
|
||||
if (logFilePath) {
|
||||
const logBasename = path.basename(logFilePath);
|
||||
const timestampMatch = logBasename.match(/e2e_run_(\d{8}_\d{6})\.log$/);
|
||||
if (timestampMatch && timestampMatch[1]) {
|
||||
const ts = timestampMatch[1]; // YYYYMMDD_HHMMSS
|
||||
// Format into YYYY-MM-DD HH:MM:SS
|
||||
formattedTime = `${ts.substring(0, 4)}-${ts.substring(4, 6)}-${ts.substring(6, 8)} ${ts.substring(9, 11)}:${ts.substring(11, 13)}:${ts.substring(13, 15)}`;
|
||||
}
|
||||
}
|
||||
// --------------------------------------------
|
||||
|
||||
// 5. Generate CLI Report (with defensive checks)
|
||||
console.log(
|
||||
'\n' +
|
||||
chalk.cyan.bold(
|
||||
boxen(
|
||||
`TASKMASTER E2E Log Analysis Report\nRun Time: ${chalk.yellow(formattedTime)}`, // Display formatted time
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'double',
|
||||
borderColor: 'cyan',
|
||||
textAlign: 'center' // Center align title
|
||||
}
|
||||
)
|
||||
) +
|
||||
'\n'
|
||||
);
|
||||
|
||||
// Overall Status
|
||||
let statusColor = chalk.white;
|
||||
const overallStatus = reportData.overall_status || 'Unknown'; // Default if missing
|
||||
if (overallStatus === 'Success') statusColor = chalk.green.bold;
|
||||
if (overallStatus === 'Warning') statusColor = chalk.yellow.bold;
|
||||
if (overallStatus === 'Failure') statusColor = chalk.red.bold;
|
||||
console.log(
|
||||
boxen(`Overall Status: ${statusColor(overallStatus)}`, {
|
||||
padding: { left: 1, right: 1 },
|
||||
margin: { bottom: 1 },
|
||||
borderColor: 'blue'
|
||||
})
|
||||
);
|
||||
|
||||
// LLM Summary Points
|
||||
console.log(chalk.blue.bold('📋 Summary Points:'));
|
||||
if (
|
||||
Array.isArray(reportData.llm_summary_points) &&
|
||||
reportData.llm_summary_points.length > 0
|
||||
) {
|
||||
reportData.llm_summary_points.forEach((point) => {
|
||||
console.log(chalk.white(` - ${point || 'N/A'}`)); // Handle null/undefined points
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.gray(' No summary points provided.'));
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Verified Steps
|
||||
console.log(chalk.green.bold('✅ Verified Steps:'));
|
||||
if (
|
||||
Array.isArray(reportData.verified_steps) &&
|
||||
reportData.verified_steps.length > 0
|
||||
) {
|
||||
reportData.verified_steps.forEach((step) => {
|
||||
console.log(chalk.green(` - ${step || 'N/A'}`)); // Handle null/undefined steps
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.gray(' No verified steps listed.'));
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Provider Add-Task Comparison
|
||||
console.log(chalk.magenta.bold('🔄 Provider Add-Task Comparison:'));
|
||||
const comp = reportData.provider_add_task_comparison;
|
||||
if (typeof comp === 'object' && comp !== null) {
|
||||
console.log(
|
||||
chalk.white(` Prompt Used: ${comp.prompt_used || 'Not specified'}`)
|
||||
);
|
||||
console.log();
|
||||
|
||||
if (
|
||||
typeof comp.provider_results === 'object' &&
|
||||
comp.provider_results !== null &&
|
||||
Object.keys(comp.provider_results).length > 0
|
||||
) {
|
||||
const providerTable = new Table({
|
||||
head: ['Provider', 'Status', 'Task ID', 'Score', 'Notes'].map((h) =>
|
||||
chalk.magenta.bold(h)
|
||||
),
|
||||
colWidths: [15, 18, 10, 12, 45],
|
||||
style: { head: [], border: [] },
|
||||
wordWrap: true
|
||||
});
|
||||
|
||||
for (const provider in comp.provider_results) {
|
||||
const result = comp.provider_results[provider] || {}; // Default to empty object if provider result is null/undefined
|
||||
const status = result.status || 'Unknown';
|
||||
const isSuccess = status === 'Success';
|
||||
const statusIcon = isSuccess ? chalk.green('✅') : chalk.red('❌');
|
||||
const statusText = isSuccess
|
||||
? chalk.green(status)
|
||||
: chalk.red(status);
|
||||
providerTable.push([
|
||||
chalk.white(provider),
|
||||
`${statusIcon} ${statusText}`,
|
||||
chalk.white(result.task_id || 'N/A'),
|
||||
chalk.white(result.score || 'N/A'),
|
||||
chalk.dim(result.notes || 'N/A')
|
||||
]);
|
||||
}
|
||||
console.log(providerTable.toString());
|
||||
console.log();
|
||||
} else {
|
||||
console.log(chalk.gray(' No provider results available.'));
|
||||
console.log();
|
||||
}
|
||||
console.log(chalk.white.bold(` Comparison Summary:`));
|
||||
console.log(chalk.white(` ${comp.comparison_summary || 'N/A'}`));
|
||||
} else {
|
||||
console.log(chalk.gray(' Provider comparison data not found.'));
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Detected Issues
|
||||
console.log(chalk.red.bold('🚨 Detected Issues:'));
|
||||
if (
|
||||
Array.isArray(reportData.detected_issues) &&
|
||||
reportData.detected_issues.length > 0
|
||||
) {
|
||||
reportData.detected_issues.forEach((issue, index) => {
|
||||
if (typeof issue !== 'object' || issue === null) return; // Skip invalid issue entries
|
||||
|
||||
const severity = issue.severity || 'Unknown';
|
||||
let boxColor = 'blue';
|
||||
let icon = 'ℹ️';
|
||||
if (severity === 'Error') {
|
||||
boxColor = 'red';
|
||||
icon = '❌';
|
||||
}
|
||||
if (severity === 'Warning') {
|
||||
boxColor = 'yellow';
|
||||
icon = '⚠️';
|
||||
}
|
||||
|
||||
let issueContent = `${chalk.bold('Description:')} ${chalk.white(issue.description || 'N/A')}`;
|
||||
// Only add log context if it exists and is not empty
|
||||
if (issue.log_context && String(issue.log_context).trim()) {
|
||||
issueContent += `\n${chalk.bold('Log Context:')} \n${chalk.dim(String(issue.log_context).trim())}`;
|
||||
}
|
||||
|
||||
console.log(
|
||||
boxen(issueContent, {
|
||||
title: `${icon} Issue ${index + 1}: [${severity}]`,
|
||||
padding: 1,
|
||||
margin: { top: 1, bottom: 0 },
|
||||
borderColor: boxColor,
|
||||
borderStyle: 'round'
|
||||
})
|
||||
);
|
||||
});
|
||||
console.log(); // Add final newline if issues exist
|
||||
} else {
|
||||
console.log(chalk.green(' No specific issues detected by the LLM.'));
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log(chalk.cyan.bold('========================================'));
|
||||
console.log(chalk.cyan.bold(' End of LLM Report'));
|
||||
console.log(chalk.cyan.bold('========================================\n'));
|
||||
} catch (error) {
|
||||
// Ensure chalk is available for error reporting, provide fallback
|
||||
const errorChalk = chalk || { red: (t) => t, yellow: (t) => t };
|
||||
console.error(
|
||||
errorChalk.red('Error processing LLM response:'),
|
||||
error.message
|
||||
);
|
||||
// Avoid printing potentially huge inputData here unless necessary for debugging
|
||||
// console.error(errorChalk.yellow('Raw input data (first 500 chars):'), inputData.substring(0, 500));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle potential errors during stdin reading
|
||||
process.stdin.on('error', (err) => {
|
||||
console.error('Error reading standard input:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,778 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Treat unset variables as an error when substituting.
|
||||
set -u
|
||||
# Prevent errors in pipelines from being masked.
|
||||
set -o pipefail
|
||||
|
||||
# --- Default Settings ---
|
||||
run_verification_test=true
|
||||
|
||||
# --- Argument Parsing ---
|
||||
# Simple loop to check for the skip flag
|
||||
# Note: This needs to happen *before* the main block piped to tee
|
||||
# if we want the decision logged early. Or handle args inside.
|
||||
# Let's handle it before for clarity.
|
||||
processed_args=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-verification)
|
||||
run_verification_test=false
|
||||
echo "[INFO] Argument '--skip-verification' detected. Fallback verification will be skipped."
|
||||
shift # Consume the flag
|
||||
;;
|
||||
--analyze-log)
|
||||
# Keep the analyze-log flag handling separate for now
|
||||
# It exits early, so doesn't conflict with the main run flags
|
||||
processed_args+=("$1")
|
||||
if [[ $# -gt 1 ]]; then
|
||||
processed_args+=("$2")
|
||||
shift 2
|
||||
else
|
||||
shift 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Unknown argument, pass it along or handle error
|
||||
# For now, just pass it along in case --analyze-log needs it later
|
||||
processed_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
# Restore processed arguments ONLY if the array is not empty
|
||||
if [ ${#processed_args[@]} -gt 0 ]; then
|
||||
set -- "${processed_args[@]}"
|
||||
fi
|
||||
|
||||
|
||||
# --- Configuration ---
|
||||
# Assumes script is run from the project root (claude-task-master)
|
||||
TASKMASTER_SOURCE_DIR="." # Current directory is the source
|
||||
# Base directory for test runs, relative to project root
|
||||
BASE_TEST_DIR="$TASKMASTER_SOURCE_DIR/tests/e2e/_runs"
|
||||
# Log directory, relative to project root
|
||||
LOG_DIR="$TASKMASTER_SOURCE_DIR/tests/e2e/log"
|
||||
# Path to the sample PRD, relative to project root
|
||||
SAMPLE_PRD_SOURCE="$TASKMASTER_SOURCE_DIR/tests/fixtures/sample-prd.txt"
|
||||
# Path to the main .env file in the source directory
|
||||
MAIN_ENV_FILE="$TASKMASTER_SOURCE_DIR/.env"
|
||||
# ---
|
||||
|
||||
# <<< Source the helper script >>>
|
||||
source "$TASKMASTER_SOURCE_DIR/tests/e2e/e2e_helpers.sh"
|
||||
# <<< Export helper functions for subshells >>>
|
||||
export -f log_info log_success log_error log_step _format_duration _get_elapsed_time_for_log
|
||||
|
||||
# --- Argument Parsing for Analysis-Only Mode ---
|
||||
# This remains the same, as it exits early if matched
|
||||
if [ "$#" -ge 1 ] && [ "$1" == "--analyze-log" ]; then
|
||||
LOG_TO_ANALYZE=""
|
||||
# Check if a log file path was provided as the second argument
|
||||
if [ "$#" -ge 2 ] && [ -n "$2" ]; then
|
||||
LOG_TO_ANALYZE="$2"
|
||||
echo "[INFO] Using specified log file for analysis: $LOG_TO_ANALYZE"
|
||||
else
|
||||
echo "[INFO] Log file not specified. Attempting to find the latest log..."
|
||||
# Find the latest log file in the LOG_DIR
|
||||
# Ensure LOG_DIR is absolute for ls to work correctly regardless of PWD
|
||||
ABS_LOG_DIR="$(cd "$TASKMASTER_SOURCE_DIR/$LOG_DIR" && pwd)"
|
||||
LATEST_LOG=$(ls -t "$ABS_LOG_DIR"/e2e_run_*.log 2>/dev/null | head -n 1)
|
||||
|
||||
if [ -z "$LATEST_LOG" ]; then
|
||||
echo "[ERROR] No log files found matching 'e2e_run_*.log' in $ABS_LOG_DIR. Cannot analyze." >&2
|
||||
exit 1
|
||||
fi
|
||||
LOG_TO_ANALYZE="$LATEST_LOG"
|
||||
echo "[INFO] Found latest log file: $LOG_TO_ANALYZE"
|
||||
fi
|
||||
|
||||
# Ensure the log path is absolute (it should be if found by ls, but double-check)
|
||||
if [[ "$LOG_TO_ANALYZE" != /* ]]; then
|
||||
LOG_TO_ANALYZE="$(pwd)/$LOG_TO_ANALYZE" # Fallback if relative path somehow occurred
|
||||
fi
|
||||
echo "[INFO] Running in analysis-only mode for log: $LOG_TO_ANALYZE"
|
||||
|
||||
# --- Derive TEST_RUN_DIR from log file path ---
|
||||
# Extract timestamp like YYYYMMDD_HHMMSS from e2e_run_YYYYMMDD_HHMMSS.log
|
||||
log_basename=$(basename "$LOG_TO_ANALYZE")
|
||||
# Ensure the sed command matches the .log suffix correctly
|
||||
timestamp_match=$(echo "$log_basename" | sed -n 's/^e2e_run_\([0-9]\{8\}_[0-9]\{6\}\)\.log$/\1/p')
|
||||
|
||||
if [ -z "$timestamp_match" ]; then
|
||||
echo "[ERROR] Could not extract timestamp from log file name: $log_basename" >&2
|
||||
echo "[ERROR] Expected format: e2e_run_YYYYMMDD_HHMMSS.log" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Construct the expected run directory path relative to project root
|
||||
EXPECTED_RUN_DIR="$TASKMASTER_SOURCE_DIR/tests/e2e/_runs/run_$timestamp_match"
|
||||
# Make it absolute
|
||||
EXPECTED_RUN_DIR_ABS="$(cd "$TASKMASTER_SOURCE_DIR" && pwd)/tests/e2e/_runs/run_$timestamp_match"
|
||||
|
||||
if [ ! -d "$EXPECTED_RUN_DIR_ABS" ]; then
|
||||
echo "[ERROR] Corresponding test run directory not found: $EXPECTED_RUN_DIR_ABS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save original dir before changing
|
||||
ORIGINAL_DIR=$(pwd)
|
||||
|
||||
echo "[INFO] Changing directory to $EXPECTED_RUN_DIR_ABS for analysis context..."
|
||||
cd "$EXPECTED_RUN_DIR_ABS"
|
||||
|
||||
# Call the analysis function (sourced from helpers)
|
||||
echo "[INFO] Calling analyze_log_with_llm function..."
|
||||
analyze_log_with_llm "$LOG_TO_ANALYZE" "$(cd "$ORIGINAL_DIR/$TASKMASTER_SOURCE_DIR" && pwd)" # Pass absolute project root
|
||||
ANALYSIS_EXIT_CODE=$?
|
||||
|
||||
# Return to original directory
|
||||
cd "$ORIGINAL_DIR"
|
||||
exit $ANALYSIS_EXIT_CODE
|
||||
fi
|
||||
# --- End Analysis-Only Mode Logic ---
|
||||
|
||||
# --- Normal Execution Starts Here (if not in analysis-only mode) ---
|
||||
|
||||
# --- Test State Variables ---
|
||||
# Note: These are mainly for step numbering within the log now, not for final summary
|
||||
test_step_count=0
|
||||
start_time_for_helpers=0 # Separate start time for helper functions inside the pipe
|
||||
# ---
|
||||
|
||||
# --- Log File Setup ---
|
||||
# Create the log directory if it doesn't exist
|
||||
mkdir -p "$LOG_DIR"
|
||||
# Define timestamped log file path
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
# <<< Use pwd to create an absolute path AND add .log extension >>>
|
||||
LOG_FILE="$(pwd)/$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 E2E test. Output will be shown here and saved to: $LOG_FILE"
|
||||
echo "Running from directory: $(pwd)"
|
||||
echo "--- Starting E2E Run ---" # Separator before piped output starts
|
||||
|
||||
# Record start time for overall duration *before* the pipe
|
||||
overall_start_time=$(date +%s)
|
||||
|
||||
# <<< DEFINE ORIGINAL_DIR GLOBALLY HERE >>>
|
||||
ORIGINAL_DIR=$(pwd)
|
||||
|
||||
# ==========================================
|
||||
# >>> MOVE FUNCTION DEFINITION HERE <<<
|
||||
# --- Helper Functions (Define globally) ---
|
||||
_format_duration() {
|
||||
local total_seconds=$1
|
||||
local minutes=$((total_seconds / 60))
|
||||
local seconds=$((total_seconds % 60))
|
||||
printf "%dm%02ds" "$minutes" "$seconds"
|
||||
}
|
||||
|
||||
# Note: This relies on 'overall_start_time' being set globally before the function is called
|
||||
_get_elapsed_time_for_log() {
|
||||
local current_time=$(date +%s)
|
||||
# Use overall_start_time here, as start_time_for_helpers might not be relevant globally
|
||||
local elapsed_seconds=$((current_time - overall_start_time))
|
||||
_format_duration "$elapsed_seconds"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo "[INFO] [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "[SUCCESS] [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[ERROR] [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1" >&2
|
||||
}
|
||||
|
||||
log_step() {
|
||||
test_step_count=$((test_step_count + 1))
|
||||
echo ""
|
||||
echo "============================================="
|
||||
echo " STEP ${test_step_count}: [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
|
||||
echo "============================================="
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
|
||||
# --- Main Execution Block (Piped to tee) ---
|
||||
# Wrap the main part of the script in braces and pipe its output (stdout and stderr) to tee
|
||||
{
|
||||
# Note: Helper functions are now defined globally above,
|
||||
# but we still need start_time_for_helpers if any logging functions
|
||||
# called *inside* this block depend on it. If not, it can be removed.
|
||||
start_time_for_helpers=$(date +%s) # Keep if needed by helpers called inside this block
|
||||
|
||||
# Log the verification decision
|
||||
if [ "$run_verification_test" = true ]; then
|
||||
log_info "Fallback verification test will be run as part of this E2E test."
|
||||
else
|
||||
log_info "Fallback verification test will be SKIPPED (--skip-verification flag detected)."
|
||||
fi
|
||||
|
||||
# --- Dependency Checks ---
|
||||
log_step "Checking for dependencies (jq)"
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_error "Dependency 'jq' is not installed or not found in PATH. Please install jq (e.g., 'brew install jq' or 'sudo apt-get install jq')."
|
||||
exit 1
|
||||
fi
|
||||
log_success "Dependency 'jq' found."
|
||||
|
||||
# --- Test Setup (Output to tee) ---
|
||||
log_step "Setting up test environment"
|
||||
|
||||
log_step "Creating global npm link for task-master-ai"
|
||||
if npm link; then
|
||||
log_success "Global link created/updated."
|
||||
else
|
||||
log_error "Failed to run 'npm link'. Check permissions or output for details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Ensured base test directory exists: $BASE_TEST_DIR"
|
||||
|
||||
log_info "Using test run directory (created earlier): $TEST_RUN_DIR"
|
||||
|
||||
# Check if source .env file exists
|
||||
if [ ! -f "$MAIN_ENV_FILE" ]; then
|
||||
log_error "Source .env file not found at $MAIN_ENV_FILE. Cannot proceed with API-dependent tests."
|
||||
exit 1
|
||||
fi
|
||||
log_info "Source .env file found at $MAIN_ENV_FILE."
|
||||
|
||||
# Check if sample PRD exists
|
||||
if [ ! -f "$SAMPLE_PRD_SOURCE" ]; then
|
||||
log_error "Sample PRD not found at $SAMPLE_PRD_SOURCE. Please check path."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Copying sample PRD to test directory..."
|
||||
cp "$SAMPLE_PRD_SOURCE" "$TEST_RUN_DIR/prd.txt"
|
||||
if [ ! -f "$TEST_RUN_DIR/prd.txt" ]; then
|
||||
log_error "Failed to copy sample PRD to $TEST_RUN_DIR."
|
||||
exit 1
|
||||
fi
|
||||
log_success "Sample PRD copied."
|
||||
|
||||
# ORIGINAL_DIR=$(pwd) # Save original dir # <<< REMOVED FROM HERE
|
||||
cd "$TEST_RUN_DIR"
|
||||
log_info "Changed directory to $(pwd)"
|
||||
|
||||
# === Copy .env file BEFORE init ===
|
||||
log_step "Copying source .env file for API keys"
|
||||
if cp "$ORIGINAL_DIR/.env" ".env"; then
|
||||
log_success ".env file copied successfully."
|
||||
else
|
||||
log_error "Failed to copy .env file from $ORIGINAL_DIR/.env"
|
||||
exit 1
|
||||
fi
|
||||
# ========================================
|
||||
|
||||
# --- Test Execution (Output to tee) ---
|
||||
|
||||
log_step "Linking task-master-ai package locally"
|
||||
npm link task-master-ai
|
||||
log_success "Package linked locally."
|
||||
|
||||
log_step "Initializing Task Master project (non-interactive)"
|
||||
task-master init -y --name="E2E Test $TIMESTAMP" --description="Automated E2E test run"
|
||||
if [ ! -f ".taskmasterconfig" ]; then
|
||||
log_error "Initialization failed: .taskmasterconfig not found."
|
||||
exit 1
|
||||
fi
|
||||
log_success "Project initialized."
|
||||
|
||||
log_step "Parsing PRD"
|
||||
task-master parse-prd ./prd.txt --force
|
||||
if [ ! -s "tasks/tasks.json" ]; then
|
||||
log_error "Parsing PRD failed: tasks/tasks.json not found or is empty."
|
||||
exit 1
|
||||
fi
|
||||
log_success "PRD parsed successfully."
|
||||
|
||||
log_step "Expanding Task 1 (to ensure subtask 1.1 exists)"
|
||||
# Add --research flag if needed and API keys support it
|
||||
task-master analyze-complexity --research --output complexity_results.json
|
||||
if [ ! -f "complexity_results.json" ]; then
|
||||
log_error "Complexity analysis failed: complexity_results.json not found."
|
||||
exit 1
|
||||
fi
|
||||
log_success "Complexity analysis saved to complexity_results.json"
|
||||
|
||||
log_step "Generating complexity report"
|
||||
task-master complexity-report --file complexity_results.json > complexity_report_formatted.log
|
||||
log_success "Formatted complexity report saved to complexity_report_formatted.log"
|
||||
|
||||
log_step "Expanding Task 1 (assuming it exists)"
|
||||
# Add --research flag if needed and API keys support it
|
||||
task-master expand --id=1 # Add --research?
|
||||
log_success "Attempted to expand Task 1."
|
||||
|
||||
log_step "Setting status for Subtask 1.1 (assuming it exists)"
|
||||
task-master set-status --id=1.1 --status=done
|
||||
log_success "Attempted to set status for Subtask 1.1 to 'done'."
|
||||
|
||||
log_step "Listing tasks again (after changes)"
|
||||
task-master list --with-subtasks > task_list_after_changes.log
|
||||
log_success "Task list after changes saved to task_list_after_changes.log"
|
||||
|
||||
# === Test Model Commands ===
|
||||
log_step "Checking initial model configuration"
|
||||
task-master models > models_initial_config.log
|
||||
log_success "Initial model config saved to models_initial_config.log"
|
||||
|
||||
log_step "Setting main model"
|
||||
task-master models --set-main claude-3-7-sonnet-20250219
|
||||
log_success "Set main model."
|
||||
|
||||
log_step "Setting research model"
|
||||
task-master models --set-research sonar-pro
|
||||
log_success "Set research model."
|
||||
|
||||
log_step "Setting fallback model"
|
||||
task-master models --set-fallback claude-3-5-sonnet-20241022
|
||||
log_success "Set fallback model."
|
||||
|
||||
log_step "Checking final model configuration"
|
||||
task-master models > 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 ===
|
||||
|
||||
# === Fallback Model generateObjectService Verification ===
|
||||
if [ "$run_verification_test" = true ]; then
|
||||
log_step "Starting Fallback Model (generateObjectService) Verification (Calls separate script)"
|
||||
verification_script_path="$ORIGINAL_DIR/tests/e2e/run_fallback_verification.sh"
|
||||
|
||||
if [ -x "$verification_script_path" ]; then
|
||||
log_info "--- Executing Fallback Verification Script: $verification_script_path ---"
|
||||
# Execute the script directly, allowing output to flow to tee
|
||||
# Pass the current directory (the test run dir) as the argument
|
||||
"$verification_script_path" "$(pwd)"
|
||||
verification_exit_code=$? # Capture exit code immediately
|
||||
log_info "--- Finished Fallback Verification Script Execution (Exit Code: $verification_exit_code) ---"
|
||||
|
||||
# Log success/failure based on captured exit code
|
||||
if [ $verification_exit_code -eq 0 ]; then
|
||||
log_success "Fallback verification script reported success."
|
||||
else
|
||||
log_error "Fallback verification script reported FAILURE (Exit Code: $verification_exit_code)."
|
||||
# Decide whether to exit the main script or just log the error
|
||||
# exit 1 # Uncomment to make verification failure fatal
|
||||
fi
|
||||
else
|
||||
log_error "Fallback verification script not found or not executable at $verification_script_path. Skipping verification."
|
||||
# Decide whether to exit or continue
|
||||
# exit 1
|
||||
fi
|
||||
else
|
||||
log_info "Skipping Fallback Verification test as requested by flag."
|
||||
fi
|
||||
# === END Verification Section ===
|
||||
|
||||
|
||||
# === Multi-Provider Add-Task Test (Keep as is) ===
|
||||
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" # Note: This is research-only, add-task might fail if not using research model
|
||||
"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\""
|
||||
echo "--- Multi-Provider Add Task Summary ---" > provider_add_task_summary.log # Initialize summary log
|
||||
|
||||
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}_${model//\//_}.log" # Sanitize ID
|
||||
# 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 "✓ Added new task #"; 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 "✓ Added new task #" | sed 's/.*✓ Added new task #\([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 (after multi-add)"
|
||||
task-master list --with-subtasks > task_list_after_multi_add.log
|
||||
log_success "Task list after multi-add saved to task_list_after_multi_add.log"
|
||||
|
||||
|
||||
# === Resume Core Task Commands Test ===
|
||||
log_step "Listing tasks (for core tests)"
|
||||
task-master list > task_list_core_test_start.log
|
||||
log_success "Core test initial task list saved."
|
||||
|
||||
log_step "Getting next task"
|
||||
task-master next > next_task_core_test.log
|
||||
log_success "Core test next task saved."
|
||||
|
||||
log_step "Showing Task 1 details"
|
||||
task-master show 1 > task_1_details_core_test.log
|
||||
log_success "Task 1 details saved."
|
||||
|
||||
log_step "Adding dependency (Task 2 depends on Task 1)"
|
||||
task-master add-dependency --id=2 --depends-on=1
|
||||
log_success "Added dependency 2->1."
|
||||
|
||||
log_step "Validating dependencies (after add)"
|
||||
task-master validate-dependencies > validate_dependencies_after_add_core.log
|
||||
log_success "Dependency validation after add saved."
|
||||
|
||||
log_step "Removing dependency (Task 2 depends on Task 1)"
|
||||
task-master remove-dependency --id=2 --depends-on=1
|
||||
log_success "Removed dependency 2->1."
|
||||
|
||||
log_step "Fixing dependencies (should be no-op now)"
|
||||
task-master fix-dependencies > fix_dependencies_output_core.log
|
||||
log_success "Fix dependencies attempted."
|
||||
|
||||
# === Start New Test Section: Validate/Fix Bad Dependencies ===
|
||||
|
||||
log_step "Intentionally adding non-existent dependency (1 -> 999)"
|
||||
task-master add-dependency --id=1 --depends-on=999 || log_error "Failed to add non-existent dependency (unexpected)"
|
||||
# Don't exit even if the above fails, the goal is to test validation
|
||||
log_success "Attempted to add dependency 1 -> 999."
|
||||
|
||||
log_step "Validating dependencies (expecting non-existent error)"
|
||||
task-master validate-dependencies > validate_deps_non_existent.log 2>&1 || true # Allow command to fail without exiting script
|
||||
if grep -q "Non-existent dependency ID: 999" validate_deps_non_existent.log; then
|
||||
log_success "Validation correctly identified non-existent dependency 999."
|
||||
else
|
||||
log_error "Validation DID NOT report non-existent dependency 999 as expected. Check validate_deps_non_existent.log"
|
||||
# Consider exiting here if this check fails, as it indicates a validation logic problem
|
||||
# exit 1
|
||||
fi
|
||||
|
||||
log_step "Fixing dependencies (should remove 1 -> 999)"
|
||||
task-master fix-dependencies > fix_deps_after_non_existent.log
|
||||
log_success "Attempted to fix dependencies."
|
||||
|
||||
log_step "Validating dependencies (after fix)"
|
||||
task-master validate-dependencies > validate_deps_after_fix_non_existent.log 2>&1 || true # Allow potential failure
|
||||
if grep -q "Non-existent dependency ID: 999" validate_deps_after_fix_non_existent.log; then
|
||||
log_error "Validation STILL reports non-existent dependency 999 after fix. Check logs."
|
||||
# exit 1
|
||||
else
|
||||
log_success "Validation shows non-existent dependency 999 was removed."
|
||||
fi
|
||||
|
||||
|
||||
log_step "Intentionally adding circular dependency (4 -> 5 -> 4)"
|
||||
task-master add-dependency --id=4 --depends-on=5 || log_error "Failed to add dependency 4->5"
|
||||
task-master add-dependency --id=5 --depends-on=4 || log_error "Failed to add dependency 5->4"
|
||||
log_success "Attempted to add dependencies 4 -> 5 and 5 -> 4."
|
||||
|
||||
|
||||
log_step "Validating dependencies (expecting circular error)"
|
||||
task-master validate-dependencies > validate_deps_circular.log 2>&1 || true # Allow command to fail
|
||||
# Note: Adjust the grep pattern based on the EXACT error message from validate-dependencies
|
||||
if grep -q -E "Circular dependency detected involving task IDs: (4, 5|5, 4)" validate_deps_circular.log; then
|
||||
log_success "Validation correctly identified circular dependency between 4 and 5."
|
||||
else
|
||||
log_error "Validation DID NOT report circular dependency 4<->5 as expected. Check validate_deps_circular.log"
|
||||
# exit 1
|
||||
fi
|
||||
|
||||
log_step "Fixing dependencies (should remove one side of 4 <-> 5)"
|
||||
task-master fix-dependencies > fix_deps_after_circular.log
|
||||
log_success "Attempted to fix dependencies."
|
||||
|
||||
log_step "Validating dependencies (after fix circular)"
|
||||
task-master validate-dependencies > validate_deps_after_fix_circular.log 2>&1 || true # Allow potential failure
|
||||
if grep -q -E "Circular dependency detected involving task IDs: (4, 5|5, 4)" validate_deps_after_fix_circular.log; then
|
||||
log_error "Validation STILL reports circular dependency 4<->5 after fix. Check logs."
|
||||
# exit 1
|
||||
else
|
||||
log_success "Validation shows circular dependency 4<->5 was resolved."
|
||||
fi
|
||||
|
||||
# === End New Test Section ===
|
||||
|
||||
# Find the next available task ID dynamically instead of hardcoding 11, 12
|
||||
# Assuming tasks are added sequentially and we didn't remove any core tasks yet
|
||||
last_task_id=$(jq '[.tasks[].id] | max' tasks/tasks.json)
|
||||
manual_task_id=$((last_task_id + 1))
|
||||
ai_task_id=$((manual_task_id + 1))
|
||||
|
||||
log_step "Adding Task $manual_task_id (Manual)"
|
||||
task-master add-task --title="Manual E2E Task" --description="Add basic health check endpoint" --priority=low --dependencies=3 # Depends on backend setup
|
||||
log_success "Added Task $manual_task_id manually."
|
||||
|
||||
log_step "Adding Task $ai_task_id (AI)"
|
||||
task-master add-task --prompt="Implement basic UI styling using CSS variables for colors and spacing" --priority=medium --dependencies=1 # Depends on frontend setup
|
||||
log_success "Added Task $ai_task_id via AI prompt."
|
||||
|
||||
|
||||
log_step "Updating Task 3 (update-task AI)"
|
||||
task-master update-task --id=3 --prompt="Update backend server setup: Ensure CORS is configured to allow requests from the frontend origin."
|
||||
log_success "Attempted update for Task 3."
|
||||
|
||||
log_step "Updating Tasks from Task 5 (update AI)"
|
||||
task-master update --from=5 --prompt="Refactor the backend storage module to use a simple JSON file (storage.json) instead of an in-memory object for persistence. Update relevant tasks."
|
||||
log_success "Attempted update from Task 5 onwards."
|
||||
|
||||
log_step "Expanding Task 8 (AI)"
|
||||
task-master expand --id=8 # Expand task 8: Frontend logic
|
||||
log_success "Attempted to expand Task 8."
|
||||
|
||||
log_step "Updating Subtask 8.1 (update-subtask AI)"
|
||||
task-master update-subtask --id=8.1 --prompt="Implementation note: Remember to handle potential API errors and display a user-friendly message."
|
||||
log_success "Attempted update for Subtask 8.1."
|
||||
|
||||
# Add a couple more subtasks for multi-remove test
|
||||
log_step 'Adding subtasks to Task 2 (for multi-remove test)'
|
||||
task-master add-subtask --parent=2 --title="Subtask 2.1 for removal"
|
||||
task-master add-subtask --parent=2 --title="Subtask 2.2 for removal"
|
||||
log_success "Added subtasks 2.1 and 2.2."
|
||||
|
||||
log_step "Removing Subtasks 2.1 and 2.2 (multi-ID)"
|
||||
task-master remove-subtask --id=2.1,2.2
|
||||
log_success "Removed subtasks 2.1 and 2.2."
|
||||
|
||||
log_step "Setting status for Task 1 to done"
|
||||
task-master set-status --id=1 --status=done
|
||||
log_success "Set status for Task 1 to done."
|
||||
|
||||
log_step "Getting next task (after status change)"
|
||||
task-master next > next_task_after_change_core.log
|
||||
log_success "Next task after change saved."
|
||||
|
||||
# === Start New Test Section: List Filtering ===
|
||||
log_step "Listing tasks filtered by status 'done'"
|
||||
task-master list --status=done > task_list_status_done.log
|
||||
log_success "Filtered list saved to task_list_status_done.log (Manual/LLM check recommended)"
|
||||
# Optional assertion: Check if Task 1 ID exists and Task 2 ID does NOT
|
||||
# if grep -q "^1\." task_list_status_done.log && ! grep -q "^2\." task_list_status_done.log; then
|
||||
# log_success "Basic check passed: Task 1 found, Task 2 not found in 'done' list."
|
||||
# else
|
||||
# log_error "Basic check failed for list --status=done."
|
||||
# fi
|
||||
# === End New Test Section ===
|
||||
|
||||
log_step "Clearing subtasks from Task 8"
|
||||
task-master clear-subtasks --id=8
|
||||
log_success "Attempted to clear subtasks from Task 8."
|
||||
|
||||
log_step "Removing Tasks $manual_task_id and $ai_task_id (multi-ID)"
|
||||
# Remove the tasks we added earlier
|
||||
task-master remove-task --id="$manual_task_id,$ai_task_id" -y
|
||||
log_success "Removed tasks $manual_task_id and $ai_task_id."
|
||||
|
||||
# === Start New Test Section: Subtasks & Dependencies ===
|
||||
|
||||
log_step "Expanding Task 2 (to ensure multiple tasks have subtasks)"
|
||||
task-master expand --id=2 # Expand task 2: Backend setup
|
||||
log_success "Attempted to expand Task 2."
|
||||
|
||||
log_step "Listing tasks with subtasks (Before Clear All)"
|
||||
task-master list --with-subtasks > task_list_before_clear_all.log
|
||||
log_success "Task list before clear-all saved."
|
||||
|
||||
log_step "Clearing ALL subtasks"
|
||||
task-master clear-subtasks --all
|
||||
log_success "Attempted to clear all subtasks."
|
||||
|
||||
log_step "Listing tasks with subtasks (After Clear All)"
|
||||
task-master list --with-subtasks > task_list_after_clear_all.log
|
||||
log_success "Task list after clear-all saved. (Manual/LLM check recommended to verify subtasks removed)"
|
||||
|
||||
log_step "Expanding Task 1 again (to have subtasks for next test)"
|
||||
task-master expand --id=1
|
||||
log_success "Attempted to expand Task 1 again."
|
||||
# Verify 1.1 exists again
|
||||
if ! jq -e '.tasks[] | select(.id == 1) | .subtasks[] | select(.id == 1)' tasks/tasks.json > /dev/null; then
|
||||
log_error "Subtask 1.1 not found in tasks.json after re-expanding Task 1."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_step "Adding dependency: Task 3 depends on Subtask 1.1"
|
||||
task-master add-dependency --id=3 --depends-on=1.1
|
||||
log_success "Added dependency 3 -> 1.1."
|
||||
|
||||
log_step "Showing Task 3 details (after adding subtask dependency)"
|
||||
task-master show 3 > task_3_details_after_dep_add.log
|
||||
log_success "Task 3 details saved. (Manual/LLM check recommended for dependency [1.1])"
|
||||
|
||||
log_step "Removing dependency: Task 3 depends on Subtask 1.1"
|
||||
task-master remove-dependency --id=3 --depends-on=1.1
|
||||
log_success "Removed dependency 3 -> 1.1."
|
||||
|
||||
log_step "Showing Task 3 details (after removing subtask dependency)"
|
||||
task-master show 3 > task_3_details_after_dep_remove.log
|
||||
log_success "Task 3 details saved. (Manual/LLM check recommended to verify dependency removed)"
|
||||
|
||||
# === End New Test Section ===
|
||||
|
||||
log_step "Generating task files (final)"
|
||||
task-master generate
|
||||
log_success "Generated task files."
|
||||
# === End Core Task Commands Test ===
|
||||
|
||||
# === AI Commands (Re-test some after changes) ===
|
||||
log_step "Analyzing complexity (AI with Research - Final Check)"
|
||||
task-master analyze-complexity --research --output complexity_results_final.json
|
||||
if [ ! -f "complexity_results_final.json" ]; then log_error "Final Complexity analysis failed."; exit 1; fi
|
||||
log_success "Final Complexity analysis saved."
|
||||
|
||||
log_step "Generating complexity report (Non-AI - Final Check)"
|
||||
task-master complexity-report --file complexity_results_final.json > complexity_report_formatted_final.log
|
||||
log_success "Final Formatted complexity report saved."
|
||||
|
||||
# === End AI Commands Re-test ===
|
||||
|
||||
log_step "Listing tasks again (final)"
|
||||
task-master list --with-subtasks > task_list_final.log
|
||||
log_success "Final task list saved to task_list_final.log"
|
||||
|
||||
# --- Test Completion (Output to tee) ---
|
||||
log_step "E2E Test Steps Completed"
|
||||
echo ""
|
||||
ABS_TEST_RUN_DIR="$(pwd)"
|
||||
echo "Test artifacts and logs are located in: $ABS_TEST_RUN_DIR"
|
||||
echo "Key artifact files (within above dir):"
|
||||
ls -1 # List files in the current directory
|
||||
echo ""
|
||||
echo "Full script log also available at: $LOG_FILE (relative to project root)"
|
||||
|
||||
# Optional: cd back to original directory
|
||||
# cd "$ORIGINAL_DIR"
|
||||
|
||||
# End of the main execution block brace
|
||||
} 2>&1 | tee "$LOG_FILE"
|
||||
|
||||
# --- Final Terminal Message ---
|
||||
EXIT_CODE=${PIPESTATUS[0]}
|
||||
overall_end_time=$(date +%s)
|
||||
total_elapsed_seconds=$((overall_end_time - overall_start_time))
|
||||
|
||||
# Format total duration
|
||||
total_minutes=$((total_elapsed_seconds / 60))
|
||||
total_sec_rem=$((total_elapsed_seconds % 60))
|
||||
formatted_total_time=$(printf "%dm%02ds" "$total_minutes" "$total_sec_rem")
|
||||
|
||||
# Count steps and successes from the log file *after* the pipe finishes
|
||||
# Use grep -c for counting lines matching the pattern
|
||||
# Corrected pattern to match ' STEP X:' format
|
||||
final_step_count=$(grep -c '^[[:space:]]\+STEP [0-9]\+:' "$LOG_FILE" || true)
|
||||
final_success_count=$(grep -c '\[SUCCESS\]' "$LOG_FILE" || true) # Count lines containing [SUCCESS]
|
||||
|
||||
echo "--- E2E Run Summary ---"
|
||||
echo "Log File: $LOG_FILE"
|
||||
echo "Total Elapsed Time: ${formatted_total_time}"
|
||||
echo "Total Steps Executed: ${final_step_count}" # Use count from log
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "Status: SUCCESS"
|
||||
# Use counts from log file
|
||||
echo "Successful Steps: ${final_success_count}/${final_step_count}"
|
||||
else
|
||||
echo "Status: FAILED"
|
||||
# Use count from log file for total steps attempted
|
||||
echo "Failure likely occurred during/after Step: ${final_step_count}"
|
||||
# Use count from log file for successes before failure
|
||||
echo "Successful Steps Before Failure: ${final_success_count}"
|
||||
echo "Please check the log file '$LOG_FILE' for error details."
|
||||
fi
|
||||
echo "-------------------------"
|
||||
|
||||
# --- Attempt LLM Analysis ---
|
||||
# Run this *after* the main execution block and tee pipe finish writing the log file
|
||||
if [ -d "$TEST_RUN_DIR" ]; then
|
||||
# Define absolute path to source dir if not already defined (though it should be by setup)
|
||||
TASKMASTER_SOURCE_DIR_ABS=${TASKMASTER_SOURCE_DIR_ABS:-$(cd "$ORIGINAL_DIR/$TASKMASTER_SOURCE_DIR" && pwd)}
|
||||
|
||||
cd "$TEST_RUN_DIR"
|
||||
# Pass the absolute source directory path
|
||||
analyze_log_with_llm "$LOG_FILE" "$TASKMASTER_SOURCE_DIR_ABS"
|
||||
ANALYSIS_EXIT_CODE=$? # Capture the exit code of the analysis function
|
||||
# Optional: cd back again if needed
|
||||
cd "$ORIGINAL_DIR" # Ensure we change back to the original directory
|
||||
else
|
||||
formatted_duration_for_error=$(_format_duration "$total_elapsed_seconds")
|
||||
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
|
||||
|
||||
exit $EXIT_CODE
|
||||
@@ -1,270 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# --- Fallback Model Verification Script ---
|
||||
# Purpose: Tests models marked as 'fallback' in supported-models.json
|
||||
# to see if they work with generateObjectService (via update-subtask).
|
||||
# Usage: 1. Run from within a prepared E2E test run directory:
|
||||
# ./path/to/script.sh .
|
||||
# 2. Run from project root (or anywhere) to use the latest run dir:
|
||||
# ./tests/e2e/run_fallback_verification.sh
|
||||
# 3. Run from project root (or anywhere) targeting a specific run dir:
|
||||
# ./tests/e2e/run_fallback_verification.sh /path/to/tests/e2e/_runs/run_YYYYMMDD_HHMMSS
|
||||
# Output: Prints a summary report to standard output. Errors to standard error.
|
||||
|
||||
# Treat unset variables as an error when substituting.
|
||||
set -u
|
||||
# Prevent errors in pipelines from being masked.
|
||||
set -o pipefail
|
||||
|
||||
# --- Embedded Helper Functions ---
|
||||
# Copied from e2e_helpers.sh to make this script standalone
|
||||
|
||||
_format_duration() {
|
||||
local total_seconds=$1
|
||||
local minutes=$((total_seconds / 60))
|
||||
local seconds=$((total_seconds % 60))
|
||||
printf "%dm%02ds" "$minutes" "$seconds"
|
||||
}
|
||||
|
||||
_get_elapsed_time_for_log() {
|
||||
# Needs overall_start_time defined in the main script body
|
||||
local current_time=$(date +%s)
|
||||
local elapsed_seconds=$((current_time - overall_start_time))
|
||||
_format_duration "$elapsed_seconds"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo "[INFO] [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "[SUCCESS] [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[ERROR] [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1" >&2
|
||||
}
|
||||
|
||||
log_step() {
|
||||
# Needs test_step_count defined and incremented in the main script body
|
||||
test_step_count=$((test_step_count + 1))
|
||||
echo ""
|
||||
echo "============================================="
|
||||
echo " STEP ${test_step_count}: [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
|
||||
echo "============================================="
|
||||
}
|
||||
|
||||
# --- Signal Handling ---
|
||||
# Global variable to hold child PID
|
||||
child_pid=0
|
||||
# Use a persistent log file name
|
||||
PROGRESS_LOG_FILE="fallback_verification_progress.log"
|
||||
|
||||
cleanup() {
|
||||
echo "" # Newline after ^C
|
||||
log_error "Interrupt received. Cleaning up any running child process..."
|
||||
if [ "$child_pid" -ne 0 ]; then
|
||||
log_info "Killing child process (PID: $child_pid) and its group..."
|
||||
kill -TERM -- "-$child_pid" 2>/dev/null || kill -KILL -- "-$child_pid" 2>/dev/null
|
||||
child_pid=0
|
||||
fi
|
||||
# DO NOT delete the progress log file on interrupt
|
||||
log_info "Progress saved in: $PROGRESS_LOG_FILE"
|
||||
exit 130 # Exit with code indicating interrupt
|
||||
}
|
||||
|
||||
# Trap SIGINT (Ctrl+C) and SIGTERM
|
||||
trap cleanup INT TERM
|
||||
|
||||
# --- Configuration ---
|
||||
# Determine the project root relative to this script's location
|
||||
# Use a robust method to find the script's own directory
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
# Assumes this script is in tests/e2e/
|
||||
PROJECT_ROOT_DIR="$( cd "$SCRIPT_DIR/../.." &> /dev/null && pwd )"
|
||||
SUPPORTED_MODELS_FILE="$PROJECT_ROOT_DIR/scripts/modules/supported-models.json"
|
||||
BASE_RUNS_DIR="$PROJECT_ROOT_DIR/tests/e2e/_runs"
|
||||
|
||||
# --- Determine Target Run Directory ---
|
||||
TARGET_RUN_DIR=""
|
||||
if [ "$#" -ge 1 ] && [ -n "$1" ]; then
|
||||
# Use provided argument if it exists
|
||||
TARGET_RUN_DIR="$1"
|
||||
# Make path absolute if it's relative
|
||||
if [[ "$TARGET_RUN_DIR" != /* ]]; then
|
||||
TARGET_RUN_DIR="$(pwd)/$TARGET_RUN_DIR"
|
||||
fi
|
||||
echo "[INFO] Using provided target run directory: $TARGET_RUN_DIR"
|
||||
else
|
||||
# Find the latest run directory
|
||||
echo "[INFO] No run directory provided, finding latest in $BASE_RUNS_DIR..."
|
||||
TARGET_RUN_DIR=$(ls -td "$BASE_RUNS_DIR"/run_* 2>/dev/null | head -n 1)
|
||||
if [ -z "$TARGET_RUN_DIR" ]; then
|
||||
echo "[ERROR] No run directories found matching 'run_*' in $BASE_RUNS_DIR. Cannot proceed." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[INFO] Found latest run directory: $TARGET_RUN_DIR"
|
||||
fi
|
||||
|
||||
# Validate the target directory
|
||||
if [ ! -d "$TARGET_RUN_DIR" ]; then
|
||||
echo "[ERROR] Target run directory not found or is not a directory: $TARGET_RUN_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Change to Target Directory ---
|
||||
echo "[INFO] Changing working directory to: $TARGET_RUN_DIR"
|
||||
if ! cd "$TARGET_RUN_DIR"; then
|
||||
echo "[ERROR] Failed to cd into target directory: $TARGET_RUN_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[INFO] Now operating inside: $(pwd)"
|
||||
|
||||
# --- Now we are inside the target run directory ---
|
||||
overall_start_time=$(date +%s)
|
||||
test_step_count=0
|
||||
log_info "Starting fallback verification script execution in $(pwd)"
|
||||
log_info "Progress will be logged to: $(pwd)/$PROGRESS_LOG_FILE"
|
||||
|
||||
# --- Dependency Checks ---
|
||||
log_step "Checking for dependencies (jq) in verification script"
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_error "Dependency 'jq' is not installed or not found in PATH."
|
||||
exit 1
|
||||
fi
|
||||
log_success "Dependency 'jq' found."
|
||||
|
||||
# --- Verification Logic ---
|
||||
log_step "Starting/Resuming Fallback Model (generateObjectService) Verification"
|
||||
# Ensure progress log exists, create if not
|
||||
touch "$PROGRESS_LOG_FILE"
|
||||
|
||||
# Ensure the supported models file exists (using absolute path)
|
||||
if [ ! -f "$SUPPORTED_MODELS_FILE" ]; then
|
||||
log_error "supported-models.json not found at absolute path: $SUPPORTED_MODELS_FILE."
|
||||
exit 1
|
||||
fi
|
||||
log_info "Using supported models file: $SUPPORTED_MODELS_FILE"
|
||||
|
||||
# Ensure subtask 1.1 exists (basic check, main script should guarantee)
|
||||
# Check for tasks.json in the current directory (which is now the run dir)
|
||||
if [ ! -f "tasks/tasks.json" ]; then
|
||||
log_error "tasks/tasks.json not found in current directory ($(pwd)). Was this run directory properly initialized?"
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '.tasks[] | select(.id == 1) | .subtasks[] | select(.id == 1)' tasks/tasks.json > /dev/null 2>&1; then
|
||||
log_error "Subtask 1.1 not found in tasks.json within $(pwd). Cannot perform update-subtask tests."
|
||||
exit 1
|
||||
fi
|
||||
log_info "Subtask 1.1 found in $(pwd)/tasks/tasks.json, proceeding with verification."
|
||||
|
||||
# Read providers and models using jq
|
||||
jq -c 'to_entries[] | .key as $provider | .value[] | select(.allowed_roles[]? == "fallback") | {provider: $provider, id: .id}' "$SUPPORTED_MODELS_FILE" | while IFS= read -r model_info; do
|
||||
provider=$(echo "$model_info" | jq -r '.provider')
|
||||
model_id=$(echo "$model_info" | jq -r '.id')
|
||||
flag="" # Default flag
|
||||
|
||||
# Check if already tested
|
||||
# Use grep -Fq for fixed string and quiet mode
|
||||
if grep -Fq "${provider},${model_id}," "$PROGRESS_LOG_FILE"; then
|
||||
log_info "--- Skipping: $provider / $model_id (already tested, result in $PROGRESS_LOG_FILE) ---"
|
||||
continue
|
||||
fi
|
||||
|
||||
log_info "--- Verifying: $provider / $model_id ---"
|
||||
|
||||
# Determine provider flag
|
||||
if [ "$provider" == "openrouter" ]; then
|
||||
flag="--openrouter"
|
||||
elif [ "$provider" == "ollama" ]; then
|
||||
flag="--ollama"
|
||||
fi
|
||||
|
||||
# 1. Set the main model
|
||||
if ! command -v task-master &> /dev/null; then
|
||||
log_error "task-master command not found."
|
||||
echo "[INSTRUCTION] Please run 'npm link task-master-ai' in the project root first."
|
||||
exit 1
|
||||
fi
|
||||
log_info "Setting main model to $model_id ${flag:+using flag $flag}..."
|
||||
set_model_cmd="task-master models --set-main \"$model_id\" $flag"
|
||||
model_set_status="SUCCESS"
|
||||
if ! eval $set_model_cmd > /dev/null 2>&1; then
|
||||
log_error "Failed to set main model for $provider / $model_id. Skipping test."
|
||||
echo "$provider,$model_id,SET_MODEL_FAILED" >> "$PROGRESS_LOG_FILE"
|
||||
continue # Skip the actual test if setting fails
|
||||
fi
|
||||
log_info "Set main model ok."
|
||||
|
||||
# 2. Run update-subtask
|
||||
log_info "Running update-subtask --id=1.1 --prompt='Test generateObjectService' (timeout 120s)"
|
||||
update_subtask_output_file="update_subtask_raw_output_${provider}_${model_id//\//_}.log"
|
||||
|
||||
timeout 120s task-master update-subtask --id=1.1 --prompt="Simple test prompt to verify generateObjectService call." > "$update_subtask_output_file" 2>&1 &
|
||||
child_pid=$!
|
||||
wait "$child_pid"
|
||||
update_subtask_exit_code=$?
|
||||
child_pid=0
|
||||
|
||||
# 3. Check result and log persistently
|
||||
result_status=""
|
||||
if [ $update_subtask_exit_code -eq 0 ] && grep -q "Successfully updated subtask #1.1" "$update_subtask_output_file"; then
|
||||
log_success "update-subtask succeeded for $provider / $model_id (Verified Output)."
|
||||
result_status="SUCCESS"
|
||||
elif [ $update_subtask_exit_code -eq 124 ]; then
|
||||
log_error "update-subtask TIMED OUT for $provider / $model_id. Check $update_subtask_output_file."
|
||||
result_status="FAILED_TIMEOUT"
|
||||
elif [ $update_subtask_exit_code -eq 130 ] || [ $update_subtask_exit_code -eq 143 ]; then
|
||||
log_error "update-subtask INTERRUPTED for $provider / $model_id."
|
||||
result_status="INTERRUPTED" # Record interruption
|
||||
# Don't exit the loop, allow script to finish or be interrupted again
|
||||
else
|
||||
log_error "update-subtask FAILED for $provider / $model_id (Exit Code: $update_subtask_exit_code). Check $update_subtask_output_file."
|
||||
result_status="FAILED"
|
||||
fi
|
||||
|
||||
# Append result to the persistent log file
|
||||
echo "$provider,$model_id,$result_status" >> "$PROGRESS_LOG_FILE"
|
||||
|
||||
done # End of fallback verification loop
|
||||
|
||||
# --- Generate Final Verification Report to STDOUT ---
|
||||
# Report reads from the persistent PROGRESS_LOG_FILE
|
||||
echo ""
|
||||
echo "--- Fallback Model Verification Report (via $0) ---"
|
||||
echo "Executed inside run directory: $(pwd)"
|
||||
echo "Progress log: $(pwd)/$PROGRESS_LOG_FILE"
|
||||
echo ""
|
||||
echo "Test Command: task-master update-subtask --id=1.1 --prompt=\"...\" (tests generateObjectService)"
|
||||
echo "Models were tested by setting them as the 'main' model temporarily."
|
||||
echo "Results based on exit code and output verification:"
|
||||
echo ""
|
||||
echo "Models CONFIRMED to support generateObjectService (Keep 'fallback' role):"
|
||||
awk -F',' '$3 == "SUCCESS" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
|
||||
echo ""
|
||||
echo "Models FAILED generateObjectService test (Suggest REMOVING 'fallback' role):"
|
||||
awk -F',' '$3 == "FAILED" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
|
||||
echo ""
|
||||
echo "Models TIMED OUT during test (Suggest REMOVING 'fallback' role):"
|
||||
awk -F',' '$3 == "FAILED_TIMEOUT" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
|
||||
echo ""
|
||||
echo "Models where setting the model failed (Inconclusive):"
|
||||
awk -F',' '$3 == "SET_MODEL_FAILED" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
|
||||
echo ""
|
||||
echo "Models INTERRUPTED during test (Inconclusive - Rerun):"
|
||||
awk -F',' '$3 == "INTERRUPTED" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
|
||||
echo ""
|
||||
echo "-------------------------------------------------------"
|
||||
echo ""
|
||||
|
||||
# Don't clean up the progress log
|
||||
# if [ -f "$PROGRESS_LOG_FILE" ]; then
|
||||
# rm "$PROGRESS_LOG_FILE"
|
||||
# fi
|
||||
|
||||
log_info "Finished Fallback Model (generateObjectService) Verification Script"
|
||||
|
||||
# Remove trap before exiting normally
|
||||
trap - INT TERM
|
||||
|
||||
exit 0 # Exit successfully after printing the report
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to test the LLM analysis function independently
|
||||
|
||||
# Exit on error
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
# Source the helper functions
|
||||
HELPER_SCRIPT="tests/e2e/e2e_helpers.sh"
|
||||
if [ -f "$HELPER_SCRIPT" ]; then
|
||||
source "$HELPER_SCRIPT"
|
||||
echo "[INFO] Sourced helper script: $HELPER_SCRIPT"
|
||||
else
|
||||
echo "[ERROR] Helper script not found at $HELPER_SCRIPT. Exiting." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Configuration ---
|
||||
# Get the absolute path to the project root (assuming this script is run from the root)
|
||||
PROJECT_ROOT="$(pwd)"
|
||||
|
||||
# --- Argument Parsing ---
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <path_to_log_file> <path_to_test_run_directory>" >&2
|
||||
echo "Example: $0 tests/e2e/log/e2e_run_YYYYMMDD_HHMMSS.log tests/e2e/_runs/run_YYYYMMDD_HHMMSS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_FILE_REL="$1" # Relative path from project root
|
||||
TEST_RUN_DIR_REL="$2" # Relative path from project root
|
||||
|
||||
# Construct absolute paths
|
||||
LOG_FILE_ABS="$PROJECT_ROOT/$LOG_FILE_REL"
|
||||
TEST_RUN_DIR_ABS="$PROJECT_ROOT/$TEST_RUN_DIR_REL"
|
||||
|
||||
# --- Validation ---
|
||||
if [ ! -f "$LOG_FILE_ABS" ]; then
|
||||
echo "[ERROR] Log file not found: $LOG_FILE_ABS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$TEST_RUN_DIR_ABS" ]; then
|
||||
echo "[ERROR] Test run directory not found: $TEST_RUN_DIR_ABS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$TEST_RUN_DIR_ABS/.env" ]; then
|
||||
echo "[ERROR] .env file not found in test run directory: $TEST_RUN_DIR_ABS/.env" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# --- Execution ---
|
||||
echo "[INFO] Changing directory to test run directory: $TEST_RUN_DIR_ABS"
|
||||
cd "$TEST_RUN_DIR_ABS" || { echo "[ERROR] Failed to cd into $TEST_RUN_DIR_ABS"; exit 1; }
|
||||
|
||||
echo "[INFO] Current directory: $(pwd)"
|
||||
echo "[INFO] Calling analyze_log_with_llm function with log file: $LOG_FILE_ABS"
|
||||
|
||||
# Call the function (sourced earlier)
|
||||
analyze_log_with_llm "$LOG_FILE_ABS"
|
||||
ANALYSIS_EXIT_CODE=$?
|
||||
|
||||
echo "[INFO] analyze_log_with_llm finished with exit code: $ANALYSIS_EXIT_CODE"
|
||||
|
||||
# Optional: cd back to original directory
|
||||
# echo "[INFO] Changing back to project root: $PROJECT_ROOT"
|
||||
# cd "$PROJECT_ROOT"
|
||||
|
||||
exit $ANALYSIS_EXIT_CODE
|
||||
16
tests/fixtures/.taskmasterconfig
vendored
16
tests/fixtures/.taskmasterconfig
vendored
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "openai",
|
||||
"modelId": "gpt-4o"
|
||||
},
|
||||
"research": {
|
||||
"provider": "perplexity",
|
||||
"modelId": "sonar-pro"
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "anthropic",
|
||||
"modelId": "claude-3-haiku-20240307"
|
||||
}
|
||||
}
|
||||
}
|
||||
110
tests/fixtures/sample-prd.txt
vendored
110
tests/fixtures/sample-prd.txt
vendored
@@ -1,82 +1,42 @@
|
||||
<context>
|
||||
# Overview
|
||||
This document outlines the requirements for a minimal web-based URL Shortener application. The application allows users to input a long URL and receive a shorter, alias URL that redirects to the original destination. This serves as a basic example of a micro-SaaS product. It's intended for anyone needing to create shorter links for sharing. The value is in providing a simple, functional utility accessible via a web browser.
|
||||
# Sample PRD for Testing
|
||||
|
||||
# Core Features
|
||||
1. **URL Input & Shortening:** A user interface with an input field for pasting a long URL and a button to trigger the shortening process.
|
||||
- *Why:* The primary function for the user interaction.
|
||||
- *How:* A React component with a text input and a submit button. Clicking the button sends the long URL to a backend API.
|
||||
2. **Short URL Display:** After successful shortening, the application displays the newly generated short URL to the user.
|
||||
- *Why:* Provides the result of the core function to the user.
|
||||
- *How:* The React frontend updates to show the short URL returned by the API (e.g., `http://your-domain.com/aB3cD`). Include a "copy to clipboard" button for convenience.
|
||||
3. **URL Redirection:** Accessing a generated short URL in a browser redirects the user to the original long URL.
|
||||
- *Why:* The fundamental purpose of the shortened link.
|
||||
* *How:* A backend API endpoint handles requests to `/:shortCode`. It looks up the code in a data store and issues an HTTP redirect (301 or 302) to the corresponding long URL.
|
||||
4. **Basic Persistence:** Short URL mappings (short code -> long URL) persist across requests.
|
||||
- *Why:* Short URLs need to remain functional after creation.
|
||||
* *How:* A simple backend data store (e.g., initially an in-memory object for testing, then potentially a JSON file or simple database) holds the mappings.
|
||||
|
||||
# User Experience
|
||||
- **User Persona:** Anyone wanting to shorten a long web link.
|
||||
- **Key User Flow:** User visits the web app -> Pastes a long URL into the input field -> Clicks "Shorten" -> Sees the generated short URL -> Copies the short URL -> (Later) Uses the short URL in a browser and gets redirected.
|
||||
- **UI/UX Considerations:** Clean, minimal single-page interface. Clear input field, prominent button, easy-to-read display of the short URL, copy button. Basic validation feedback (e.g., "Invalid URL", "Success!").
|
||||
</context>
|
||||
<PRD>
|
||||
# Technical Architecture
|
||||
- **System Components:**
|
||||
- Frontend: Single Page Application (SPA) built with Vite + React.
|
||||
- Backend: Simple API server (e.g., Node.js with Express).
|
||||
- **Data Model:** A key-value store mapping `shortCode` (string) to `longUrl` (string).
|
||||
- **APIs & Integrations:**
|
||||
- Backend API:
|
||||
- `POST /api/shorten`: Accepts `{ longUrl: string }` in the request body. Generates a unique `shortCode`, stores the mapping, returns `{ shortUrl: string }`.
|
||||
- `GET /:shortCode`: Looks up `shortCode`. If found, performs HTTP redirect to `longUrl`. If not found, returns 404.
|
||||
- **Infrastructure:** Frontend can be hosted on static hosting. Backend needs a simple server environment (Node.js).
|
||||
- **Libraries:**
|
||||
- Frontend: `react`, `react-dom`, `axios` (or `fetch` API) for API calls. Consider a simple state management solution if needed (e.g., `useState`, `useContext`).
|
||||
- Backend: `express`, `nanoid` (or similar for short code generation).
|
||||
|
||||
## System Components
|
||||
1. **Task Management Core**
|
||||
- Tasks.json file structure
|
||||
- Task model with dependencies
|
||||
- Task state management
|
||||
|
||||
2. **Command Line Interface**
|
||||
- Command parsing and execution
|
||||
- Display utilities
|
||||
|
||||
## Data Models
|
||||
|
||||
### Task Model
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Task Title",
|
||||
"description": "Brief task description",
|
||||
"status": "pending|done|deferred",
|
||||
"dependencies": [0],
|
||||
"priority": "high|medium|low",
|
||||
"details": "Implementation instructions",
|
||||
"testStrategy": "Verification approach"
|
||||
}
|
||||
```
|
||||
|
||||
# Development Roadmap
|
||||
- **MVP Requirements:**
|
||||
1. Setup Vite + React project.
|
||||
2. Create basic React UI components (InputForm, ResultDisplay).
|
||||
3. Setup basic Node.js/Express backend server.
|
||||
4. Implement backend data storage module (start with in-memory object).
|
||||
5. Implement unique short code generation logic (e.g., using `nanoid`).
|
||||
6. Implement backend `POST /api/shorten` endpoint logic.
|
||||
7. Implement backend `GET /:shortCode` redirect logic.
|
||||
8. Implement frontend logic to take input, call `POST /api/shorten`, and display the result.
|
||||
9. Basic frontend input validation (check if likely a URL).
|
||||
- **Future Enhancements:** User accounts, custom short codes, analytics (click tracking), using a persistent database, error handling improvements, UI styling. (Out of scope for MVP).
|
||||
|
||||
# Logical Dependency Chain
|
||||
1. Vite + React Project Setup.
|
||||
2. Basic Backend Server Setup (Express).
|
||||
3. Backend Storage Module (in-memory first).
|
||||
4. Short Code Generation Logic.
|
||||
5. Implement `POST /api/shorten` endpoint (depends on 3 & 4).
|
||||
6. Implement `GET /:shortCode` endpoint (depends on 3).
|
||||
7. Frontend UI Components.
|
||||
8. Frontend logic to call `POST /api/shorten` (depends on 5 & 7).
|
||||
9. Frontend display logic (depends on 7 & 8).
|
||||
*Goal is to get the backend API working first, then build the frontend to consume it.*
|
||||
## Phase 1: Core Task Management System
|
||||
1. **Task Data Structure**
|
||||
- Implement the tasks.json structure
|
||||
- Create file system interactions
|
||||
|
||||
# Risks and Mitigations
|
||||
- **Risk:** Short code collisions (generating the same code twice).
|
||||
- **Mitigation (MVP):** Use a library like `nanoid` with sufficient length to make collisions highly improbable for a simple service. Add a retry loop in generation if a collision *is* detected (check if code exists before storing).
|
||||
- **Risk:** Storing invalid or malicious URLs.
|
||||
- **Mitigation (MVP):** Basic URL validation on the frontend (simple regex) and potentially on the backend. Sanitize input. Advanced checks are out of scope.
|
||||
- **Risk:** Scalability of in-memory store.
|
||||
- **Mitigation (MVP):** Acceptable for MVP. Acknowledge need for persistent database (JSON file, Redis, SQL/NoSQL DB) for future enhancement.
|
||||
|
||||
# Appendix
|
||||
- Example Data Store (in-memory object):
|
||||
```javascript
|
||||
// backend/storage.js
|
||||
const urlMap = {
|
||||
'aB3cD': 'https://very-long-url-example.com/with/path/and/query?params=true',
|
||||
'xY7zW': 'https://another-example.org/'
|
||||
};
|
||||
// ... functions to get/set URLs ...
|
||||
```
|
||||
</PRD>
|
||||
2. **Command Line Interface Foundation**
|
||||
- Implement command parsing
|
||||
- Create help documentation
|
||||
</PRD>
|
||||
@@ -1,350 +0,0 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// --- Define mock functions ---
|
||||
const mockGetMainModelId = jest.fn().mockReturnValue('claude-3-opus');
|
||||
const mockGetResearchModelId = jest.fn().mockReturnValue('gpt-4-turbo');
|
||||
const mockGetFallbackModelId = jest.fn().mockReturnValue('claude-3-haiku');
|
||||
const mockSetMainModel = jest.fn().mockResolvedValue(true);
|
||||
const mockSetResearchModel = jest.fn().mockResolvedValue(true);
|
||||
const mockSetFallbackModel = jest.fn().mockResolvedValue(true);
|
||||
const mockGetAvailableModels = jest.fn().mockReturnValue([
|
||||
{ id: 'claude-3-opus', name: 'Claude 3 Opus', provider: 'anthropic' },
|
||||
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai' },
|
||||
{ id: 'claude-3-haiku', name: 'Claude 3 Haiku', provider: 'anthropic' },
|
||||
{ id: 'claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'anthropic' }
|
||||
]);
|
||||
|
||||
// Mock UI related functions
|
||||
const mockDisplayHelp = jest.fn();
|
||||
const mockDisplayBanner = jest.fn();
|
||||
const mockLog = jest.fn();
|
||||
const mockStartLoadingIndicator = jest.fn(() => ({ stop: jest.fn() }));
|
||||
const mockStopLoadingIndicator = jest.fn();
|
||||
|
||||
// --- Setup mocks using unstable_mockModule (recommended for ES modules) ---
|
||||
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
|
||||
getMainModelId: mockGetMainModelId,
|
||||
getResearchModelId: mockGetResearchModelId,
|
||||
getFallbackModelId: mockGetFallbackModelId,
|
||||
setMainModel: mockSetMainModel,
|
||||
setResearchModel: mockSetResearchModel,
|
||||
setFallbackModel: mockSetFallbackModel,
|
||||
getAvailableModels: mockGetAvailableModels,
|
||||
VALID_PROVIDERS: ['anthropic', 'openai']
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../../scripts/modules/ui.js', () => ({
|
||||
displayHelp: mockDisplayHelp,
|
||||
displayBanner: mockDisplayBanner,
|
||||
log: mockLog,
|
||||
startLoadingIndicator: mockStartLoadingIndicator,
|
||||
stopLoadingIndicator: mockStopLoadingIndicator
|
||||
}));
|
||||
|
||||
// --- Mock chalk for consistent output formatting ---
|
||||
const mockChalk = {
|
||||
red: jest.fn((text) => text),
|
||||
yellow: jest.fn((text) => text),
|
||||
blue: jest.fn((text) => text),
|
||||
green: jest.fn((text) => text),
|
||||
gray: jest.fn((text) => text),
|
||||
dim: jest.fn((text) => text),
|
||||
bold: {
|
||||
cyan: jest.fn((text) => text),
|
||||
white: jest.fn((text) => text),
|
||||
red: jest.fn((text) => text)
|
||||
},
|
||||
cyan: {
|
||||
bold: jest.fn((text) => text)
|
||||
},
|
||||
white: {
|
||||
bold: jest.fn((text) => text)
|
||||
}
|
||||
};
|
||||
// Default function for chalk itself
|
||||
mockChalk.default = jest.fn((text) => text);
|
||||
// Add the methods to the function itself for dual usage
|
||||
Object.keys(mockChalk).forEach((key) => {
|
||||
if (key !== 'default') mockChalk.default[key] = mockChalk[key];
|
||||
});
|
||||
|
||||
jest.unstable_mockModule('chalk', () => ({
|
||||
default: mockChalk.default
|
||||
}));
|
||||
|
||||
// --- Import modules (AFTER mock setup) ---
|
||||
let configManager, ui, chalk;
|
||||
|
||||
describe('CLI Models Command (Action Handler Test)', () => {
|
||||
// Setup dynamic imports before tests run
|
||||
beforeAll(async () => {
|
||||
configManager = await import('../../../scripts/modules/config-manager.js');
|
||||
ui = await import('../../../scripts/modules/ui.js');
|
||||
chalk = (await import('chalk')).default;
|
||||
});
|
||||
|
||||
// --- Replicate the action handler logic from commands.js ---
|
||||
async function modelsAction(options) {
|
||||
options = options || {}; // Ensure options object exists
|
||||
const availableModels = configManager.getAvailableModels();
|
||||
|
||||
const findProvider = (modelId) => {
|
||||
const modelInfo = availableModels.find((m) => m.id === modelId);
|
||||
return modelInfo?.provider;
|
||||
};
|
||||
|
||||
let modelSetAction = false;
|
||||
|
||||
try {
|
||||
if (options.setMain) {
|
||||
const modelId = options.setMain;
|
||||
if (typeof modelId !== 'string' || modelId.trim() === '') {
|
||||
console.error(
|
||||
chalk.red('Error: --set-main flag requires a valid model ID.')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const provider = findProvider(modelId);
|
||||
if (!provider) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Model ID "${modelId}" not found in available models.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (await configManager.setMainModel(provider, modelId)) {
|
||||
console.log(
|
||||
chalk.green(`Main model set to: ${modelId} (Provider: ${provider})`)
|
||||
);
|
||||
modelSetAction = true;
|
||||
} else {
|
||||
console.error(chalk.red(`Failed to set main model.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.setResearch) {
|
||||
const modelId = options.setResearch;
|
||||
if (typeof modelId !== 'string' || modelId.trim() === '') {
|
||||
console.error(
|
||||
chalk.red('Error: --set-research flag requires a valid model ID.')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const provider = findProvider(modelId);
|
||||
if (!provider) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Model ID "${modelId}" not found in available models.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (await configManager.setResearchModel(provider, modelId)) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Research model set to: ${modelId} (Provider: ${provider})`
|
||||
)
|
||||
);
|
||||
modelSetAction = true;
|
||||
} else {
|
||||
console.error(chalk.red(`Failed to set research model.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.setFallback) {
|
||||
const modelId = options.setFallback;
|
||||
if (typeof modelId !== 'string' || modelId.trim() === '') {
|
||||
console.error(
|
||||
chalk.red('Error: --set-fallback flag requires a valid model ID.')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const provider = findProvider(modelId);
|
||||
if (!provider) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Model ID "${modelId}" not found in available models.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (await configManager.setFallbackModel(provider, modelId)) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Fallback model set to: ${modelId} (Provider: ${provider})`
|
||||
)
|
||||
);
|
||||
modelSetAction = true;
|
||||
} else {
|
||||
console.error(chalk.red(`Failed to set fallback model.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!modelSetAction) {
|
||||
const currentMain = configManager.getMainModelId();
|
||||
const currentResearch = configManager.getResearchModelId();
|
||||
const currentFallback = configManager.getFallbackModelId();
|
||||
|
||||
if (!availableModels || availableModels.length === 0) {
|
||||
console.log(chalk.yellow('No models defined in configuration.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a mock table for testing - avoid using Table constructor
|
||||
const mockTableData = [];
|
||||
availableModels.forEach((model) => {
|
||||
if (model.id.startsWith('[') && model.id.endsWith(']')) return;
|
||||
mockTableData.push([
|
||||
model.id,
|
||||
model.name || 'N/A',
|
||||
model.provider || 'N/A',
|
||||
model.id === currentMain ? chalk.green(' ✓') : '',
|
||||
model.id === currentResearch ? chalk.green(' ✓') : '',
|
||||
model.id === currentFallback ? chalk.green(' ✓') : ''
|
||||
]);
|
||||
});
|
||||
|
||||
// In a real implementation, we would use cli-table3, but for testing
|
||||
// we'll just log 'Mock Table Output'
|
||||
console.log('Mock Table Output');
|
||||
}
|
||||
} catch (error) {
|
||||
// Use ui.log mock if available, otherwise console.error
|
||||
(ui.log || console.error)(
|
||||
`Error processing models command: ${error.message}`,
|
||||
'error'
|
||||
);
|
||||
if (error.stack) {
|
||||
(ui.log || console.error)(error.stack, 'debug');
|
||||
}
|
||||
throw error; // Re-throw for test failure
|
||||
}
|
||||
}
|
||||
// --- End of Action Handler Logic ---
|
||||
|
||||
let originalConsoleLog;
|
||||
let originalConsoleError;
|
||||
let originalProcessExit;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Save original console methods
|
||||
originalConsoleLog = console.log;
|
||||
originalConsoleError = console.error;
|
||||
originalProcessExit = process.exit;
|
||||
|
||||
// Mock console and process.exit
|
||||
console.log = jest.fn();
|
||||
console.error = jest.fn();
|
||||
process.exit = jest.fn((code) => {
|
||||
throw new Error(`process.exit(${code}) called`);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original console methods
|
||||
console.log = originalConsoleLog;
|
||||
console.error = originalConsoleError;
|
||||
process.exit = originalProcessExit;
|
||||
});
|
||||
|
||||
// --- Test Cases (Calling modelsAction directly) ---
|
||||
|
||||
it('should call setMainModel with correct provider and ID', async () => {
|
||||
const modelId = 'claude-3-opus';
|
||||
const expectedProvider = 'anthropic';
|
||||
await modelsAction({ setMain: modelId });
|
||||
expect(mockSetMainModel).toHaveBeenCalledWith(expectedProvider, modelId);
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`Main model set to: ${modelId}`)
|
||||
);
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`(Provider: ${expectedProvider})`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should show an error if --set-main model ID is not found', async () => {
|
||||
await expect(
|
||||
modelsAction({ setMain: 'non-existent-model' })
|
||||
).rejects.toThrow(/process.exit/); // Expect exit call
|
||||
expect(mockSetMainModel).not.toHaveBeenCalled();
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Model ID "non-existent-model" not found')
|
||||
);
|
||||
});
|
||||
|
||||
it('should call setResearchModel with correct provider and ID', async () => {
|
||||
const modelId = 'gpt-4-turbo';
|
||||
const expectedProvider = 'openai';
|
||||
await modelsAction({ setResearch: modelId });
|
||||
expect(mockSetResearchModel).toHaveBeenCalledWith(
|
||||
expectedProvider,
|
||||
modelId
|
||||
);
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`Research model set to: ${modelId}`)
|
||||
);
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`(Provider: ${expectedProvider})`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should call setFallbackModel with correct provider and ID', async () => {
|
||||
const modelId = 'claude-3-haiku';
|
||||
const expectedProvider = 'anthropic';
|
||||
await modelsAction({ setFallback: modelId });
|
||||
expect(mockSetFallbackModel).toHaveBeenCalledWith(
|
||||
expectedProvider,
|
||||
modelId
|
||||
);
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`Fallback model set to: ${modelId}`)
|
||||
);
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`(Provider: ${expectedProvider})`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should call all set*Model functions when all flags are used', async () => {
|
||||
const mainModelId = 'claude-3-opus';
|
||||
const researchModelId = 'gpt-4-turbo';
|
||||
const fallbackModelId = 'claude-3-haiku';
|
||||
const mainProvider = 'anthropic';
|
||||
const researchProvider = 'openai';
|
||||
const fallbackProvider = 'anthropic';
|
||||
|
||||
await modelsAction({
|
||||
setMain: mainModelId,
|
||||
setResearch: researchModelId,
|
||||
setFallback: fallbackModelId
|
||||
});
|
||||
expect(mockSetMainModel).toHaveBeenCalledWith(mainProvider, mainModelId);
|
||||
expect(mockSetResearchModel).toHaveBeenCalledWith(
|
||||
researchProvider,
|
||||
researchModelId
|
||||
);
|
||||
expect(mockSetFallbackModel).toHaveBeenCalledWith(
|
||||
fallbackProvider,
|
||||
fallbackModelId
|
||||
);
|
||||
});
|
||||
|
||||
it('should call specific get*ModelId and getAvailableModels and log table when run without flags', async () => {
|
||||
await modelsAction({}); // Call with empty options
|
||||
|
||||
expect(mockGetMainModelId).toHaveBeenCalled();
|
||||
expect(mockGetResearchModelId).toHaveBeenCalled();
|
||||
expect(mockGetFallbackModelId).toHaveBeenCalled();
|
||||
expect(mockGetAvailableModels).toHaveBeenCalled();
|
||||
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
// Check the mocked Table.toString() was used via console.log
|
||||
expect(console.log).toHaveBeenCalledWith('Mock Table Output');
|
||||
});
|
||||
});
|
||||
@@ -144,11 +144,11 @@ jest.mock('../../../mcp-server/src/core/utils/path-utils.js', () => ({
|
||||
}));
|
||||
|
||||
// Mock the AI module to prevent any real API calls
|
||||
jest.mock('../../../scripts/modules/ai-services-unified.js', () => ({
|
||||
// Mock the functions exported by ai-services-unified.js as needed
|
||||
// For example, if you are testing a function that uses generateTextService:
|
||||
generateTextService: jest.fn().mockResolvedValue('Mock AI Response')
|
||||
// Add other mocks for generateObjectService, streamTextService if used
|
||||
jest.mock('../../../scripts/modules/ai-services.js', () => ({
|
||||
getAnthropicClient: mockGetAnthropicClient,
|
||||
getConfiguredAnthropicClient: mockGetConfiguredAnthropicClient,
|
||||
_handleAnthropicStream: mockHandleAnthropicStream,
|
||||
parseSubtasksFromText: mockParseSubtasksFromText
|
||||
}));
|
||||
|
||||
// Mock task-manager.js to avoid real operations
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
describe('Roo Files Inclusion in Package', () => {
|
||||
// This test verifies that the required Roo files are included in the final package
|
||||
|
||||
test('package.json includes assets/** in the "files" array for Roo source files', () => {
|
||||
// Read the package.json file
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
// Check if assets/** is included in the files array (which contains Roo files)
|
||||
expect(packageJson.files).toContain('assets/**');
|
||||
});
|
||||
|
||||
test('init.js creates Roo directories and copies files', () => {
|
||||
// Read the init.js file
|
||||
const initJsPath = path.join(process.cwd(), 'scripts', 'init.js');
|
||||
const initJsContent = fs.readFileSync(initJsPath, 'utf8');
|
||||
|
||||
// Check for Roo directory creation (using more flexible pattern matching)
|
||||
const hasRooDir = initJsContent.includes(
|
||||
"ensureDirectoryExists(path.join(targetDir, '.roo"
|
||||
);
|
||||
expect(hasRooDir).toBe(true);
|
||||
|
||||
// Check for .roomodes file copying
|
||||
const hasRoomodes = initJsContent.includes("copyTemplateFile('.roomodes'");
|
||||
expect(hasRoomodes).toBe(true);
|
||||
|
||||
// Check for mode-specific patterns (using more flexible pattern matching)
|
||||
const hasArchitect = initJsContent.includes('architect');
|
||||
const hasAsk = initJsContent.includes('ask');
|
||||
const hasBoomerang = initJsContent.includes('boomerang');
|
||||
const hasCode = initJsContent.includes('code');
|
||||
const hasDebug = initJsContent.includes('debug');
|
||||
const hasTest = initJsContent.includes('test');
|
||||
|
||||
expect(hasArchitect).toBe(true);
|
||||
expect(hasAsk).toBe(true);
|
||||
expect(hasBoomerang).toBe(true);
|
||||
expect(hasCode).toBe(true);
|
||||
expect(hasDebug).toBe(true);
|
||||
expect(hasTest).toBe(true);
|
||||
});
|
||||
|
||||
test('source Roo files exist in assets directory', () => {
|
||||
// Verify that the source files for Roo integration exist
|
||||
expect(
|
||||
fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roo'))
|
||||
).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roomodes'))
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Roo Initialization Functionality', () => {
|
||||
let initJsContent;
|
||||
|
||||
beforeAll(() => {
|
||||
// Read the init.js file content once for all tests
|
||||
const initJsPath = path.join(process.cwd(), 'scripts', 'init.js');
|
||||
initJsContent = fs.readFileSync(initJsPath, 'utf8');
|
||||
});
|
||||
|
||||
test('init.js creates Roo directories in createProjectStructure function', () => {
|
||||
// Check if createProjectStructure function exists
|
||||
expect(initJsContent).toContain('function createProjectStructure');
|
||||
|
||||
// Check for the line that creates the .roo directory
|
||||
const hasRooDir = initJsContent.includes(
|
||||
"ensureDirectoryExists(path.join(targetDir, '.roo'))"
|
||||
);
|
||||
expect(hasRooDir).toBe(true);
|
||||
|
||||
// Check for the line that creates .roo/rules directory
|
||||
const hasRooRulesDir = initJsContent.includes(
|
||||
"ensureDirectoryExists(path.join(targetDir, '.roo', 'rules'))"
|
||||
);
|
||||
expect(hasRooRulesDir).toBe(true);
|
||||
|
||||
// Check for the for loop that creates mode-specific directories
|
||||
const hasRooModeLoop =
|
||||
initJsContent.includes(
|
||||
"for (const mode of ['architect', 'ask', 'boomerang', 'code', 'debug', 'test'])"
|
||||
) ||
|
||||
(initJsContent.includes('for (const mode of [') &&
|
||||
initJsContent.includes('architect') &&
|
||||
initJsContent.includes('ask') &&
|
||||
initJsContent.includes('boomerang') &&
|
||||
initJsContent.includes('code') &&
|
||||
initJsContent.includes('debug') &&
|
||||
initJsContent.includes('test'));
|
||||
expect(hasRooModeLoop).toBe(true);
|
||||
});
|
||||
|
||||
test('init.js copies Roo files from assets/roocode directory', () => {
|
||||
// Check for the .roomodes case in the copyTemplateFile function
|
||||
const casesRoomodes = initJsContent.includes("case '.roomodes':");
|
||||
expect(casesRoomodes).toBe(true);
|
||||
|
||||
// Check that assets/roocode appears somewhere in the file
|
||||
const hasRoocodePath = initJsContent.includes("'assets', 'roocode'");
|
||||
expect(hasRoocodePath).toBe(true);
|
||||
|
||||
// Check that roomodes file is copied
|
||||
const copiesRoomodes = initJsContent.includes(
|
||||
"copyTemplateFile('.roomodes'"
|
||||
);
|
||||
expect(copiesRoomodes).toBe(true);
|
||||
});
|
||||
|
||||
test('init.js has code to copy rule files for each mode', () => {
|
||||
// Look for template copying for rule files
|
||||
const hasModeRulesCopying =
|
||||
initJsContent.includes('copyTemplateFile(') &&
|
||||
initJsContent.includes('rules-') &&
|
||||
initJsContent.includes('-rules');
|
||||
expect(hasModeRulesCopying).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -25,9 +25,9 @@ global.wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
if (process.env.SILENCE_CONSOLE === 'true') {
|
||||
global.console = {
|
||||
...console,
|
||||
log: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn()
|
||||
};
|
||||
}
|
||||
|
||||
350
tests/unit/ai-client-utils.test.js
Normal file
350
tests/unit/ai-client-utils.test.js
Normal file
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* 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,289 +0,0 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock config-manager
|
||||
const mockGetMainProvider = jest.fn();
|
||||
const mockGetMainModelId = jest.fn();
|
||||
const mockGetResearchProvider = jest.fn();
|
||||
const mockGetResearchModelId = jest.fn();
|
||||
const mockGetFallbackProvider = jest.fn();
|
||||
const mockGetFallbackModelId = jest.fn();
|
||||
const mockGetParametersForRole = jest.fn();
|
||||
|
||||
jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
|
||||
getMainProvider: mockGetMainProvider,
|
||||
getMainModelId: mockGetMainModelId,
|
||||
getResearchProvider: mockGetResearchProvider,
|
||||
getResearchModelId: mockGetResearchModelId,
|
||||
getFallbackProvider: mockGetFallbackProvider,
|
||||
getFallbackModelId: mockGetFallbackModelId,
|
||||
getParametersForRole: mockGetParametersForRole
|
||||
}));
|
||||
|
||||
// Mock AI Provider Modules
|
||||
const mockGenerateAnthropicText = jest.fn();
|
||||
const mockStreamAnthropicText = jest.fn();
|
||||
const mockGenerateAnthropicObject = jest.fn();
|
||||
jest.unstable_mockModule('../../src/ai-providers/anthropic.js', () => ({
|
||||
generateAnthropicText: mockGenerateAnthropicText,
|
||||
streamAnthropicText: mockStreamAnthropicText,
|
||||
generateAnthropicObject: mockGenerateAnthropicObject
|
||||
}));
|
||||
|
||||
const mockGeneratePerplexityText = jest.fn();
|
||||
const mockStreamPerplexityText = jest.fn();
|
||||
const mockGeneratePerplexityObject = jest.fn();
|
||||
jest.unstable_mockModule('../../src/ai-providers/perplexity.js', () => ({
|
||||
generatePerplexityText: mockGeneratePerplexityText,
|
||||
streamPerplexityText: mockStreamPerplexityText,
|
||||
generatePerplexityObject: mockGeneratePerplexityObject
|
||||
}));
|
||||
|
||||
// ... Mock other providers (google, openai, etc.) similarly ...
|
||||
|
||||
// Mock utils logger, API key resolver, AND findProjectRoot
|
||||
const mockLog = jest.fn();
|
||||
const mockResolveEnvVariable = jest.fn();
|
||||
const mockFindProjectRoot = jest.fn();
|
||||
jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
|
||||
log: mockLog,
|
||||
resolveEnvVariable: mockResolveEnvVariable,
|
||||
findProjectRoot: mockFindProjectRoot
|
||||
}));
|
||||
|
||||
// Import the module to test (AFTER mocks)
|
||||
const { generateTextService } = await import(
|
||||
'../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
describe('Unified AI Services', () => {
|
||||
const fakeProjectRoot = '/fake/project/root'; // Define for reuse
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear mocks before each test
|
||||
jest.clearAllMocks(); // Clears all mocks
|
||||
|
||||
// Set default mock behaviors
|
||||
mockGetMainProvider.mockReturnValue('anthropic');
|
||||
mockGetMainModelId.mockReturnValue('test-main-model');
|
||||
mockGetResearchProvider.mockReturnValue('perplexity');
|
||||
mockGetResearchModelId.mockReturnValue('test-research-model');
|
||||
mockGetFallbackProvider.mockReturnValue('anthropic');
|
||||
mockGetFallbackModelId.mockReturnValue('test-fallback-model');
|
||||
mockGetParametersForRole.mockImplementation((role) => {
|
||||
if (role === 'main') return { maxTokens: 100, temperature: 0.5 };
|
||||
if (role === 'research') return { maxTokens: 200, temperature: 0.3 };
|
||||
if (role === 'fallback') return { maxTokens: 150, temperature: 0.6 };
|
||||
return { maxTokens: 100, temperature: 0.5 }; // Default
|
||||
});
|
||||
mockResolveEnvVariable.mockImplementation((key) => {
|
||||
if (key === 'ANTHROPIC_API_KEY') return 'mock-anthropic-key';
|
||||
if (key === 'PERPLEXITY_API_KEY') return 'mock-perplexity-key';
|
||||
return null;
|
||||
});
|
||||
|
||||
// Set a default behavior for the new mock
|
||||
mockFindProjectRoot.mockReturnValue(fakeProjectRoot);
|
||||
});
|
||||
|
||||
describe('generateTextService', () => {
|
||||
test('should use main provider/model and succeed', async () => {
|
||||
mockGenerateAnthropicText.mockResolvedValue('Main provider response');
|
||||
|
||||
const params = {
|
||||
role: 'main',
|
||||
session: { env: {} },
|
||||
systemPrompt: 'System',
|
||||
prompt: 'Test'
|
||||
};
|
||||
const result = await generateTextService(params);
|
||||
|
||||
expect(result).toBe('Main provider response');
|
||||
expect(mockGetMainProvider).toHaveBeenCalledWith(fakeProjectRoot);
|
||||
expect(mockGetMainModelId).toHaveBeenCalledWith(fakeProjectRoot);
|
||||
expect(mockGetParametersForRole).toHaveBeenCalledWith(
|
||||
'main',
|
||||
fakeProjectRoot
|
||||
);
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
'ANTHROPIC_API_KEY',
|
||||
params.session,
|
||||
fakeProjectRoot
|
||||
);
|
||||
expect(mockGenerateAnthropicText).toHaveBeenCalledTimes(1);
|
||||
expect(mockGenerateAnthropicText).toHaveBeenCalledWith({
|
||||
apiKey: 'mock-anthropic-key',
|
||||
modelId: 'test-main-model',
|
||||
maxTokens: 100,
|
||||
temperature: 0.5,
|
||||
messages: [
|
||||
{ role: 'system', content: 'System' },
|
||||
{ role: 'user', content: 'Test' }
|
||||
]
|
||||
});
|
||||
expect(mockGeneratePerplexityText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should fall back to fallback provider if main fails', async () => {
|
||||
const mainError = new Error('Main provider failed');
|
||||
mockGenerateAnthropicText
|
||||
.mockRejectedValueOnce(mainError)
|
||||
.mockResolvedValueOnce('Fallback provider response');
|
||||
|
||||
const explicitRoot = '/explicit/test/root';
|
||||
const params = {
|
||||
role: 'main',
|
||||
prompt: 'Fallback test',
|
||||
projectRoot: explicitRoot
|
||||
};
|
||||
const result = await generateTextService(params);
|
||||
|
||||
expect(result).toBe('Fallback provider response');
|
||||
expect(mockGetMainProvider).toHaveBeenCalledWith(explicitRoot);
|
||||
expect(mockGetFallbackProvider).toHaveBeenCalledWith(explicitRoot);
|
||||
expect(mockGetParametersForRole).toHaveBeenCalledWith(
|
||||
'main',
|
||||
explicitRoot
|
||||
);
|
||||
expect(mockGetParametersForRole).toHaveBeenCalledWith(
|
||||
'fallback',
|
||||
explicitRoot
|
||||
);
|
||||
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
'ANTHROPIC_API_KEY',
|
||||
undefined,
|
||||
explicitRoot
|
||||
);
|
||||
|
||||
expect(mockGenerateAnthropicText).toHaveBeenCalledTimes(2);
|
||||
expect(mockGeneratePerplexityText).not.toHaveBeenCalled();
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
expect.stringContaining('Service call failed for role main')
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('New AI service call with role: fallback')
|
||||
);
|
||||
});
|
||||
|
||||
test('should fall back to research provider if main and fallback fail', async () => {
|
||||
const mainError = new Error('Main failed');
|
||||
const fallbackError = new Error('Fallback failed');
|
||||
mockGenerateAnthropicText
|
||||
.mockRejectedValueOnce(mainError)
|
||||
.mockRejectedValueOnce(fallbackError);
|
||||
mockGeneratePerplexityText.mockResolvedValue(
|
||||
'Research provider response'
|
||||
);
|
||||
|
||||
const params = { role: 'main', prompt: 'Research fallback test' };
|
||||
const result = await generateTextService(params);
|
||||
|
||||
expect(result).toBe('Research provider response');
|
||||
expect(mockGetMainProvider).toHaveBeenCalledWith(fakeProjectRoot);
|
||||
expect(mockGetFallbackProvider).toHaveBeenCalledWith(fakeProjectRoot);
|
||||
expect(mockGetResearchProvider).toHaveBeenCalledWith(fakeProjectRoot);
|
||||
expect(mockGetParametersForRole).toHaveBeenCalledWith(
|
||||
'main',
|
||||
fakeProjectRoot
|
||||
);
|
||||
expect(mockGetParametersForRole).toHaveBeenCalledWith(
|
||||
'fallback',
|
||||
fakeProjectRoot
|
||||
);
|
||||
expect(mockGetParametersForRole).toHaveBeenCalledWith(
|
||||
'research',
|
||||
fakeProjectRoot
|
||||
);
|
||||
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
'ANTHROPIC_API_KEY',
|
||||
undefined,
|
||||
fakeProjectRoot
|
||||
);
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
'ANTHROPIC_API_KEY',
|
||||
undefined,
|
||||
fakeProjectRoot
|
||||
);
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
'PERPLEXITY_API_KEY',
|
||||
undefined,
|
||||
fakeProjectRoot
|
||||
);
|
||||
|
||||
expect(mockGenerateAnthropicText).toHaveBeenCalledTimes(2);
|
||||
expect(mockGeneratePerplexityText).toHaveBeenCalledTimes(1);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
expect.stringContaining('Service call failed for role fallback')
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('New AI service call with role: research')
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error if all providers in sequence fail', async () => {
|
||||
mockGenerateAnthropicText.mockRejectedValue(
|
||||
new Error('Anthropic failed')
|
||||
);
|
||||
mockGeneratePerplexityText.mockRejectedValue(
|
||||
new Error('Perplexity failed')
|
||||
);
|
||||
|
||||
const params = { role: 'main', prompt: 'All fail test' };
|
||||
|
||||
await expect(generateTextService(params)).rejects.toThrow(
|
||||
'Perplexity failed' // Error from the last attempt (research)
|
||||
);
|
||||
|
||||
expect(mockGenerateAnthropicText).toHaveBeenCalledTimes(2); // main, fallback
|
||||
expect(mockGeneratePerplexityText).toHaveBeenCalledTimes(1); // research
|
||||
});
|
||||
|
||||
test('should handle retryable errors correctly', async () => {
|
||||
const retryableError = new Error('Rate limit');
|
||||
mockGenerateAnthropicText
|
||||
.mockRejectedValueOnce(retryableError) // Fails once
|
||||
.mockResolvedValue('Success after retry'); // Succeeds on retry
|
||||
|
||||
const params = { role: 'main', prompt: 'Retry success test' };
|
||||
const result = await generateTextService(params);
|
||||
|
||||
expect(result).toBe('Success after retry');
|
||||
expect(mockGenerateAnthropicText).toHaveBeenCalledTimes(2); // Initial + 1 retry
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('Retryable error detected. Retrying')
|
||||
);
|
||||
});
|
||||
|
||||
test('should use default project root or handle null if findProjectRoot returns null', async () => {
|
||||
mockFindProjectRoot.mockReturnValue(null); // Simulate not finding root
|
||||
mockGenerateAnthropicText.mockResolvedValue('Response with no root');
|
||||
|
||||
const params = { role: 'main', prompt: 'No root test' }; // No explicit root passed
|
||||
await generateTextService(params);
|
||||
|
||||
expect(mockGetMainProvider).toHaveBeenCalledWith(null);
|
||||
expect(mockGetParametersForRole).toHaveBeenCalledWith('main', null);
|
||||
expect(mockResolveEnvVariable).toHaveBeenCalledWith(
|
||||
'ANTHROPIC_API_KEY',
|
||||
undefined,
|
||||
null
|
||||
);
|
||||
expect(mockGenerateAnthropicText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Add more tests for edge cases:
|
||||
// - Missing API keys (should throw from _resolveApiKey)
|
||||
// - Unsupported provider configured (should skip and log)
|
||||
// - Missing provider/model config for a role (should skip and log)
|
||||
// - Missing prompt
|
||||
// - Different initial roles (research, fallback)
|
||||
// - generateObjectService (mock schema, check object result)
|
||||
// - streamTextService (more complex to test, might need stream helpers)
|
||||
});
|
||||
});
|
||||
373
tests/unit/ai-services.test.js
Normal file
373
tests/unit/ai-services.test.js
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* 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'"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -155,19 +155,19 @@ describe('Commands Module', () => {
|
||||
const program = setupCLI();
|
||||
const version = program._version();
|
||||
expect(mockReadFileSync).not.toHaveBeenCalled();
|
||||
expect(version).toBe('unknown');
|
||||
expect(version).toBe('1.5.0');
|
||||
});
|
||||
|
||||
test('should use default version when package.json reading throws an error', () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error('Read error');
|
||||
throw new Error('Invalid JSON');
|
||||
});
|
||||
|
||||
const program = setupCLI();
|
||||
const version = program._version();
|
||||
expect(mockReadFileSync).toHaveBeenCalled();
|
||||
expect(version).toBe('unknown');
|
||||
expect(version).toBe('1.5.0');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,670 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { jest } from '@jest/globals';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// --- Read REAL supported-models.json data BEFORE mocks ---
|
||||
const __filename = fileURLToPath(import.meta.url); // Get current file path
|
||||
const __dirname = path.dirname(__filename); // Get current directory
|
||||
const realSupportedModelsPath = path.resolve(
|
||||
__dirname,
|
||||
'../../scripts/modules/supported-models.json'
|
||||
);
|
||||
let REAL_SUPPORTED_MODELS_CONTENT;
|
||||
let REAL_SUPPORTED_MODELS_DATA;
|
||||
try {
|
||||
REAL_SUPPORTED_MODELS_CONTENT = fs.readFileSync(
|
||||
realSupportedModelsPath,
|
||||
'utf-8'
|
||||
);
|
||||
REAL_SUPPORTED_MODELS_DATA = JSON.parse(REAL_SUPPORTED_MODELS_CONTENT);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'FATAL TEST SETUP ERROR: Could not read or parse real supported-models.json',
|
||||
err
|
||||
);
|
||||
REAL_SUPPORTED_MODELS_CONTENT = '{}'; // Default to empty object on error
|
||||
REAL_SUPPORTED_MODELS_DATA = {};
|
||||
process.exit(1); // Exit if essential test data can't be loaded
|
||||
}
|
||||
|
||||
// --- Define Mock Function Instances ---
|
||||
const mockFindProjectRoot = jest.fn();
|
||||
const mockLog = jest.fn();
|
||||
|
||||
// --- Mock Dependencies BEFORE importing the module under test ---
|
||||
|
||||
// Mock the entire 'fs' module
|
||||
jest.mock('fs');
|
||||
|
||||
// Mock the 'utils.js' module using a factory function
|
||||
jest.mock('../../scripts/modules/utils.js', () => ({
|
||||
__esModule: true, // Indicate it's an ES module mock
|
||||
findProjectRoot: mockFindProjectRoot, // Use the mock function instance
|
||||
log: mockLog, // Use the mock function instance
|
||||
// Include other necessary exports from utils if config-manager uses them directly
|
||||
resolveEnvVariable: jest.fn() // Example if needed
|
||||
}));
|
||||
|
||||
// DO NOT MOCK 'chalk'
|
||||
|
||||
// --- Import the module under test AFTER mocks are defined ---
|
||||
import * as configManager from '../../scripts/modules/config-manager.js';
|
||||
// Import the mocked 'fs' module to allow spying on its functions
|
||||
import fsMocked from 'fs';
|
||||
|
||||
// --- Test Data (Keep as is, ensure DEFAULT_CONFIG is accurate) ---
|
||||
const MOCK_PROJECT_ROOT = '/mock/project';
|
||||
const MOCK_CONFIG_PATH = path.join(MOCK_PROJECT_ROOT, '.taskmasterconfig');
|
||||
|
||||
// Updated DEFAULT_CONFIG reflecting the implementation
|
||||
const DEFAULT_CONFIG = {
|
||||
models: {
|
||||
main: {
|
||||
provider: 'anthropic',
|
||||
modelId: 'claude-3-7-sonnet-20250219',
|
||||
maxTokens: 64000,
|
||||
temperature: 0.2
|
||||
},
|
||||
research: {
|
||||
provider: 'perplexity',
|
||||
modelId: 'sonar-pro',
|
||||
maxTokens: 8700,
|
||||
temperature: 0.1
|
||||
},
|
||||
fallback: {
|
||||
provider: 'anthropic',
|
||||
modelId: 'claude-3-5-sonnet',
|
||||
maxTokens: 64000,
|
||||
temperature: 0.2
|
||||
}
|
||||
},
|
||||
global: {
|
||||
logLevel: 'info',
|
||||
debug: false,
|
||||
defaultSubtasks: 5,
|
||||
defaultPriority: 'medium',
|
||||
projectName: 'Task Master',
|
||||
ollamaBaseUrl: 'http://localhost:11434/api'
|
||||
}
|
||||
};
|
||||
|
||||
// Other test data (VALID_CUSTOM_CONFIG, PARTIAL_CONFIG, INVALID_PROVIDER_CONFIG)
|
||||
const VALID_CUSTOM_CONFIG = {
|
||||
models: {
|
||||
main: {
|
||||
provider: 'openai',
|
||||
modelId: 'gpt-4o',
|
||||
maxTokens: 4096,
|
||||
temperature: 0.5
|
||||
},
|
||||
research: {
|
||||
provider: 'google',
|
||||
modelId: 'gemini-1.5-pro-latest',
|
||||
maxTokens: 8192,
|
||||
temperature: 0.3
|
||||
},
|
||||
fallback: {
|
||||
provider: 'anthropic',
|
||||
modelId: 'claude-3-opus-20240229',
|
||||
maxTokens: 100000,
|
||||
temperature: 0.4
|
||||
}
|
||||
},
|
||||
global: {
|
||||
logLevel: 'debug',
|
||||
defaultPriority: 'high',
|
||||
projectName: 'My Custom Project'
|
||||
}
|
||||
};
|
||||
|
||||
const PARTIAL_CONFIG = {
|
||||
models: {
|
||||
main: { provider: 'openai', modelId: 'gpt-4-turbo' }
|
||||
},
|
||||
global: {
|
||||
projectName: 'Partial Project'
|
||||
}
|
||||
};
|
||||
|
||||
const INVALID_PROVIDER_CONFIG = {
|
||||
models: {
|
||||
main: { provider: 'invalid-provider', modelId: 'some-model' },
|
||||
research: {
|
||||
provider: 'perplexity',
|
||||
modelId: 'llama-3-sonar-large-32k-online'
|
||||
}
|
||||
},
|
||||
global: {
|
||||
logLevel: 'warn'
|
||||
}
|
||||
};
|
||||
|
||||
// Define spies globally to be restored in afterAll
|
||||
let consoleErrorSpy;
|
||||
let consoleWarnSpy;
|
||||
let fsReadFileSyncSpy;
|
||||
let fsWriteFileSyncSpy;
|
||||
let fsExistsSyncSpy;
|
||||
|
||||
beforeAll(() => {
|
||||
// Set up console spies
|
||||
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Restore all spies
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// Reset mocks before each test for isolation
|
||||
beforeEach(() => {
|
||||
// Clear all mock calls and reset implementations between tests
|
||||
jest.clearAllMocks();
|
||||
// Reset the external mock instances for utils
|
||||
mockFindProjectRoot.mockReset();
|
||||
mockLog.mockReset();
|
||||
|
||||
// --- Set up spies ON the imported 'fs' mock ---
|
||||
fsExistsSyncSpy = jest.spyOn(fsMocked, 'existsSync');
|
||||
fsReadFileSyncSpy = jest.spyOn(fsMocked, 'readFileSync');
|
||||
fsWriteFileSyncSpy = jest.spyOn(fsMocked, 'writeFileSync');
|
||||
|
||||
// --- Default Mock Implementations ---
|
||||
mockFindProjectRoot.mockReturnValue(MOCK_PROJECT_ROOT); // Default for utils.findProjectRoot
|
||||
fsExistsSyncSpy.mockReturnValue(true); // Assume files exist by default
|
||||
|
||||
// Default readFileSync: Return REAL models content, mocked config, or throw error
|
||||
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
||||
const baseName = path.basename(filePath);
|
||||
if (baseName === 'supported-models.json') {
|
||||
// Return the REAL file content stringified
|
||||
return REAL_SUPPORTED_MODELS_CONTENT;
|
||||
} else if (filePath === MOCK_CONFIG_PATH) {
|
||||
// Still mock the .taskmasterconfig reads
|
||||
return JSON.stringify(DEFAULT_CONFIG); // Default behavior
|
||||
}
|
||||
// Throw for unexpected reads - helps catch errors
|
||||
throw new Error(`Unexpected fs.readFileSync call in test: ${filePath}`);
|
||||
});
|
||||
|
||||
// Default writeFileSync: Do nothing, just allow calls
|
||||
fsWriteFileSyncSpy.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
// --- Validation Functions ---
|
||||
describe('Validation Functions', () => {
|
||||
// Tests for validateProvider and validateProviderModelCombination
|
||||
test('validateProvider should return true for valid providers', () => {
|
||||
expect(configManager.validateProvider('openai')).toBe(true);
|
||||
expect(configManager.validateProvider('anthropic')).toBe(true);
|
||||
expect(configManager.validateProvider('google')).toBe(true);
|
||||
expect(configManager.validateProvider('perplexity')).toBe(true);
|
||||
expect(configManager.validateProvider('ollama')).toBe(true);
|
||||
expect(configManager.validateProvider('openrouter')).toBe(true);
|
||||
});
|
||||
|
||||
test('validateProvider should return false for invalid providers', () => {
|
||||
expect(configManager.validateProvider('invalid-provider')).toBe(false);
|
||||
expect(configManager.validateProvider('grok')).toBe(false); // Not in mock map
|
||||
expect(configManager.validateProvider('')).toBe(false);
|
||||
expect(configManager.validateProvider(null)).toBe(false);
|
||||
});
|
||||
|
||||
test('validateProviderModelCombination should validate known good combinations', () => {
|
||||
// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
|
||||
configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
||||
expect(
|
||||
configManager.validateProviderModelCombination('openai', 'gpt-4o')
|
||||
).toBe(true);
|
||||
expect(
|
||||
configManager.validateProviderModelCombination(
|
||||
'anthropic',
|
||||
'claude-3-5-sonnet-20241022'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('validateProviderModelCombination should return false for known bad combinations', () => {
|
||||
// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
|
||||
configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
||||
expect(
|
||||
configManager.validateProviderModelCombination(
|
||||
'openai',
|
||||
'claude-3-opus-20240229'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('validateProviderModelCombination should return true for ollama/openrouter (empty lists in map)', () => {
|
||||
// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
|
||||
configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
||||
expect(
|
||||
configManager.validateProviderModelCombination('ollama', 'any-model')
|
||||
).toBe(false);
|
||||
expect(
|
||||
configManager.validateProviderModelCombination('openrouter', 'any/model')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('validateProviderModelCombination should return true for providers not in map', () => {
|
||||
// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
|
||||
configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
||||
// The implementation returns true if the provider isn't in the map
|
||||
expect(
|
||||
configManager.validateProviderModelCombination(
|
||||
'unknown-provider',
|
||||
'some-model'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// --- getConfig Tests ---
|
||||
describe('getConfig Tests', () => {
|
||||
test('should return default config if .taskmasterconfig does not exist', () => {
|
||||
// Arrange
|
||||
fsExistsSyncSpy.mockReturnValue(false);
|
||||
// findProjectRoot mock is set in beforeEach
|
||||
|
||||
// Act: Call getConfig with explicit root
|
||||
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Force reload
|
||||
|
||||
// Assert
|
||||
expect(config).toEqual(DEFAULT_CONFIG);
|
||||
expect(mockFindProjectRoot).not.toHaveBeenCalled(); // Explicit root provided
|
||||
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
||||
expect(fsReadFileSyncSpy).not.toHaveBeenCalled(); // No read if file doesn't exist
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('not found at provided project root')
|
||||
);
|
||||
});
|
||||
|
||||
test.skip('should use findProjectRoot and return defaults if file not found', () => {
|
||||
// TODO: Fix mock interaction, findProjectRoot isn't being registered as called
|
||||
// Arrange
|
||||
fsExistsSyncSpy.mockReturnValue(false);
|
||||
// findProjectRoot mock is set in beforeEach
|
||||
|
||||
// Act: Call getConfig without explicit root
|
||||
const config = configManager.getConfig(null, true); // Force reload
|
||||
|
||||
// Assert
|
||||
expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now
|
||||
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
||||
expect(config).toEqual(DEFAULT_CONFIG);
|
||||
expect(fsReadFileSyncSpy).not.toHaveBeenCalled();
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('not found at derived root')
|
||||
); // Adjusted expected warning
|
||||
});
|
||||
|
||||
test('should read and merge valid config file with defaults', () => {
|
||||
// Arrange: Override readFileSync for this test
|
||||
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
||||
if (filePath === MOCK_CONFIG_PATH)
|
||||
return JSON.stringify(VALID_CUSTOM_CONFIG);
|
||||
if (path.basename(filePath) === 'supported-models.json') {
|
||||
// Provide necessary models for validation within getConfig
|
||||
return JSON.stringify({
|
||||
openai: [{ id: 'gpt-4o' }],
|
||||
google: [{ id: 'gemini-1.5-pro-latest' }],
|
||||
perplexity: [{ id: 'sonar-pro' }],
|
||||
anthropic: [
|
||||
{ id: 'claude-3-opus-20240229' },
|
||||
{ id: 'claude-3-5-sonnet' },
|
||||
{ id: 'claude-3-7-sonnet-20250219' },
|
||||
{ id: 'claude-3-5-sonnet' }
|
||||
],
|
||||
ollama: [],
|
||||
openrouter: []
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
||||
});
|
||||
fsExistsSyncSpy.mockReturnValue(true);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
|
||||
// Act
|
||||
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Force reload
|
||||
|
||||
// Assert: Construct expected merged config
|
||||
const expectedMergedConfig = {
|
||||
models: {
|
||||
main: {
|
||||
...DEFAULT_CONFIG.models.main,
|
||||
...VALID_CUSTOM_CONFIG.models.main
|
||||
},
|
||||
research: {
|
||||
...DEFAULT_CONFIG.models.research,
|
||||
...VALID_CUSTOM_CONFIG.models.research
|
||||
},
|
||||
fallback: {
|
||||
...DEFAULT_CONFIG.models.fallback,
|
||||
...VALID_CUSTOM_CONFIG.models.fallback
|
||||
}
|
||||
},
|
||||
global: { ...DEFAULT_CONFIG.global, ...VALID_CUSTOM_CONFIG.global }
|
||||
};
|
||||
expect(config).toEqual(expectedMergedConfig);
|
||||
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
||||
expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
|
||||
});
|
||||
|
||||
test('should merge defaults for partial config file', () => {
|
||||
// Arrange
|
||||
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
||||
if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(PARTIAL_CONFIG);
|
||||
if (path.basename(filePath) === 'supported-models.json') {
|
||||
return JSON.stringify({
|
||||
openai: [{ id: 'gpt-4-turbo' }],
|
||||
perplexity: [{ id: 'sonar-pro' }],
|
||||
anthropic: [
|
||||
{ id: 'claude-3-7-sonnet-20250219' },
|
||||
{ id: 'claude-3-5-sonnet' }
|
||||
],
|
||||
ollama: [],
|
||||
openrouter: []
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
||||
});
|
||||
fsExistsSyncSpy.mockReturnValue(true);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
|
||||
// Act
|
||||
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
||||
|
||||
// Assert: Construct expected merged config
|
||||
const expectedMergedConfig = {
|
||||
models: {
|
||||
main: { ...DEFAULT_CONFIG.models.main, ...PARTIAL_CONFIG.models.main },
|
||||
research: { ...DEFAULT_CONFIG.models.research },
|
||||
fallback: { ...DEFAULT_CONFIG.models.fallback }
|
||||
},
|
||||
global: { ...DEFAULT_CONFIG.global, ...PARTIAL_CONFIG.global }
|
||||
};
|
||||
expect(config).toEqual(expectedMergedConfig);
|
||||
expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
|
||||
});
|
||||
|
||||
test('should handle JSON parsing error and return defaults', () => {
|
||||
// Arrange
|
||||
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
||||
if (filePath === MOCK_CONFIG_PATH) return 'invalid json';
|
||||
// Mock models read needed for initial load before parse error
|
||||
if (path.basename(filePath) === 'supported-models.json') {
|
||||
return JSON.stringify({
|
||||
anthropic: [{ id: 'claude-3-7-sonnet-20250219' }],
|
||||
perplexity: [{ id: 'sonar-pro' }],
|
||||
fallback: [{ id: 'claude-3-5-sonnet' }],
|
||||
ollama: [],
|
||||
openrouter: []
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
||||
});
|
||||
fsExistsSyncSpy.mockReturnValue(true);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
|
||||
// Act
|
||||
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
||||
|
||||
// Assert
|
||||
expect(config).toEqual(DEFAULT_CONFIG);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Error reading or parsing')
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle file read error and return defaults', () => {
|
||||
// Arrange
|
||||
const readError = new Error('Permission denied');
|
||||
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
||||
if (filePath === MOCK_CONFIG_PATH) throw readError;
|
||||
// Mock models read needed for initial load before read error
|
||||
if (path.basename(filePath) === 'supported-models.json') {
|
||||
return JSON.stringify({
|
||||
anthropic: [{ id: 'claude-3-7-sonnet-20250219' }],
|
||||
perplexity: [{ id: 'sonar-pro' }],
|
||||
fallback: [{ id: 'claude-3-5-sonnet' }],
|
||||
ollama: [],
|
||||
openrouter: []
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
||||
});
|
||||
fsExistsSyncSpy.mockReturnValue(true);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
|
||||
// Act
|
||||
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
||||
|
||||
// Assert
|
||||
expect(config).toEqual(DEFAULT_CONFIG);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`Permission denied. Using default configuration.`)
|
||||
);
|
||||
});
|
||||
|
||||
test('should validate provider and fallback to default if invalid', () => {
|
||||
// Arrange
|
||||
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
||||
if (filePath === MOCK_CONFIG_PATH)
|
||||
return JSON.stringify(INVALID_PROVIDER_CONFIG);
|
||||
if (path.basename(filePath) === 'supported-models.json') {
|
||||
return JSON.stringify({
|
||||
perplexity: [{ id: 'llama-3-sonar-large-32k-online' }],
|
||||
anthropic: [
|
||||
{ id: 'claude-3-7-sonnet-20250219' },
|
||||
{ id: 'claude-3-5-sonnet' }
|
||||
],
|
||||
ollama: [],
|
||||
openrouter: []
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
||||
});
|
||||
fsExistsSyncSpy.mockReturnValue(true);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
|
||||
// Act
|
||||
const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
|
||||
|
||||
// Assert
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Warning: Invalid main provider "invalid-provider"'
|
||||
)
|
||||
);
|
||||
const expectedMergedConfig = {
|
||||
models: {
|
||||
main: { ...DEFAULT_CONFIG.models.main },
|
||||
research: {
|
||||
...DEFAULT_CONFIG.models.research,
|
||||
...INVALID_PROVIDER_CONFIG.models.research
|
||||
},
|
||||
fallback: { ...DEFAULT_CONFIG.models.fallback }
|
||||
},
|
||||
global: { ...DEFAULT_CONFIG.global, ...INVALID_PROVIDER_CONFIG.global }
|
||||
};
|
||||
expect(config).toEqual(expectedMergedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
// --- writeConfig Tests ---
|
||||
describe('writeConfig', () => {
|
||||
test('should write valid config to file', () => {
|
||||
// Arrange (Default mocks are sufficient)
|
||||
// findProjectRoot mock set in beforeEach
|
||||
fsWriteFileSyncSpy.mockImplementation(() => {}); // Ensure it doesn't throw
|
||||
|
||||
// Act
|
||||
const success = configManager.writeConfig(
|
||||
VALID_CUSTOM_CONFIG,
|
||||
MOCK_PROJECT_ROOT
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(success).toBe(true);
|
||||
expect(fsWriteFileSyncSpy).toHaveBeenCalledWith(
|
||||
MOCK_CONFIG_PATH,
|
||||
JSON.stringify(VALID_CUSTOM_CONFIG, null, 2) // writeConfig stringifies
|
||||
);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should return false and log error if write fails', () => {
|
||||
// Arrange
|
||||
const mockWriteError = new Error('Disk full');
|
||||
fsWriteFileSyncSpy.mockImplementation(() => {
|
||||
throw mockWriteError;
|
||||
});
|
||||
// findProjectRoot mock set in beforeEach
|
||||
|
||||
// Act
|
||||
const success = configManager.writeConfig(
|
||||
VALID_CUSTOM_CONFIG,
|
||||
MOCK_PROJECT_ROOT
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(success).toBe(false);
|
||||
expect(fsWriteFileSyncSpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`Disk full`)
|
||||
);
|
||||
});
|
||||
|
||||
test.skip('should return false if project root cannot be determined', () => {
|
||||
// TODO: Fix mock interaction or function logic, returns true unexpectedly in test
|
||||
// Arrange: Override mock for this specific test
|
||||
mockFindProjectRoot.mockReturnValue(null);
|
||||
|
||||
// Act: Call without explicit root
|
||||
const success = configManager.writeConfig(VALID_CUSTOM_CONFIG);
|
||||
|
||||
// Assert
|
||||
expect(success).toBe(false); // Function should return false if root is null
|
||||
expect(mockFindProjectRoot).toHaveBeenCalled();
|
||||
expect(fsWriteFileSyncSpy).not.toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Could not determine project root')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Getter Functions ---
|
||||
describe('Getter Functions', () => {
|
||||
test('getMainProvider should return provider from config', () => {
|
||||
// Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG
|
||||
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
||||
if (filePath === MOCK_CONFIG_PATH)
|
||||
return JSON.stringify(VALID_CUSTOM_CONFIG);
|
||||
if (path.basename(filePath) === 'supported-models.json') {
|
||||
return JSON.stringify({
|
||||
openai: [{ id: 'gpt-4o' }],
|
||||
google: [{ id: 'gemini-1.5-pro-latest' }],
|
||||
anthropic: [
|
||||
{ id: 'claude-3-opus-20240229' },
|
||||
{ id: 'claude-3-7-sonnet-20250219' },
|
||||
{ id: 'claude-3-5-sonnet' }
|
||||
],
|
||||
perplexity: [{ id: 'sonar-pro' }],
|
||||
ollama: [],
|
||||
openrouter: []
|
||||
}); // Added perplexity
|
||||
}
|
||||
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
||||
});
|
||||
fsExistsSyncSpy.mockReturnValue(true);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
|
||||
// Act
|
||||
const provider = configManager.getMainProvider(MOCK_PROJECT_ROOT);
|
||||
|
||||
// Assert
|
||||
expect(provider).toBe(VALID_CUSTOM_CONFIG.models.main.provider);
|
||||
});
|
||||
|
||||
test('getLogLevel should return logLevel from config', () => {
|
||||
// Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG
|
||||
fsReadFileSyncSpy.mockImplementation((filePath) => {
|
||||
if (filePath === MOCK_CONFIG_PATH)
|
||||
return JSON.stringify(VALID_CUSTOM_CONFIG);
|
||||
if (path.basename(filePath) === 'supported-models.json') {
|
||||
// Provide enough mock model data for validation within getConfig
|
||||
return JSON.stringify({
|
||||
openai: [{ id: 'gpt-4o' }],
|
||||
google: [{ id: 'gemini-1.5-pro-latest' }],
|
||||
anthropic: [
|
||||
{ id: 'claude-3-opus-20240229' },
|
||||
{ id: 'claude-3-7-sonnet-20250219' },
|
||||
{ id: 'claude-3-5-sonnet' }
|
||||
],
|
||||
perplexity: [{ id: 'sonar-pro' }],
|
||||
ollama: [],
|
||||
openrouter: []
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
|
||||
});
|
||||
fsExistsSyncSpy.mockReturnValue(true);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
|
||||
// Act
|
||||
const logLevel = configManager.getLogLevel(MOCK_PROJECT_ROOT);
|
||||
|
||||
// Assert
|
||||
expect(logLevel).toBe(VALID_CUSTOM_CONFIG.global.logLevel);
|
||||
});
|
||||
|
||||
// Add more tests for other getters (getResearchProvider, getProjectName, etc.)
|
||||
});
|
||||
|
||||
// --- isConfigFilePresent Tests ---
|
||||
describe('isConfigFilePresent', () => {
|
||||
test('should return true if config file exists', () => {
|
||||
fsExistsSyncSpy.mockReturnValue(true);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(true);
|
||||
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
||||
});
|
||||
|
||||
test('should return false if config file does not exist', () => {
|
||||
fsExistsSyncSpy.mockReturnValue(false);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(false);
|
||||
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
||||
});
|
||||
|
||||
test.skip('should use findProjectRoot if explicitRoot is not provided', () => {
|
||||
// TODO: Fix mock interaction, findProjectRoot isn't being registered as called
|
||||
fsExistsSyncSpy.mockReturnValue(true);
|
||||
// findProjectRoot mock set in beforeEach
|
||||
expect(configManager.isConfigFilePresent()).toBe(true);
|
||||
expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now
|
||||
});
|
||||
});
|
||||
|
||||
// --- getAllProviders Tests ---
|
||||
describe('getAllProviders', () => {
|
||||
test('should return list of providers from supported-models.json', () => {
|
||||
// Arrange: Ensure config is loaded with real data
|
||||
configManager.getConfig(null, true); // Force load using the mock that returns real data
|
||||
|
||||
// Act
|
||||
const providers = configManager.getAllProviders();
|
||||
// Assert
|
||||
// Assert against the actual keys in the REAL loaded data
|
||||
const expectedProviders = Object.keys(REAL_SUPPORTED_MODELS_DATA);
|
||||
expect(providers).toEqual(expect.arrayContaining(expectedProviders));
|
||||
expect(providers.length).toBe(expectedProviders.length);
|
||||
});
|
||||
});
|
||||
|
||||
// Add tests for getParametersForRole if needed
|
||||
|
||||
// Note: Tests for setMainModel, setResearchModel were removed as the functions were removed in the implementation.
|
||||
// If similar setter functions exist, add tests for them following the writeConfig pattern.
|
||||
@@ -1,182 +0,0 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
// Mock external modules
|
||||
jest.mock('child_process', () => ({
|
||||
execSync: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock console methods
|
||||
jest.mock('console', () => ({
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
clear: jest.fn()
|
||||
}));
|
||||
|
||||
describe('Roo Integration', () => {
|
||||
let tempDir;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
|
||||
|
||||
// Spy on fs methods
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('.roomodes')) {
|
||||
return 'Existing roomodes content';
|
||||
}
|
||||
if (filePath.toString().includes('-rules')) {
|
||||
return 'Existing mode rules content';
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
|
||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test function that simulates the createProjectStructure behavior for Roo files
|
||||
function mockCreateRooStructure() {
|
||||
// Create main .roo directory
|
||||
fs.mkdirSync(path.join(tempDir, '.roo'), { recursive: true });
|
||||
|
||||
// Create rules directory
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', 'rules'), { recursive: true });
|
||||
|
||||
// Create mode-specific rule directories
|
||||
const rooModes = ['architect', 'ask', 'boomerang', 'code', 'debug', 'test'];
|
||||
for (const mode of rooModes) {
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', `rules-${mode}`), {
|
||||
recursive: true
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.roo', `rules-${mode}`, `${mode}-rules`),
|
||||
`Content for ${mode} rules`
|
||||
);
|
||||
}
|
||||
|
||||
// Create additional directories
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', 'config'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', 'templates'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', 'logs'), { recursive: true });
|
||||
|
||||
// Copy .roomodes file
|
||||
fs.writeFileSync(path.join(tempDir, '.roomodes'), 'Roomodes file content');
|
||||
}
|
||||
|
||||
test('creates all required .roo directories', () => {
|
||||
// Act
|
||||
mockCreateRooStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.roo'), {
|
||||
recursive: true
|
||||
});
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules'),
|
||||
{ recursive: true }
|
||||
);
|
||||
|
||||
// Verify all mode directories are created
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-architect'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-ask'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-boomerang'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-code'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-debug'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-test'),
|
||||
{ recursive: true }
|
||||
);
|
||||
});
|
||||
|
||||
test('creates rule files for all modes', () => {
|
||||
// Act
|
||||
mockCreateRooStructure();
|
||||
|
||||
// Assert - check all rule files are created
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-architect', 'architect-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-ask', 'ask-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-boomerang', 'boomerang-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-code', 'code-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-debug', 'debug-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-test', 'test-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
test('creates .roomodes file in project root', () => {
|
||||
// Act
|
||||
mockCreateRooStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roomodes'),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
test('creates additional required Roo directories', () => {
|
||||
// Act
|
||||
mockCreateRooStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'config'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'templates'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'logs'),
|
||||
{ recursive: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { convertCursorRuleToRooRule } from '../../scripts/modules/rule-transformer.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('Rule Transformer', () => {
|
||||
const testDir = path.join(__dirname, 'temp-test-dir');
|
||||
|
||||
beforeAll(() => {
|
||||
// Create test directory
|
||||
if (!fs.existsSync(testDir)) {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up test directory
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should correctly convert basic terms', () => {
|
||||
// Create a test Cursor rule file with basic terms
|
||||
const testCursorRule = path.join(testDir, 'basic-terms.mdc');
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for basic terms
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
|
||||
Also has references to .mdc files.`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testRooRule = path.join(testDir, 'basic-terms.md');
|
||||
convertCursorRuleToRooRule(testCursorRule, testRooRule);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('Roo Code');
|
||||
expect(convertedContent).toContain('roocode.com');
|
||||
expect(convertedContent).toContain('.md');
|
||||
expect(convertedContent).not.toContain('cursor.so');
|
||||
expect(convertedContent).not.toContain('Cursor rule');
|
||||
});
|
||||
|
||||
it('should correctly convert tool references', () => {
|
||||
// Create a test Cursor rule file with tool references
|
||||
const testCursorRule = path.join(testDir, 'tool-refs.mdc');
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for tool references
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
- Use the search tool to find code
|
||||
- The edit_file tool lets you modify files
|
||||
- run_command executes terminal commands
|
||||
- use_mcp connects to external services`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testRooRule = path.join(testDir, 'tool-refs.md');
|
||||
convertCursorRuleToRooRule(testCursorRule, testRooRule);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('search_files tool');
|
||||
expect(convertedContent).toContain('apply_diff tool');
|
||||
expect(convertedContent).toContain('execute_command');
|
||||
expect(convertedContent).toContain('use_mcp_tool');
|
||||
});
|
||||
|
||||
it('should correctly update file references', () => {
|
||||
// Create a test Cursor rule file with file references
|
||||
const testCursorRule = path.join(testDir, 'file-refs.mdc');
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for file references
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testRooRule = path.join(testDir, 'file-refs.md');
|
||||
convertCursorRuleToRooRule(testCursorRule, testRooRule);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('(mdc:.roo/rules/dev_workflow.md)');
|
||||
expect(convertedContent).toContain('(mdc:.roo/rules/taskmaster.md)');
|
||||
expect(convertedContent).not.toContain('(mdc:.cursor/rules/');
|
||||
});
|
||||
});
|
||||
@@ -8,52 +8,43 @@ import { sampleTasks, emptySampleTasks } from '../fixtures/sample-tasks.js';
|
||||
describe('Task Finder', () => {
|
||||
describe('findTaskById function', () => {
|
||||
test('should find a task by numeric ID', () => {
|
||||
const result = findTaskById(sampleTasks.tasks, 2);
|
||||
expect(result.task).toBeDefined();
|
||||
expect(result.task.id).toBe(2);
|
||||
expect(result.task.title).toBe('Create Core Functionality');
|
||||
expect(result.originalSubtaskCount).toBeNull();
|
||||
const task = findTaskById(sampleTasks.tasks, 2);
|
||||
expect(task).toBeDefined();
|
||||
expect(task.id).toBe(2);
|
||||
expect(task.title).toBe('Create Core Functionality');
|
||||
});
|
||||
|
||||
test('should find a task by string ID', () => {
|
||||
const result = findTaskById(sampleTasks.tasks, '2');
|
||||
expect(result.task).toBeDefined();
|
||||
expect(result.task.id).toBe(2);
|
||||
expect(result.originalSubtaskCount).toBeNull();
|
||||
const task = findTaskById(sampleTasks.tasks, '2');
|
||||
expect(task).toBeDefined();
|
||||
expect(task.id).toBe(2);
|
||||
});
|
||||
|
||||
test('should find a subtask using dot notation', () => {
|
||||
const result = findTaskById(sampleTasks.tasks, '3.1');
|
||||
expect(result.task).toBeDefined();
|
||||
expect(result.task.id).toBe(1);
|
||||
expect(result.task.title).toBe('Create Header Component');
|
||||
expect(result.task.isSubtask).toBe(true);
|
||||
expect(result.task.parentTask.id).toBe(3);
|
||||
expect(result.originalSubtaskCount).toBeNull();
|
||||
const subtask = findTaskById(sampleTasks.tasks, '3.1');
|
||||
expect(subtask).toBeDefined();
|
||||
expect(subtask.id).toBe(1);
|
||||
expect(subtask.title).toBe('Create Header Component');
|
||||
});
|
||||
|
||||
test('should return null for non-existent task ID', () => {
|
||||
const result = findTaskById(sampleTasks.tasks, 99);
|
||||
expect(result.task).toBeNull();
|
||||
expect(result.originalSubtaskCount).toBeNull();
|
||||
const task = findTaskById(sampleTasks.tasks, 99);
|
||||
expect(task).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null for non-existent subtask ID', () => {
|
||||
const result = findTaskById(sampleTasks.tasks, '3.99');
|
||||
expect(result.task).toBeNull();
|
||||
expect(result.originalSubtaskCount).toBeNull();
|
||||
const subtask = findTaskById(sampleTasks.tasks, '3.99');
|
||||
expect(subtask).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null for non-existent parent task ID in subtask notation', () => {
|
||||
const result = findTaskById(sampleTasks.tasks, '99.1');
|
||||
expect(result.task).toBeNull();
|
||||
expect(result.originalSubtaskCount).toBeNull();
|
||||
const subtask = findTaskById(sampleTasks.tasks, '99.1');
|
||||
expect(subtask).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null when tasks array is empty', () => {
|
||||
const result = findTaskById(emptySampleTasks.tasks, 1);
|
||||
expect(result.task).toBeNull();
|
||||
expect(result.originalSubtaskCount).toBeNull();
|
||||
const task = findTaskById(emptySampleTasks.tasks, 1);
|
||||
expect(task).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
|
||||
// Import the actual module to test
|
||||
import {
|
||||
@@ -18,14 +19,21 @@ import {
|
||||
taskExists,
|
||||
formatTaskId,
|
||||
findCycles,
|
||||
CONFIG,
|
||||
LOG_LEVELS,
|
||||
findTaskById,
|
||||
toKebabCase
|
||||
} from '../../scripts/modules/utils.js';
|
||||
|
||||
// Mock config-manager to provide config values
|
||||
const mockGetLogLevel = jest.fn(() => 'info'); // Default log level for tests
|
||||
jest.mock('../../scripts/modules/config-manager.js', () => ({
|
||||
getLogLevel: mockGetLogLevel
|
||||
// Mock other getters if needed by utils.js functions under test
|
||||
// Skip the import of detectCamelCaseFlags as we'll implement our own version for testing
|
||||
|
||||
// Mock chalk functions
|
||||
jest.mock('chalk', () => ({
|
||||
gray: jest.fn((text) => `gray:${text}`),
|
||||
blue: jest.fn((text) => `blue:${text}`),
|
||||
yellow: jest.fn((text) => `yellow:${text}`),
|
||||
red: jest.fn((text) => `red:${text}`),
|
||||
green: jest.fn((text) => `green:${text}`)
|
||||
}));
|
||||
|
||||
// Test implementation of detectCamelCaseFlags
|
||||
@@ -121,27 +129,23 @@ describe('Utils Module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('log function', () => {
|
||||
// const originalConsoleLog = console.log; // Keep original for potential restore if needed
|
||||
describe('log function', () => {
|
||||
// Save original console.log
|
||||
const originalConsoleLog = console.log;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock console.log for each test
|
||||
// console.log = jest.fn(); // REMOVE console.log spy
|
||||
mockGetLogLevel.mockClear(); // Clear mock calls
|
||||
console.log = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original console.log after each test
|
||||
// console.log = originalConsoleLog; // REMOVE console.log restore
|
||||
console.log = originalConsoleLog;
|
||||
});
|
||||
|
||||
test('should log messages according to log level from config-manager', () => {
|
||||
// Test with info level (default from mock)
|
||||
mockGetLogLevel.mockReturnValue('info');
|
||||
|
||||
// Spy on console.log JUST for this test to verify calls
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
test('should log messages according to log level', () => {
|
||||
// Test with info level (1)
|
||||
CONFIG.logLevel = 'info';
|
||||
|
||||
log('debug', 'Debug message');
|
||||
log('info', 'Info message');
|
||||
@@ -149,47 +153,36 @@ describe('Utils Module', () => {
|
||||
log('error', 'Error message');
|
||||
|
||||
// Debug should not be logged (level 0 < 1)
|
||||
expect(consoleSpy).not.toHaveBeenCalledWith(
|
||||
expect(console.log).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Debug message')
|
||||
);
|
||||
|
||||
// Info and above should be logged
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Info message')
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Warning message')
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Error message')
|
||||
);
|
||||
|
||||
// Verify the formatting includes text prefixes
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[INFO]')
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[WARN]')
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[ERROR]')
|
||||
);
|
||||
|
||||
// Verify getLogLevel was called by log function
|
||||
expect(mockGetLogLevel).toHaveBeenCalled();
|
||||
|
||||
// Restore spy for this test
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should not log messages below the configured log level', () => {
|
||||
// Set log level to error via mock
|
||||
mockGetLogLevel.mockReturnValue('error');
|
||||
|
||||
// Spy on console.log JUST for this test
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
// Set log level to error (3)
|
||||
CONFIG.logLevel = 'error';
|
||||
|
||||
log('debug', 'Debug message');
|
||||
log('info', 'Info message');
|
||||
@@ -197,44 +190,30 @@ describe('Utils Module', () => {
|
||||
log('error', 'Error message');
|
||||
|
||||
// Only error should be logged
|
||||
expect(consoleSpy).not.toHaveBeenCalledWith(
|
||||
expect(console.log).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Debug message')
|
||||
);
|
||||
expect(consoleSpy).not.toHaveBeenCalledWith(
|
||||
expect(console.log).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Info message')
|
||||
);
|
||||
expect(consoleSpy).not.toHaveBeenCalledWith(
|
||||
expect(console.log).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Warning message')
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Error message')
|
||||
);
|
||||
|
||||
// Verify getLogLevel was called
|
||||
expect(mockGetLogLevel).toHaveBeenCalled();
|
||||
|
||||
// Restore spy for this test
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should join multiple arguments into a single message', () => {
|
||||
mockGetLogLevel.mockReturnValue('info');
|
||||
// Spy on console.log JUST for this test
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
CONFIG.logLevel = 'info';
|
||||
log('info', 'Message', 'with', 'multiple', 'parts');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Message with multiple parts')
|
||||
);
|
||||
|
||||
// Restore spy for this test
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('readJSON function', () => {
|
||||
describe('readJSON function', () => {
|
||||
test('should read and parse a valid JSON file', () => {
|
||||
const testData = { key: 'value', nested: { prop: true } };
|
||||
fsReadFileSyncSpy.mockReturnValue(JSON.stringify(testData));
|
||||
@@ -280,7 +259,7 @@ describe('Utils Module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('writeJSON function', () => {
|
||||
describe('writeJSON function', () => {
|
||||
test('should write JSON data to a file', () => {
|
||||
const testData = { key: 'value', nested: { prop: true } };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user