This commit is contained in:
Ralph Khreish
2025-07-11 13:29:52 +03:00
parent 74232d0e0d
commit 14cc09d241
27 changed files with 5699 additions and 958 deletions

View File

@@ -0,0 +1,424 @@
---
description: Guide for using Taskmaster to manage task-driven development workflows
globs: **/*
alwaysApply: true
---
# Taskmaster Development Workflow
This guide outlines the standard process for using Taskmaster to manage software development projects. It is written as a set of instructions for you, the AI agent.
- **Your Default Stance**: For most projects, the user can work directly within the `master` task context. Your initial actions should operate on this default context unless a clear pattern for multi-context work emerges.
- **Your Goal**: Your role is to elevate the user's workflow by intelligently introducing advanced features like **Tagged Task Lists** when you detect the appropriate context. Do not force tags on the user; suggest them as a helpful solution to a specific need.
## The Basic Loop
The fundamental development cycle you will facilitate is:
1. **`list`**: Show the user what needs to be done.
2. **`next`**: Help the user decide what to work on.
3. **`show <id>`**: Provide details for a specific task.
4. **`expand <id>`**: Break down a complex task into smaller, manageable subtasks.
5. **Implement**: The user writes the code and tests.
6. **`update-subtask`**: Log progress and findings on behalf of the user.
7. **`set-status`**: Mark tasks and subtasks as `done` as work is completed.
8. **Repeat**.
All your standard command executions should operate on the user's current task context, which defaults to `master`.
---
## Standard Development Workflow Process
### Simple Workflow (Default Starting Point)
For new projects or when users are getting started, operate within the `master` tag context:
- Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input='<prd-file.txt>'` (see @`taskmaster.mdc`) to generate initial tasks.json with tagged structure
- Configure rule sets during initialization with `--rules` flag (e.g., `task-master init --rules cursor,windsurf`) or manage them later with `task-master rules add/remove` commands
- Begin coding sessions with `get_tasks` / `task-master list` (see @`taskmaster.mdc`) to see current tasks, status, and IDs
- Determine the next task to work on using `next_task` / `task-master next` (see @`taskmaster.mdc`)
- Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.mdc`) before breaking down tasks
- Review complexity report using `complexity_report` / `task-master complexity-report` (see @`taskmaster.mdc`)
- Select tasks based on dependencies (all marked 'done'), priority level, and ID order
- View specific task details using `get_task` / `task-master show <id>` (see @`taskmaster.mdc`) to understand implementation requirements
- Break down complex tasks using `expand_task` / `task-master expand --id=<id> --force --research` (see @`taskmaster.mdc`) with appropriate flags like `--force` (to replace existing subtasks) and `--research`
- Implement code following task details, dependencies, and project standards
- Mark completed tasks with `set_task_status` / `task-master set-status --id=<id> --status=done` (see @`taskmaster.mdc`)
- Update dependent tasks when implementation differs from original plan using `update` / `task-master update --from=<id> --prompt="..."` or `update_task` / `task-master update-task --id=<id> --prompt="..."` (see @`taskmaster.mdc`)
---
## Leveling Up: Agent-Led Multi-Context Workflows
While the basic workflow is powerful, your primary opportunity to add value is by identifying when to introduce **Tagged Task Lists**. These patterns are your tools for creating a more organized and efficient development environment for the user, especially if you detect agentic or parallel development happening across the same session.
**Critical Principle**: Most users should never see a difference in their experience. Only introduce advanced workflows when you detect clear indicators that the project has evolved beyond simple task management.
### When to Introduce Tags: Your Decision Patterns
Here are the patterns to look for. When you detect one, you should propose the corresponding workflow to the user.
#### Pattern 1: Simple Git Feature Branching
This is the most common and direct use case for tags.
- **Trigger**: The user creates a new git branch (e.g., `git checkout -b feature/user-auth`).
- **Your Action**: Propose creating a new tag that mirrors the branch name to isolate the feature's tasks from `master`.
- **Your Suggested Prompt**: *"I see you've created a new branch named 'feature/user-auth'. To keep all related tasks neatly organized and separate from your main list, I can create a corresponding task tag for you. This helps prevent merge conflicts in your `tasks.json` file later. Shall I create the 'feature-user-auth' tag?"*
- **Tool to Use**: `task-master add-tag --from-branch`
#### Pattern 2: Team Collaboration
- **Trigger**: The user mentions working with teammates (e.g., "My teammate Alice is handling the database schema," or "I need to review Bob's work on the API.").
- **Your Action**: Suggest creating a separate tag for the user's work to prevent conflicts with shared master context.
- **Your Suggested Prompt**: *"Since you're working with Alice, I can create a separate task context for your work to avoid conflicts. This way, Alice can continue working with the master list while you have your own isolated context. When you're ready to merge your work, we can coordinate the tasks back to master. Shall I create a tag for your current work?"*
- **Tool to Use**: `task-master add-tag my-work --copy-from-current --description="My tasks while collaborating with Alice"`
#### Pattern 3: Experiments or Risky Refactors
- **Trigger**: The user wants to try something that might not be kept (e.g., "I want to experiment with switching our state management library," or "Let's refactor the old API module, but I want to keep the current tasks as a reference.").
- **Your Action**: Propose creating a sandboxed tag for the experimental work.
- **Your Suggested Prompt**: *"This sounds like a great experiment. To keep these new tasks separate from our main plan, I can create a temporary 'experiment-zustand' tag for this work. If we decide not to proceed, we can simply delete the tag without affecting the main task list. Sound good?"*
- **Tool to Use**: `task-master add-tag experiment-zustand --description="Exploring Zustand migration"`
#### Pattern 4: Large Feature Initiatives (PRD-Driven)
This is a more structured approach for significant new features or epics.
- **Trigger**: The user describes a large, multi-step feature that would benefit from a formal plan.
- **Your Action**: Propose a comprehensive, PRD-driven workflow.
- **Your Suggested Prompt**: *"This sounds like a significant new feature. To manage this effectively, I suggest we create a dedicated task context for it. Here's the plan: I'll create a new tag called 'feature-xyz', then we can draft a Product Requirements Document (PRD) together to scope the work. Once the PRD is ready, I'll automatically generate all the necessary tasks within that new tag. How does that sound?"*
- **Your Implementation Flow**:
1. **Create an empty tag**: `task-master add-tag feature-xyz --description "Tasks for the new XYZ feature"`. You can also start by creating a git branch if applicable, and then create the tag from that branch.
2. **Collaborate & Create PRD**: Work with the user to create a detailed PRD file (e.g., `.taskmaster/docs/feature-xyz-prd.txt`).
3. **Parse PRD into the new tag**: `task-master parse-prd .taskmaster/docs/feature-xyz-prd.txt --tag feature-xyz`
4. **Prepare the new task list**: Follow up by suggesting `analyze-complexity` and `expand-all` for the newly created tasks within the `feature-xyz` tag.
#### Pattern 5: Version-Based Development
Tailor your approach based on the project maturity indicated by tag names.
- **Prototype/MVP Tags** (`prototype`, `mvp`, `poc`, `v0.x`):
- **Your Approach**: Focus on speed and functionality over perfection
- **Task Generation**: Create tasks that emphasize "get it working" over "get it perfect"
- **Complexity Level**: Lower complexity, fewer subtasks, more direct implementation paths
- **Research Prompts**: Include context like "This is a prototype - prioritize speed and basic functionality over optimization"
- **Example Prompt Addition**: *"Since this is for the MVP, I'll focus on tasks that get core functionality working quickly rather than over-engineering."*
- **Production/Mature Tags** (`v1.0+`, `production`, `stable`):
- **Your Approach**: Emphasize robustness, testing, and maintainability
- **Task Generation**: Include comprehensive error handling, testing, documentation, and optimization
- **Complexity Level**: Higher complexity, more detailed subtasks, thorough implementation paths
- **Research Prompts**: Include context like "This is for production - prioritize reliability, performance, and maintainability"
- **Example Prompt Addition**: *"Since this is for production, I'll ensure tasks include proper error handling, testing, and documentation."*
### Advanced Workflow (Tag-Based & PRD-Driven)
**When to Transition**: Recognize when the project has evolved (or has initiated a project which existing code) beyond simple task management. Look for these indicators:
- User mentions teammates or collaboration needs
- Project has grown to 15+ tasks with mixed priorities
- User creates feature branches or mentions major initiatives
- User initializes Taskmaster on an existing, complex codebase
- User describes large features that would benefit from dedicated planning
**Your Role in Transition**: Guide the user to a more sophisticated workflow that leverages tags for organization and PRDs for comprehensive planning.
#### Master List Strategy (High-Value Focus)
Once you transition to tag-based workflows, the `master` tag should ideally contain only:
- **High-level deliverables** that provide significant business value
- **Major milestones** and epic-level features
- **Critical infrastructure** work that affects the entire project
- **Release-blocking** items
**What NOT to put in master**:
- Detailed implementation subtasks (these go in feature-specific tags' parent tasks)
- Refactoring work (create dedicated tags like `refactor-auth`)
- Experimental features (use `experiment-*` tags)
- Team member-specific tasks (use person-specific tags)
#### PRD-Driven Feature Development
**For New Major Features**:
1. **Identify the Initiative**: When user describes a significant feature
2. **Create Dedicated Tag**: `add_tag feature-[name] --description="[Feature description]"`
3. **Collaborative PRD Creation**: Work with user to create comprehensive PRD in `.taskmaster/docs/feature-[name]-prd.txt`
4. **Parse & Prepare**:
- `parse_prd .taskmaster/docs/feature-[name]-prd.txt --tag=feature-[name]`
- `analyze_project_complexity --tag=feature-[name] --research`
- `expand_all --tag=feature-[name] --research`
5. **Add Master Reference**: Create a high-level task in `master` that references the feature tag
**For Existing Codebase Analysis**:
When users initialize Taskmaster on existing projects:
1. **Codebase Discovery**: Use your native tools for producing deep context about the code base. You may use `research` tool with `--tree` and `--files` to collect up to date information using the existing architecture as context.
2. **Collaborative Assessment**: Work with user to identify improvement areas, technical debt, or new features
3. **Strategic PRD Creation**: Co-author PRDs that include:
- Current state analysis (based on your codebase research)
- Proposed improvements or new features
- Implementation strategy considering existing code
4. **Tag-Based Organization**: Parse PRDs into appropriate tags (`refactor-api`, `feature-dashboard`, `tech-debt`, etc.)
5. **Master List Curation**: Keep only the most valuable initiatives in master
The parse-prd's `--append` flag enables the user to parse multiple PRDs within tags or across tags. PRDs should be focused and the number of tasks they are parsed into should be strategically chosen relative to the PRD's complexity and level of detail.
### Workflow Transition Examples
**Example 1: Simple → Team-Based**
```
User: "Alice is going to help with the API work"
Your Response: "Great! To avoid conflicts, I'll create a separate task context for your work. Alice can continue with the master list while you work in your own context. When you're ready to merge, we can coordinate the tasks back together."
Action: add_tag my-api-work --copy-from-current --description="My API tasks while collaborating with Alice"
```
**Example 2: Simple → PRD-Driven**
```
User: "I want to add a complete user dashboard with analytics, user management, and reporting"
Your Response: "This sounds like a major feature that would benefit from detailed planning. Let me create a dedicated context for this work and we can draft a PRD together to ensure we capture all requirements."
Actions:
1. add_tag feature-dashboard --description="User dashboard with analytics and management"
2. Collaborate on PRD creation
3. parse_prd dashboard-prd.txt --tag=feature-dashboard
4. Add high-level "User Dashboard" task to master
```
**Example 3: Existing Project → Strategic Planning**
```
User: "I just initialized Taskmaster on my existing React app. It's getting messy and I want to improve it."
Your Response: "Let me research your codebase to understand the current architecture, then we can create a strategic plan for improvements."
Actions:
1. research "Current React app architecture and improvement opportunities" --tree --files=src/
2. Collaborate on improvement PRD based on findings
3. Create tags for different improvement areas (refactor-components, improve-state-management, etc.)
4. Keep only major improvement initiatives in master
```
---
## Primary Interaction: MCP Server vs. CLI
Taskmaster offers two primary ways to interact:
1. **MCP Server (Recommended for Integrated Tools)**:
- For AI agents and integrated development environments (like Cursor), interacting via the **MCP server is the preferred method**.
- The MCP server exposes Taskmaster functionality through a set of tools (e.g., `get_tasks`, `add_subtask`).
- This method offers better performance, structured data exchange, and richer error handling compared to CLI parsing.
- Refer to @`mcp.mdc` for details on the MCP architecture and available tools.
- A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in @`taskmaster.mdc`.
- **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change.
- **Note**: MCP tools fully support tagged task lists with complete tag management capabilities.
2. **`task-master` CLI (For Users & Fallback)**:
- The global `task-master` command provides a user-friendly interface for direct terminal interaction.
- It can also serve as a fallback if the MCP server is inaccessible or a specific function isn't exposed via MCP.
- Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`.
- The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`).
- Refer to @`taskmaster.mdc` for a detailed command reference.
- **Tagged Task Lists**: CLI fully supports the new tagged system with seamless migration.
## How the Tag System Works (For Your Reference)
- **Data Structure**: Tasks are organized into separate contexts (tags) like "master", "feature-branch", or "v2.0".
- **Silent Migration**: Existing projects automatically migrate to use a "master" tag with zero disruption.
- **Context Isolation**: Tasks in different tags are completely separate. Changes in one tag do not affect any other tag.
- **Manual Control**: The user is always in control. There is no automatic switching. You facilitate switching by using `use-tag <name>`.
- **Full CLI & MCP Support**: All tag management commands are available through both the CLI and MCP tools for you to use. Refer to @`taskmaster.mdc` for a full command list.
---
## Task Complexity Analysis
- Run `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.mdc`) for comprehensive analysis
- Review complexity report via `complexity_report` / `task-master complexity-report` (see @`taskmaster.mdc`) for a formatted, readable version.
- Focus on tasks with highest complexity scores (8-10) for detailed breakdown
- Use analysis results to determine appropriate subtask allocation
- Note that reports are automatically used by the `expand_task` tool/command
## Task Breakdown Process
- Use `expand_task` / `task-master expand --id=<id>`. It automatically uses the complexity report if found, otherwise generates default number of subtasks.
- Use `--num=<number>` to specify an explicit number of subtasks, overriding defaults or complexity report recommendations.
- Add `--research` flag to leverage Perplexity AI for research-backed expansion.
- Add `--force` flag to clear existing subtasks before generating new ones (default is to append).
- Use `--prompt="<context>"` to provide additional context when needed.
- Review and adjust generated subtasks as necessary.
- Use `expand_all` tool or `task-master expand --all` to expand multiple pending tasks at once, respecting flags like `--force` and `--research`.
- If subtasks need complete replacement (regardless of the `--force` flag on `expand`), clear them first with `clear_subtasks` / `task-master clear-subtasks --id=<id>`.
## Implementation Drift Handling
- When implementation differs significantly from planned approach
- When future tasks need modification due to current implementation choices
- When new dependencies or requirements emerge
- Use `update` / `task-master update --from=<futureTaskId> --prompt='<explanation>\nUpdate context...' --research` to update multiple future tasks.
- Use `update_task` / `task-master update-task --id=<taskId> --prompt='<explanation>\nUpdate context...' --research` to update a single specific task.
## Task Status Management
- Use 'pending' for tasks ready to be worked on
- Use 'done' for completed and verified tasks
- Use 'deferred' for postponed tasks
- Add custom status values as needed for project-specific workflows
## Task Structure Fields
- **id**: Unique identifier for the task (Example: `1`, `1.1`)
- **title**: Brief, descriptive title (Example: `"Initialize Repo"`)
- **description**: Concise summary of what the task involves (Example: `"Create a new repository, set up initial structure."`)
- **status**: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
- **dependencies**: IDs of prerequisite tasks (Example: `[1, 2.1]`)
- Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
- This helps quickly identify which prerequisite tasks are blocking work
- **priority**: Importance level (Example: `"high"`, `"medium"`, `"low"`)
- **details**: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
- **testStrategy**: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
- **subtasks**: List of smaller, more specific tasks (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
- Refer to task structure details (previously linked to `tasks.mdc`).
## Configuration Management (Updated)
Taskmaster configuration is managed through two main mechanisms:
1. **`.taskmaster/config.json` File (Primary):**
* Located in the project root directory.
* Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc.
* **Tagged System Settings**: Includes `global.defaultTag` (defaults to "master") and `tags` section for tag management configuration.
* **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing.
* **View/Set specific models via `task-master models` command or `models` MCP tool.**
* Created automatically when you run `task-master models --setup` for the first time or during tagged system migration.
2. **Environment Variables (`.env` / `mcp.json`):**
* Used **only** for sensitive API keys and specific endpoint URLs.
* Place API keys (one per provider) in a `.env` file in the project root for CLI usage.
* For MCP/Cursor integration, configure these keys in the `env` section of `.cursor/mcp.json`.
* Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.mdc`).
3. **`.taskmaster/state.json` File (Tagged System State):**
* Tracks current tag context and migration status.
* Automatically created during tagged system migration.
* Contains: `currentTag`, `lastSwitched`, `migrationNoticeShown`.
**Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool.
**If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.cursor/mcp.json`.
**If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project.
## Rules Management
Taskmaster supports multiple AI coding assistant rule sets that can be configured during project initialization or managed afterward:
- **Available Profiles**: Claude Code, Cline, Codex, Cursor, Roo Code, Trae, Windsurf (claude, cline, codex, cursor, roo, trae, windsurf)
- **During Initialization**: Use `task-master init --rules cursor,windsurf` to specify which rule sets to include
- **After Initialization**: Use `task-master rules add <profiles>` or `task-master rules remove <profiles>` to manage rule sets
- **Interactive Setup**: Use `task-master rules setup` to launch an interactive prompt for selecting rule profiles
- **Default Behavior**: If no `--rules` flag is specified during initialization, all available rule profiles are included
- **Rule Structure**: Each profile creates its own directory (e.g., `.cursor/rules`, `.roo/rules`) with appropriate configuration files
## Determining the Next Task
- Run `next_task` / `task-master next` to show the next task to work on.
- The command identifies tasks with all dependencies satisfied
- Tasks are prioritized by priority level, dependency count, and ID
- The command shows comprehensive task information including:
- Basic task details and description
- Implementation details
- Subtasks (if they exist)
- Contextual suggested actions
- Recommended before starting any new development work
- Respects your project's dependency structure
- Ensures tasks are completed in the appropriate sequence
- Provides ready-to-use commands for common task actions
## Viewing Specific Task Details
- Run `get_task` / `task-master show <id>` to view a specific task.
- Use dot notation for subtasks: `task-master show 1.2` (shows subtask 2 of task 1)
- Displays comprehensive information similar to the next command, but for a specific task
- For parent tasks, shows all subtasks and their current status
- For subtasks, shows parent task information and relationship
- Provides contextual suggested actions appropriate for the specific task
- Useful for examining task details before implementation or checking status
## Managing Task Dependencies
- Use `add_dependency` / `task-master add-dependency --id=<id> --depends-on=<id>` to add a dependency.
- Use `remove_dependency` / `task-master remove-dependency --id=<id> --depends-on=<id>` to remove a dependency.
- The system prevents circular dependencies and duplicate dependency entries
- Dependencies are checked for existence before being added or removed
- Task files are automatically regenerated after dependency changes
- Dependencies are visualized with status indicators in task listings and files
## Task Reorganization
- Use `move_task` / `task-master move --from=<id> --to=<id>` to move tasks or subtasks within the hierarchy
- This command supports several use cases:
- Moving a standalone task to become a subtask (e.g., `--from=5 --to=7`)
- Moving a subtask to become a standalone task (e.g., `--from=5.2 --to=7`)
- Moving a subtask to a different parent (e.g., `--from=5.2 --to=7.3`)
- Reordering subtasks within the same parent (e.g., `--from=5.2 --to=5.4`)
- Moving a task to a new, non-existent ID position (e.g., `--from=5 --to=25`)
- Moving multiple tasks at once using comma-separated IDs (e.g., `--from=10,11,12 --to=16,17,18`)
- The system includes validation to prevent data loss:
- Allows moving to non-existent IDs by creating placeholder tasks
- Prevents moving to existing task IDs that have content (to avoid overwriting)
- Validates source tasks exist before attempting to move them
- The system maintains proper parent-child relationships and dependency integrity
- Task files are automatically regenerated after the move operation
- This provides greater flexibility in organizing and refining your task structure as project understanding evolves
- This is especially useful when dealing with potential merge conflicts arising from teams creating tasks on separate branches. Solve these conflicts very easily by moving your tasks and keeping theirs.
## Iterative Subtask Implementation
Once a task has been broken down into subtasks using `expand_task` or similar methods, follow this iterative process for implementation:
1. **Understand the Goal (Preparation):**
* Use `get_task` / `task-master show <subtaskId>` (see @`taskmaster.mdc`) to thoroughly understand the specific goals and requirements of the subtask.
2. **Initial Exploration & Planning (Iteration 1):**
* This is the first attempt at creating a concrete implementation plan.
* Explore the codebase to identify the precise files, functions, and even specific lines of code that will need modification.
* Determine the intended code changes (diffs) and their locations.
* Gather *all* relevant details from this exploration phase.
3. **Log the Plan:**
* Run `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<detailed plan>'`.
* Provide the *complete and detailed* findings from the exploration phase in the prompt. Include file paths, line numbers, proposed diffs, reasoning, and any potential challenges identified. Do not omit details. The goal is to create a rich, timestamped log within the subtask's `details`.
4. **Verify the Plan:**
* Run `get_task` / `task-master show <subtaskId>` again to confirm that the detailed implementation plan has been successfully appended to the subtask's details.
5. **Begin Implementation:**
* Set the subtask status using `set_task_status` / `task-master set-status --id=<subtaskId> --status=in-progress`.
* Start coding based on the logged plan.
6. **Refine and Log Progress (Iteration 2+):**
* As implementation progresses, you will encounter challenges, discover nuances, or confirm successful approaches.
* **Before appending new information**: Briefly review the *existing* details logged in the subtask (using `get_task` or recalling from context) to ensure the update adds fresh insights and avoids redundancy.
* **Regularly** use `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<update details>\n- What worked...\n- What didn't work...'` to append new findings.
* **Crucially, log:**
* What worked ("fundamental truths" discovered).
* What didn't work and why (to avoid repeating mistakes).
* Specific code snippets or configurations that were successful.
* Decisions made, especially if confirmed with user input.
* Any deviations from the initial plan and the reasoning.
* The objective is to continuously enrich the subtask's details, creating a log of the implementation journey that helps the AI (and human developers) learn, adapt, and avoid repeating errors.
7. **Review & Update Rules (Post-Implementation):**
* Once the implementation for the subtask is functionally complete, review all code changes and the relevant chat history.
* Identify any new or modified code patterns, conventions, or best practices established during the implementation.
* Create new or update existing rules following internal guidelines (previously linked to `cursor_rules.mdc` and `self_improve.mdc`).
8. **Mark Task Complete:**
* After verifying the implementation and updating any necessary rules, mark the subtask as completed: `set_task_status` / `task-master set-status --id=<subtaskId> --status=done`.
9. **Commit Changes (If using Git):**
* Stage the relevant code changes and any updated/new rule files (`git add .`).
* Craft a comprehensive Git commit message summarizing the work done for the subtask, including both code implementation and any rule adjustments.
* Execute the commit command directly in the terminal (e.g., `git commit -m 'feat(module): Implement feature X for subtask <subtaskId>\n\n- Details about changes...\n- Updated rule Y for pattern Z'`).
* Consider if a Changeset is needed according to internal versioning guidelines (previously linked to `changeset.mdc`). If so, run `npm run changeset`, stage the generated file, and amend the commit or create a new one.
10. **Proceed to Next Subtask:**
* Identify the next subtask (e.g., using `next_task` / `task-master next`).
## Code Analysis & Refactoring Techniques
- **Top-Level Function Search**:
- Useful for understanding module structure or planning refactors.
- Use grep/ripgrep to find exported functions/constants:
`rg "export (async function|function|const) \w+"` or similar patterns.
- Can help compare functions between files during migrations or identify potential naming conflicts.
---
*This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.*

View File

@@ -0,0 +1,558 @@
---
description: Comprehensive reference for Taskmaster MCP tools and CLI commands.
globs: **/*
alwaysApply: true
---
# Taskmaster Tool & Command Reference
This document provides a detailed reference for interacting with Taskmaster, covering both the recommended MCP tools, suitable for integrations like Cursor, and the corresponding `task-master` CLI commands, designed for direct user interaction or fallback.
**Note:** For interacting with Taskmaster programmatically or via integrated tools, using the **MCP tools is strongly recommended** due to better performance, structured data, and error handling. The CLI commands serve as a user-friendly alternative and fallback.
**Important:** Several MCP tools involve AI processing... The AI-powered tools include `parse_prd`, `analyze_project_complexity`, `update_subtask`, `update_task`, `update`, `expand_all`, `expand_task`, and `add_task`.
**🏷️ Tagged Task Lists System:** Task Master now supports **tagged task lists** for multi-context task management. This allows you to maintain separate, isolated lists of tasks for different features, branches, or experiments. Existing projects are seamlessly migrated to use a default "master" tag. Most commands now support a `--tag <name>` flag to specify which context to operate on. If omitted, commands use the currently active tag.
---
## Initialization & Setup
### 1. Initialize Project (`init`)
* **MCP Tool:** `initialize_project`
* **CLI Command:** `task-master init [options]`
* **Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project.`
* **Key CLI Options:**
* `--name <name>`: `Set the name for your project in Taskmaster's configuration.`
* `--description <text>`: `Provide a brief description for your project.`
* `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.`
* `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.`
* **Usage:** Run this once at the beginning of a new project.
* **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.`
* **Key MCP Parameters/Options:**
* `projectName`: `Set the name for your project.` (CLI: `--name <name>`)
* `projectDescription`: `Provide a brief description for your project.` (CLI: `--description <text>`)
* `projectVersion`: `Set the initial version for your project, e.g., '0.1.0'.` (CLI: `--version <version>`)
* `authorName`: `Author name.` (CLI: `--author <author>`)
* `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`)
* `addAliases`: `Add shell aliases tm and taskmaster. Default is false.` (CLI: `--aliases`)
* `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`)
* **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like Cursor. Operates on the current working directory of the MCP server.
* **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt.
* **Tagging:** Use the `--tag` option to parse the PRD into a specific, non-default tag context. If the tag doesn't exist, it will be created automatically. Example: `task-master parse-prd spec.txt --tag=new-feature`.
### 2. Parse PRD (`parse_prd`)
* **MCP Tool:** `parse_prd`
* **CLI Command:** `task-master parse-prd [file] [options]`
* **Description:** `Parse a Product Requirements Document, PRD, or text file with Taskmaster to automatically generate an initial set of tasks in tasks.json.`
* **Key Parameters/Options:**
* `input`: `Path to your PRD or requirements text file that Taskmaster should parse for tasks.` (CLI: `[file]` positional or `-i, --input <file>`)
* `output`: `Specify where Taskmaster should save the generated 'tasks.json' file. Defaults to '.taskmaster/tasks/tasks.json'.` (CLI: `-o, --output <file>`)
* `numTasks`: `Approximate number of top-level tasks Taskmaster should aim to generate from the document.` (CLI: `-n, --num-tasks <number>`)
* `force`: `Use this to allow Taskmaster to overwrite an existing 'tasks.json' without asking for confirmation.` (CLI: `-f, --force`)
* **Usage:** Useful for bootstrapping a project from an existing requirements document.
* **Notes:** Task Master will strictly adhere to any specific requirements mentioned in the PRD, such as libraries, database schemas, frameworks, tech stacks, etc., while filling in any gaps where the PRD isn't fully specified. Tasks are designed to provide the most direct implementation path while avoiding over-engineering.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. If the user does not have a PRD, suggest discussing their idea and then use the example PRD in `.taskmaster/templates/example_prd.txt` as a template for creating the PRD based on their idea, for use with `parse-prd`.
---
## AI Model Configuration
### 2. Manage Models (`models`)
* **MCP Tool:** `models`
* **CLI Command:** `task-master models [options]`
* **Description:** `View the current AI model configuration or set specific models for different roles (main, research, fallback). Allows setting custom model IDs for Ollama and OpenRouter.`
* **Key MCP Parameters/Options:**
* `setMain <model_id>`: `Set the primary model ID for task generation/updates.` (CLI: `--set-main <model_id>`)
* `setResearch <model_id>`: `Set the model ID for research-backed operations.` (CLI: `--set-research <model_id>`)
* `setFallback <model_id>`: `Set the model ID to use if the primary fails.` (CLI: `--set-fallback <model_id>`)
* `ollama <boolean>`: `Indicates the set model ID is a custom Ollama model.` (CLI: `--ollama`)
* `openrouter <boolean>`: `Indicates the set model ID is a custom OpenRouter model.` (CLI: `--openrouter`)
* `listAvailableModels <boolean>`: `If true, lists available models not currently assigned to a role.` (CLI: No direct equivalent; CLI lists available automatically)
* `projectRoot <string>`: `Optional. Absolute path to the project root directory.` (CLI: Determined automatically)
* **Key CLI Options:**
* `--set-main <model_id>`: `Set the primary model.`
* `--set-research <model_id>`: `Set the research model.`
* `--set-fallback <model_id>`: `Set the fallback model.`
* `--ollama`: `Specify that the provided model ID is for Ollama (use with --set-*).`
* `--openrouter`: `Specify that the provided model ID is for OpenRouter (use with --set-*). Validates against OpenRouter API.`
* `--bedrock`: `Specify that the provided model ID is for AWS Bedrock (use with --set-*).`
* `--setup`: `Run interactive setup to configure models, including custom Ollama/OpenRouter IDs.`
* **Usage (MCP):** Call without set flags to get current config. Use `setMain`, `setResearch`, or `setFallback` with a valid model ID to update the configuration. Use `listAvailableModels: true` to get a list of unassigned models. To set a custom model, provide the model ID and set `ollama: true` or `openrouter: true`.
* **Usage (CLI):** Run without flags to view current configuration and available models. Use set flags to update specific roles. Use `--setup` for guided configuration, including custom models. To set a custom model via flags, use `--set-<role>=<model_id>` along with either `--ollama` or `--openrouter`.
* **Notes:** Configuration is stored in `.taskmaster/config.json` in the project root. This command/tool modifies that file. Use `listAvailableModels` or `task-master models` to see internally supported models. OpenRouter custom models are validated against their live API. Ollama custom models are not validated live.
* **API note:** API keys for selected AI providers (based on their model) need to exist in the mcp.json file to be accessible in MCP context. The API keys must be present in the local .env file for the CLI to be able to read them.
* **Model costs:** The costs in supported models are expressed in dollars. An input/output value of 3 is $3.00. A value of 0.8 is $0.80.
* **Warning:** DO NOT MANUALLY EDIT THE .taskmaster/config.json FILE. Use the included commands either in the MCP or CLI format as needed. Always prioritize MCP tools when available and use the CLI as a fallback.
---
## Task Listing & Viewing
### 3. Get Tasks (`get_tasks`)
* **MCP Tool:** `get_tasks`
* **CLI Command:** `task-master list [options]`
* **Description:** `List your Taskmaster tasks, optionally filtering by status and showing subtasks.`
* **Key Parameters/Options:**
* `status`: `Show only Taskmaster tasks matching this status (or multiple statuses, comma-separated), e.g., 'pending' or 'done,in-progress'.` (CLI: `-s, --status <status>`)
* `withSubtasks`: `Include subtasks indented under their parent tasks in the list.` (CLI: `--with-subtasks`)
* `tag`: `Specify which tag context to list tasks from. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Get an overview of the project status, often used at the start of a work session.
### 4. Get Next Task (`next_task`)
* **MCP Tool:** `next_task`
* **CLI Command:** `task-master next [options]`
* **Description:** `Ask Taskmaster to show the next available task you can work on, based on status and completed dependencies.`
* **Key Parameters/Options:**
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* `tag`: `Specify which tag context to use. Defaults to the current active tag.` (CLI: `--tag <name>`)
* **Usage:** Identify what to work on next according to the plan.
### 5. Get Task Details (`get_task`)
* **MCP Tool:** `get_task`
* **CLI Command:** `task-master show [id] [options]`
* **Description:** `Display detailed information for one or more specific Taskmaster tasks or subtasks by ID.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster task (e.g., '15'), subtask (e.g., '15.2'), or a comma-separated list of IDs ('1,5,10.2') you want to view.` (CLI: `[id]` positional or `-i, --id <id>`)
* `tag`: `Specify which tag context to get the task(s) from. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Understand the full details for a specific task. When multiple IDs are provided, a summary table is shown.
* **CRITICAL INFORMATION** If you need to collect information from multiple tasks, use comma-separated IDs (i.e. 1,2,3) to receive an array of tasks. Do not needlessly get tasks one at a time if you need to get many as that is wasteful.
---
## Task Creation & Modification
### 6. Add Task (`add_task`)
* **MCP Tool:** `add_task`
* **CLI Command:** `task-master add-task [options]`
* **Description:** `Add a new task to Taskmaster by describing it; AI will structure it.`
* **Key Parameters/Options:**
* `prompt`: `Required. Describe the new task you want Taskmaster to create, e.g., "Implement user authentication using JWT".` (CLI: `-p, --prompt <text>`)
* `dependencies`: `Specify the IDs of any Taskmaster tasks that must be completed before this new one can start, e.g., '12,14'.` (CLI: `-d, --dependencies <ids>`)
* `priority`: `Set the priority for the new task: 'high', 'medium', or 'low'. Default is 'medium'.` (CLI: `--priority <priority>`)
* `research`: `Enable Taskmaster to use the research role for potentially more informed task creation.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context to add the task to. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Quickly add newly identified tasks during development.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 7. Add Subtask (`add_subtask`)
* **MCP Tool:** `add_subtask`
* **CLI Command:** `task-master add-subtask [options]`
* **Description:** `Add a new subtask to a Taskmaster parent task, or convert an existing task into a subtask.`
* **Key Parameters/Options:**
* `id` / `parent`: `Required. The ID of the Taskmaster task that will be the parent.` (MCP: `id`, CLI: `-p, --parent <id>`)
* `taskId`: `Use this if you want to convert an existing top-level Taskmaster task into a subtask of the specified parent.` (CLI: `-i, --task-id <id>`)
* `title`: `Required if not using taskId. The title for the new subtask Taskmaster should create.` (CLI: `-t, --title <title>`)
* `description`: `A brief description for the new subtask.` (CLI: `-d, --description <text>`)
* `details`: `Provide implementation notes or details for the new subtask.` (CLI: `--details <text>`)
* `dependencies`: `Specify IDs of other tasks or subtasks, e.g., '15' or '16.1', that must be done before this new subtask.` (CLI: `--dependencies <ids>`)
* `status`: `Set the initial status for the new subtask. Default is 'pending'.` (CLI: `-s, --status <status>`)
* `skipGenerate`: `Prevent Taskmaster from automatically regenerating markdown task files after adding the subtask.` (CLI: `--skip-generate`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Break down tasks manually or reorganize existing tasks.
### 8. Update Tasks (`update`)
* **MCP Tool:** `update`
* **CLI Command:** `task-master update [options]`
* **Description:** `Update multiple upcoming tasks in Taskmaster based on new context or changes, starting from a specific task ID.`
* **Key Parameters/Options:**
* `from`: `Required. The ID of the first task Taskmaster should update. All tasks with this ID or higher that are not 'done' will be considered.` (CLI: `--from <id>`)
* `prompt`: `Required. Explain the change or new context for Taskmaster to apply to the tasks, e.g., "We are now using React Query instead of Redux Toolkit for data fetching".` (CLI: `-p, --prompt <text>`)
* `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Handle significant implementation changes or pivots that affect multiple future tasks. Example CLI: `task-master update --from='18' --prompt='Switching to React Query.\nNeed to refactor data fetching...'`
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 9. Update Task (`update_task`)
* **MCP Tool:** `update_task`
* **CLI Command:** `task-master update-task [options]`
* **Description:** `Modify a specific Taskmaster task by ID, incorporating new information or changes. By default, this replaces the existing task details.`
* **Key Parameters/Options:**
* `id`: `Required. The specific ID of the Taskmaster task, e.g., '15', you want to update.` (CLI: `-i, --id <id>`)
* `prompt`: `Required. Explain the specific changes or provide the new information Taskmaster should incorporate into this task.` (CLI: `-p, --prompt <text>`)
* `append`: `If true, appends the prompt content to the task's details with a timestamp, rather than replacing them. Behaves like update-subtask.` (CLI: `--append`)
* `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Refine a specific task based on new understanding. Use `--append` to log progress without creating subtasks.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 10. Update Subtask (`update_subtask`)
* **MCP Tool:** `update_subtask`
* **CLI Command:** `task-master update-subtask [options]`
* **Description:** `Append timestamped notes or details to a specific Taskmaster subtask without overwriting existing content. Intended for iterative implementation logging.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster subtask, e.g., '5.2', to update with new information.` (CLI: `-i, --id <id>`)
* `prompt`: `Required. The information, findings, or progress notes to append to the subtask's details with a timestamp.` (CLI: `-p, --prompt <text>`)
* `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context the subtask belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Log implementation progress, findings, and discoveries during subtask development. Each update is timestamped and appended to preserve the implementation journey.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 11. Set Task Status (`set_task_status`)
* **MCP Tool:** `set_task_status`
* **CLI Command:** `task-master set-status [options]`
* **Description:** `Update the status of one or more Taskmaster tasks or subtasks, e.g., 'pending', 'in-progress', 'done'.`
* **Key Parameters/Options:**
* `id`: `Required. The ID(s) of the Taskmaster task(s) or subtask(s), e.g., '15', '15.2', or '16,17.1', to update.` (CLI: `-i, --id <id>`)
* `status`: `Required. The new status to set, e.g., 'done', 'pending', 'in-progress', 'review', 'cancelled'.` (CLI: `-s, --status <status>`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Mark progress as tasks move through the development cycle.
### 12. Remove Task (`remove_task`)
* **MCP Tool:** `remove_task`
* **CLI Command:** `task-master remove-task [options]`
* **Description:** `Permanently remove a task or subtask from the Taskmaster tasks list.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster task, e.g., '5', or subtask, e.g., '5.2', to permanently remove.` (CLI: `-i, --id <id>`)
* `yes`: `Skip the confirmation prompt and immediately delete the task.` (CLI: `-y, --yes`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Permanently delete tasks or subtasks that are no longer needed in the project.
* **Notes:** Use with caution as this operation cannot be undone. Consider using 'blocked', 'cancelled', or 'deferred' status instead if you just want to exclude a task from active planning but keep it for reference. The command automatically cleans up dependency references in other tasks.
---
## Task Structure & Breakdown
### 13. Expand Task (`expand_task`)
* **MCP Tool:** `expand_task`
* **CLI Command:** `task-master expand [options]`
* **Description:** `Use Taskmaster's AI to break down a complex task into smaller, manageable subtasks. Appends subtasks by default.`
* **Key Parameters/Options:**
* `id`: `The ID of the specific Taskmaster task you want to break down into subtasks.` (CLI: `-i, --id <id>`)
* `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create. Uses complexity analysis/defaults otherwise.` (CLI: `-n, --num <number>`)
* `research`: `Enable Taskmaster to use the research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
* `prompt`: `Optional: Provide extra context or specific instructions to Taskmaster for generating the subtasks.` (CLI: `-p, --prompt <text>`)
* `force`: `Optional: If true, clear existing subtasks before generating new ones. Default is false (append).` (CLI: `--force`)
* `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Generate a detailed implementation plan for a complex task before starting coding. Automatically uses complexity report recommendations if available and `num` is not specified.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 14. Expand All Tasks (`expand_all`)
* **MCP Tool:** `expand_all`
* **CLI Command:** `task-master expand --all [options]` (Note: CLI uses the `expand` command with the `--all` flag)
* **Description:** `Tell Taskmaster to automatically expand all eligible pending/in-progress tasks based on complexity analysis or defaults. Appends subtasks by default.`
* **Key Parameters/Options:**
* `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create per task.` (CLI: `-n, --num <number>`)
* `research`: `Enable research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
* `prompt`: `Optional: Provide extra context for Taskmaster to apply generally during expansion.` (CLI: `-p, --prompt <text>`)
* `force`: `Optional: If true, clear existing subtasks before generating new ones for each eligible task. Default is false (append).` (CLI: `--force`)
* `tag`: `Specify which tag context to expand. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Useful after initial task generation or complexity analysis to break down multiple tasks at once.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 15. Clear Subtasks (`clear_subtasks`)
* **MCP Tool:** `clear_subtasks`
* **CLI Command:** `task-master clear-subtasks [options]`
* **Description:** `Remove all subtasks from one or more specified Taskmaster parent tasks.`
* **Key Parameters/Options:**
* `id`: `The ID(s) of the Taskmaster parent task(s) whose subtasks you want to remove, e.g., '15' or '16,18'. Required unless using 'all'.` (CLI: `-i, --id <ids>`)
* `all`: `Tell Taskmaster to remove subtasks from all parent tasks.` (CLI: `--all`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Used before regenerating subtasks with `expand_task` if the previous breakdown needs replacement.
### 16. Remove Subtask (`remove_subtask`)
* **MCP Tool:** `remove_subtask`
* **CLI Command:** `task-master remove-subtask [options]`
* **Description:** `Remove a subtask from its Taskmaster parent, optionally converting it into a standalone task.`
* **Key Parameters/Options:**
* `id`: `Required. The ID(s) of the Taskmaster subtask(s) to remove, e.g., '15.2' or '16.1,16.3'.` (CLI: `-i, --id <id>`)
* `convert`: `If used, Taskmaster will turn the subtask into a regular top-level task instead of deleting it.` (CLI: `-c, --convert`)
* `skipGenerate`: `Prevent Taskmaster from automatically regenerating markdown task files after removing the subtask.` (CLI: `--skip-generate`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Delete unnecessary subtasks or promote a subtask to a top-level task.
### 17. Move Task (`move_task`)
* **MCP Tool:** `move_task`
* **CLI Command:** `task-master move [options]`
* **Description:** `Move a task or subtask to a new position within the task hierarchy.`
* **Key Parameters/Options:**
* `from`: `Required. ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated for multiple tasks.` (CLI: `--from <id>`)
* `to`: `Required. ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated.` (CLI: `--to <id>`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Reorganize tasks by moving them within the hierarchy. Supports various scenarios like:
* Moving a task to become a subtask
* Moving a subtask to become a standalone task
* Moving a subtask to a different parent
* Reordering subtasks within the same parent
* Moving a task to a new, non-existent ID (automatically creates placeholders)
* Moving multiple tasks at once with comma-separated IDs
* **Validation Features:**
* Allows moving tasks to non-existent destination IDs (creates placeholder tasks)
* Prevents moving to existing task IDs that already have content (to avoid overwriting)
* Validates that source tasks exist before attempting to move them
* Maintains proper parent-child relationships
* **Example CLI:** `task-master move --from=5.2 --to=7.3` to move subtask 5.2 to become subtask 7.3.
* **Example Multi-Move:** `task-master move --from=10,11,12 --to=16,17,18` to move multiple tasks to new positions.
* **Common Use:** Resolving merge conflicts in tasks.json when multiple team members create tasks on different branches.
---
## Dependency Management
### 18. Add Dependency (`add_dependency`)
* **MCP Tool:** `add_dependency`
* **CLI Command:** `task-master add-dependency [options]`
* **Description:** `Define a dependency in Taskmaster, making one task a prerequisite for another.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster task that will depend on another.` (CLI: `-i, --id <id>`)
* `dependsOn`: `Required. The ID of the Taskmaster task that must be completed first, the prerequisite.` (CLI: `-d, --depends-on <id>`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <path>`)
* **Usage:** Establish the correct order of execution between tasks.
### 19. Remove Dependency (`remove_dependency`)
* **MCP Tool:** `remove_dependency`
* **CLI Command:** `task-master remove-dependency [options]`
* **Description:** `Remove a dependency relationship between two Taskmaster tasks.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster task you want to remove a prerequisite from.` (CLI: `-i, --id <id>`)
* `dependsOn`: `Required. The ID of the Taskmaster task that should no longer be a prerequisite.` (CLI: `-d, --depends-on <id>`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Update task relationships when the order of execution changes.
### 20. Validate Dependencies (`validate_dependencies`)
* **MCP Tool:** `validate_dependencies`
* **CLI Command:** `task-master validate-dependencies [options]`
* **Description:** `Check your Taskmaster tasks for dependency issues (like circular references or links to non-existent tasks) without making changes.`
* **Key Parameters/Options:**
* `tag`: `Specify which tag context to validate. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Audit the integrity of your task dependencies.
### 21. Fix Dependencies (`fix_dependencies`)
* **MCP Tool:** `fix_dependencies`
* **CLI Command:** `task-master fix-dependencies [options]`
* **Description:** `Automatically fix dependency issues (like circular references or links to non-existent tasks) in your Taskmaster tasks.`
* **Key Parameters/Options:**
* `tag`: `Specify which tag context to fix dependencies in. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Clean up dependency errors automatically.
---
## Analysis & Reporting
### 22. Analyze Project Complexity (`analyze_project_complexity`)
* **MCP Tool:** `analyze_project_complexity`
* **CLI Command:** `task-master analyze-complexity [options]`
* **Description:** `Have Taskmaster analyze your tasks to determine their complexity and suggest which ones need to be broken down further.`
* **Key Parameters/Options:**
* `output`: `Where to save the complexity analysis report. Default is '.taskmaster/reports/task-complexity-report.json' (or '..._tagname.json' if a tag is used).` (CLI: `-o, --output <file>`)
* `threshold`: `The minimum complexity score (1-10) that should trigger a recommendation to expand a task.` (CLI: `-t, --threshold <number>`)
* `research`: `Enable research role for more accurate complexity analysis. Requires appropriate API key.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context to analyze. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Used before breaking down tasks to identify which ones need the most attention.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 23. View Complexity Report (`complexity_report`)
* **MCP Tool:** `complexity_report`
* **CLI Command:** `task-master complexity-report [options]`
* **Description:** `Display the task complexity analysis report in a readable format.`
* **Key Parameters/Options:**
* `tag`: `Specify which tag context to show the report for. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to the complexity report (default: '.taskmaster/reports/task-complexity-report.json').` (CLI: `-f, --file <file>`)
* **Usage:** Review and understand the complexity analysis results after running analyze-complexity.
---
## File Management
### 24. Generate Task Files (`generate`)
* **MCP Tool:** `generate`
* **CLI Command:** `task-master generate [options]`
* **Description:** `Create or update individual Markdown files for each task based on your tasks.json.`
* **Key Parameters/Options:**
* `output`: `The directory where Taskmaster should save the task files (default: in a 'tasks' directory).` (CLI: `-o, --output <directory>`)
* `tag`: `Specify which tag context to generate files for. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Run this after making changes to tasks.json to keep individual task files up to date. This command is now manual and no longer runs automatically.
---
## AI-Powered Research
### 25. Research (`research`)
* **MCP Tool:** `research`
* **CLI Command:** `task-master research [options]`
* **Description:** `Perform AI-powered research queries with project context to get fresh, up-to-date information beyond the AI's knowledge cutoff.`
* **Key Parameters/Options:**
* `query`: `Required. Research query/prompt (e.g., "What are the latest best practices for React Query v5?").` (CLI: `[query]` positional or `-q, --query <text>`)
* `taskIds`: `Comma-separated list of task/subtask IDs from the current tag context (e.g., "15,16.2,17").` (CLI: `-i, --id <ids>`)
* `filePaths`: `Comma-separated list of file paths for context (e.g., "src/api.js,docs/readme.md").` (CLI: `-f, --files <paths>`)
* `customContext`: `Additional custom context text to include in the research.` (CLI: `-c, --context <text>`)
* `includeProjectTree`: `Include project file tree structure in context (default: false).` (CLI: `--tree`)
* `detailLevel`: `Detail level for the research response: 'low', 'medium', 'high' (default: medium).` (CLI: `--detail <level>`)
* `saveTo`: `Task or subtask ID (e.g., "15", "15.2") to automatically save the research conversation to.` (CLI: `--save-to <id>`)
* `saveFile`: `If true, saves the research conversation to a markdown file in '.taskmaster/docs/research/'.` (CLI: `--save-file`)
* `noFollowup`: `Disables the interactive follow-up question menu in the CLI.` (CLI: `--no-followup`)
* `tag`: `Specify which tag context to use for task-based context gathering. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `projectRoot`: `The directory of the project. Must be an absolute path.` (CLI: Determined automatically)
* **Usage:** **This is a POWERFUL tool that agents should use FREQUENTLY** to:
* Get fresh information beyond knowledge cutoff dates
* Research latest best practices, library updates, security patches
* Find implementation examples for specific technologies
* Validate approaches against current industry standards
* Get contextual advice based on project files and tasks
* **When to Consider Using Research:**
* **Before implementing any task** - Research current best practices
* **When encountering new technologies** - Get up-to-date implementation guidance (libraries, apis, etc)
* **For security-related tasks** - Find latest security recommendations
* **When updating dependencies** - Research breaking changes and migration guides
* **For performance optimization** - Get current performance best practices
* **When debugging complex issues** - Research known solutions and workarounds
* **Research + Action Pattern:**
* Use `research` to gather fresh information
* Use `update_subtask` to commit findings with timestamps
* Use `update_task` to incorporate research into task details
* Use `add_task` with research flag for informed task creation
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. The research provides FRESH data beyond the AI's training cutoff, making it invaluable for current best practices and recent developments.
---
## Tag Management
This new suite of commands allows you to manage different task contexts (tags).
### 26. List Tags (`tags`)
* **MCP Tool:** `list_tags`
* **CLI Command:** `task-master tags [options]`
* **Description:** `List all available tags with task counts, completion status, and other metadata.`
* **Key Parameters/Options:**
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* `--show-metadata`: `Include detailed metadata in the output (e.g., creation date, description).` (CLI: `--show-metadata`)
### 27. Add Tag (`add_tag`)
* **MCP Tool:** `add_tag`
* **CLI Command:** `task-master add-tag <tagName> [options]`
* **Description:** `Create a new, empty tag context, or copy tasks from another tag.`
* **Key Parameters/Options:**
* `tagName`: `Name of the new tag to create (alphanumeric, hyphens, underscores).` (CLI: `<tagName>` positional)
* `--from-branch`: `Creates a tag with a name derived from the current git branch, ignoring the <tagName> argument.` (CLI: `--from-branch`)
* `--copy-from-current`: `Copy tasks from the currently active tag to the new tag.` (CLI: `--copy-from-current`)
* `--copy-from <tag>`: `Copy tasks from a specific source tag to the new tag.` (CLI: `--copy-from <tag>`)
* `--description <text>`: `Provide an optional description for the new tag.` (CLI: `-d, --description <text>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
### 28. Delete Tag (`delete_tag`)
* **MCP Tool:** `delete_tag`
* **CLI Command:** `task-master delete-tag <tagName> [options]`
* **Description:** `Permanently delete a tag and all of its associated tasks.`
* **Key Parameters/Options:**
* `tagName`: `Name of the tag to delete.` (CLI: `<tagName>` positional)
* `--yes`: `Skip the confirmation prompt.` (CLI: `-y, --yes`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
### 29. Use Tag (`use_tag`)
* **MCP Tool:** `use_tag`
* **CLI Command:** `task-master use-tag <tagName>`
* **Description:** `Switch your active task context to a different tag.`
* **Key Parameters/Options:**
* `tagName`: `Name of the tag to switch to.` (CLI: `<tagName>` positional)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
### 30. Rename Tag (`rename_tag`)
* **MCP Tool:** `rename_tag`
* **CLI Command:** `task-master rename-tag <oldName> <newName>`
* **Description:** `Rename an existing tag.`
* **Key Parameters/Options:**
* `oldName`: `The current name of the tag.` (CLI: `<oldName>` positional)
* `newName`: `The new name for the tag.` (CLI: `<newName>` positional)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
### 31. Copy Tag (`copy_tag`)
* **MCP Tool:** `copy_tag`
* **CLI Command:** `task-master copy-tag <sourceName> <targetName> [options]`
* **Description:** `Copy an entire tag context, including all its tasks and metadata, to a new tag.`
* **Key Parameters/Options:**
* `sourceName`: `Name of the tag to copy from.` (CLI: `<sourceName>` positional)
* `targetName`: `Name of the new tag to create.` (CLI: `<targetName>` positional)
* `--description <text>`: `Optional description for the new tag.` (CLI: `-d, --description <text>`)
---
## Miscellaneous
### 32. Sync Readme (`sync-readme`) -- experimental
* **MCP Tool:** N/A
* **CLI Command:** `task-master sync-readme [options]`
* **Description:** `Exports your task list to your project's README.md file, useful for showcasing progress.`
* **Key Parameters/Options:**
* `status`: `Filter tasks by status (e.g., 'pending', 'done').` (CLI: `-s, --status <status>`)
* `withSubtasks`: `Include subtasks in the export.` (CLI: `--with-subtasks`)
* `tag`: `Specify which tag context to export from. Defaults to the current active tag.` (CLI: `--tag <name>`)
---
## Environment Variables Configuration (Updated)
Taskmaster primarily uses the **`.taskmaster/config.json`** file (in project root) for configuration (models, parameters, logging level, etc.), managed via `task-master models --setup`.
Environment variables are used **only** for sensitive API keys related to AI providers and specific overrides like the Ollama base URL:
* **API Keys (Required for corresponding provider):**
* `ANTHROPIC_API_KEY`
* `PERPLEXITY_API_KEY`
* `OPENAI_API_KEY`
* `GOOGLE_API_KEY`
* `MISTRAL_API_KEY`
* `AZURE_OPENAI_API_KEY` (Requires `AZURE_OPENAI_ENDPOINT` too)
* `OPENROUTER_API_KEY`
* `XAI_API_KEY`
* `OLLAMA_API_KEY` (Requires `OLLAMA_BASE_URL` too)
* **Endpoints (Optional/Provider Specific inside .taskmaster/config.json):**
* `AZURE_OPENAI_ENDPOINT`
* `OLLAMA_BASE_URL` (Default: `http://localhost:11434/api`)
**Set API keys** in your **`.env`** file in the project root (for CLI use) or within the `env` section of your **`.cursor/mcp.json`** file (for MCP/Cursor integration). All other settings (model choice, max tokens, temperature, log level, custom endpoints) are managed in `.taskmaster/config.json` via `task-master models` command or `models` MCP tool.
---
For details on how these commands fit into the development process, see the [dev_workflow.mdc](mdc:.cursor/rules/taskmaster/dev_workflow.mdc).

6
.gitignore vendored
View File

@@ -87,3 +87,9 @@ dev-debug.log
*.njsproj
*.sln
*.sw?
# OS specific
# Task files
# tasks.json
# tasks/

22
package-lock.json generated
View File

@@ -69,6 +69,7 @@
"ink": "^5.0.1",
"jest": "^29.7.0",
"jest-environment-node": "^29.7.0",
"mcp-jest": "^1.0.10",
"mock-fs": "^5.5.0",
"prettier": "^3.5.3",
"react": "^18.3.1",
@@ -10726,6 +10727,27 @@
"node": ">= 0.4"
}
},
"node_modules/mcp-jest": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/mcp-jest/-/mcp-jest-1.0.10.tgz",
"integrity": "sha512-gmvWzgj+p789Hofeuej60qBDfHTFn98aNfpgb+Q7a69vLSLvXBXDv2pcjYOLEuBvss/AGe26xq0WHbbX01X5AA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"zod": "^3.22.0"
},
"bin": {
"mcp-jest": "dist/cli.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/josharsh"
}
},
"node_modules/mcp-proxy": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-5.3.0.tgz",

View File

@@ -95,8 +95,8 @@
},
"optionalDependencies": {
"@anthropic-ai/claude-code": "^1.0.25",
"ai-sdk-provider-gemini-cli": "^0.0.4",
"@biomejs/cli-linux-x64": "^1.9.4"
"@biomejs/cli-linux-x64": "^1.9.4",
"ai-sdk-provider-gemini-cli": "^0.0.4"
},
"engines": {
"node": ">=18.0.0"
@@ -132,6 +132,7 @@
"ink": "^5.0.1",
"jest": "^29.7.0",
"jest-environment-node": "^29.7.0",
"mcp-jest": "^1.0.10",
"mock-fs": "^5.5.0",
"prettier": "^3.5.3",
"react": "^18.3.1",

View File

@@ -3,10 +3,7 @@
const { spawn } = require('child_process');
const path = require('path');
const args = [
'--config', 'jest.e2e.config.js',
...process.argv.slice(2)
];
const args = ['--config', 'jest.e2e.config.js', ...process.argv.slice(2)];
const jest = spawn('jest', args, {
cwd: path.join(__dirname, '../..'),
@@ -16,4 +13,4 @@ const jest = spawn('jest', args, {
jest.on('exit', (code) => {
process.exit(code);
});
});

View File

@@ -9,34 +9,37 @@ const { join } = require('path');
module.exports = async () => {
console.log('\n🚀 Setting up E2E test environment...\n');
try {
// Ensure task-master is linked globally
const projectRoot = join(__dirname, '../../..');
console.log('📦 Linking task-master globally...');
execSync('npm link', {
execSync('npm link', {
cwd: projectRoot,
stdio: 'inherit'
});
// Verify .env file exists
const envPath = join(projectRoot, '.env');
if (!existsSync(envPath)) {
console.warn('⚠️ Warning: .env file not found. Some tests may fail without API keys.');
console.warn(
'⚠️ Warning: .env file not found. Some tests may fail without API keys.'
);
} else {
console.log('✅ .env file found');
}
// Verify task-master command is available
try {
execSync('task-master --version', { stdio: 'pipe' });
console.log('✅ task-master command is available\n');
} catch (error) {
throw new Error('task-master command not found. Please ensure npm link succeeded.');
throw new Error(
'task-master command not found. Please ensure npm link succeeded.'
);
}
} catch (error) {
console.error('❌ Global setup failed:', error.message);
throw error;
}
};
};

View File

@@ -5,7 +5,7 @@
module.exports = async () => {
console.log('\n🧹 Cleaning up E2E test environment...\n');
// Any global cleanup needed
// Note: Individual test directories are cleaned up in afterEach hooks
};
};

View File

@@ -14,7 +14,7 @@ expect.extend({
toContainTaskId(received) {
const taskIdRegex = /#?\d+/;
const pass = taskIdRegex.test(received);
if (pass) {
return {
message: () => `expected ${received} not to contain a task ID`,
@@ -27,10 +27,10 @@ expect.extend({
};
}
},
toHaveExitCode(received, expected) {
const pass = received.exitCode === expected;
if (pass) {
return {
message: () => `expected exit code not to be ${expected}`,
@@ -38,16 +38,17 @@ expect.extend({
};
} else {
return {
message: () => `expected exit code ${expected} but got ${received.exitCode}\nstderr: ${received.stderr}`,
message: () =>
`expected exit code ${expected} but got ${received.exitCode}\nstderr: ${received.stderr}`,
pass: false
};
}
},
toContainInOutput(received, expected) {
const output = (received.stdout || '') + (received.stderr || '');
const pass = output.includes(expected);
if (pass) {
return {
message: () => `expected output not to contain "${expected}"`,
@@ -55,7 +56,8 @@ expect.extend({
};
} else {
return {
message: () => `expected output to contain "${expected}"\nstdout: ${received.stdout}\nstderr: ${received.stderr}`,
message: () =>
`expected output to contain "${expected}"\nstdout: ${received.stdout}\nstderr: ${received.stderr}`,
pass: false
};
}
@@ -76,5 +78,5 @@ global.createTestContext = (testName) => {
// Clean up any hanging processes
afterAll(async () => {
// Give time for any async operations to complete
await new Promise(resolve => setTimeout(resolve, 100));
});
await new Promise((resolve) => setTimeout(resolve, 100));
});

View File

@@ -0,0 +1,526 @@
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { mkdtempSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { rmSync, existsSync, readFileSync } from 'fs';
describe('task-master add-dependency', () => {
let testDir;
let helpers;
beforeEach(() => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'tm-test-add-dep-'));
process.chdir(testDir);
// Get helpers from global context
helpers = global.testHelpers;
// Copy .env if exists
const envPath = join(process.cwd(), '../../.env');
if (existsSync(envPath)) {
const envContent = readFileSync(envPath, 'utf-8');
helpers.writeFile('.env', envContent);
}
// Initialize task-master project
const initResult = helpers.taskMaster('init', ['-y']);
expect(initResult).toHaveExitCode(0);
// Ensure tasks.json exists
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (!helpers.fileExists(tasksPath)) {
helpers.writeFile(tasksPath, JSON.stringify({ tasks: [] }, null, 2));
}
});
afterEach(() => {
// Clean up test directory
process.chdir('..');
rmSync(testDir, { recursive: true, force: true });
});
describe('Basic dependency creation', () => {
it('should add a single dependency to a task', () => {
// Create tasks
const dep = helpers.taskMaster('add-task', ['Dependency task', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
const task = helpers.taskMaster('add-task', ['Main task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
// Add dependency
const result = helpers.taskMaster('add-dependency', [taskId, depId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Dependency added successfully');
expect(result.stdout).toContain(`${taskId} now depends on ${depId}`);
// Verify dependency was added
const showResult = helpers.taskMaster('show', [taskId]);
expect(showResult.stdout).toContain('Dependencies:');
expect(showResult.stdout).toContain(`${depId} - Dependency task`);
});
it('should add multiple dependencies at once', () => {
// Create dependency tasks
const dep1 = helpers.taskMaster('add-task', ['First dependency', '-m']);
const depId1 = helpers.extractTaskId(dep1.stdout);
const dep2 = helpers.taskMaster('add-task', ['Second dependency', '-m']);
const depId2 = helpers.extractTaskId(dep2.stdout);
const dep3 = helpers.taskMaster('add-task', ['Third dependency', '-m']);
const depId3 = helpers.extractTaskId(dep3.stdout);
const task = helpers.taskMaster('add-task', ['Main task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
// Add multiple dependencies
const result = helpers.taskMaster('add-dependency', [
taskId,
`${depId1},${depId2},${depId3}`
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('3 dependencies added');
// Verify all dependencies were added
const showResult = helpers.taskMaster('show', [taskId]);
expect(showResult.stdout).toContain(depId1);
expect(showResult.stdout).toContain(depId2);
expect(showResult.stdout).toContain(depId3);
});
});
describe('Dependency validation', () => {
it('should prevent circular dependencies', () => {
// Create circular dependency chain
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m']);
const id2 = helpers.extractTaskId(task2.stdout);
// Add first dependency
helpers.taskMaster('add-dependency', [id2, id1]);
// Try to create circular dependency
const result = helpers.taskMaster('add-dependency', [id1, id2], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('circular dependency');
});
it('should prevent self-dependencies', () => {
const task = helpers.taskMaster('add-task', ['Task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
const result = helpers.taskMaster('add-dependency', [taskId, taskId], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('cannot depend on itself');
});
it('should detect transitive circular dependencies', () => {
// Create chain: A -> B -> C, then try C -> A
const taskA = helpers.taskMaster('add-task', ['Task A', '-m']);
const idA = helpers.extractTaskId(taskA.stdout);
const taskB = helpers.taskMaster('add-task', ['Task B', '-m']);
const idB = helpers.extractTaskId(taskB.stdout);
const taskC = helpers.taskMaster('add-task', ['Task C', '-m']);
const idC = helpers.extractTaskId(taskC.stdout);
// Create chain
helpers.taskMaster('add-dependency', [idB, idA]);
helpers.taskMaster('add-dependency', [idC, idB]);
// Try to create circular dependency
const result = helpers.taskMaster('add-dependency', [idA, idC], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('circular dependency');
});
it('should prevent duplicate dependencies', () => {
const dep = helpers.taskMaster('add-task', ['Dependency', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
const task = helpers.taskMaster('add-task', ['Task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
// Add dependency first time
helpers.taskMaster('add-dependency', [taskId, depId]);
// Try to add same dependency again
const result = helpers.taskMaster('add-dependency', [taskId, depId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('already depends on');
expect(result.stdout).toContain('No changes made');
});
});
describe('Status updates', () => {
it('should update task status to blocked when adding dependencies', () => {
const dep = helpers.taskMaster('add-task', [
'Incomplete dependency',
'-m'
]);
const depId = helpers.extractTaskId(dep.stdout);
const task = helpers.taskMaster('add-task', ['Task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
// Start the task
helpers.taskMaster('set-status', [taskId, 'in-progress']);
// Add dependency (should change status to blocked)
const result = helpers.taskMaster('add-dependency', [taskId, depId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Status changed to: blocked');
// Verify status
const showResult = helpers.taskMaster('show', [taskId]);
expect(showResult.stdout).toContain('Status: blocked');
});
it('should not change status if all dependencies are complete', () => {
const dep = helpers.taskMaster('add-task', ['Complete dependency', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
helpers.taskMaster('set-status', [depId, 'done']);
const task = helpers.taskMaster('add-task', ['Task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
helpers.taskMaster('set-status', [taskId, 'in-progress']);
// Add completed dependency
const result = helpers.taskMaster('add-dependency', [taskId, depId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).not.toContain('Status changed');
// Status should remain in-progress
const showResult = helpers.taskMaster('show', [taskId]);
expect(showResult.stdout).toContain('Status: in-progress');
});
});
describe('Subtask dependencies', () => {
it('should add dependency to a subtask', () => {
// Create parent and dependency
const parent = helpers.taskMaster('add-task', ['Parent task', '-m']);
const parentId = helpers.extractTaskId(parent.stdout);
const dep = helpers.taskMaster('add-task', ['Dependency', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
// Expand parent
helpers.taskMaster('expand', ['-i', parentId, '-n', '2'], {
timeout: 60000
});
// Add dependency to subtask
const subtaskId = `${parentId}.1`;
const result = helpers.taskMaster('add-dependency', [subtaskId, depId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain(`${subtaskId} now depends on ${depId}`);
});
it('should allow subtask to depend on another subtask', () => {
// Create parent task
const parent = helpers.taskMaster('add-task', ['Parent', '-m']);
const parentId = helpers.extractTaskId(parent.stdout);
// Expand to create subtasks
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
timeout: 60000
});
// Make subtask 2 depend on subtask 1
const result = helpers.taskMaster('add-dependency', [
`${parentId}.2`,
`${parentId}.1`
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Dependency added successfully');
});
it('should prevent parent depending on its own subtask', () => {
const parent = helpers.taskMaster('add-task', ['Parent', '-m']);
const parentId = helpers.extractTaskId(parent.stdout);
helpers.taskMaster('expand', ['-i', parentId, '-n', '2'], {
timeout: 60000
});
const result = helpers.taskMaster(
'add-dependency',
[parentId, `${parentId}.1`],
{ allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('cannot depend on its own subtask');
});
});
describe('Bulk operations', () => {
it('should add dependencies to multiple tasks', () => {
// Create dependency
const dep = helpers.taskMaster('add-task', ['Shared dependency', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
// Create multiple tasks
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m']);
const id2 = helpers.extractTaskId(task2.stdout);
const task3 = helpers.taskMaster('add-task', ['Task 3', '-m']);
const id3 = helpers.extractTaskId(task3.stdout);
// Add dependency to all tasks
const result = helpers.taskMaster('add-dependency', [
`${id1},${id2},${id3}`,
depId
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('3 tasks updated');
// Verify all have the dependency
for (const id of [id1, id2, id3]) {
const showResult = helpers.taskMaster('show', [id]);
expect(showResult.stdout).toContain(depId);
}
});
it('should add dependencies by range', () => {
// Create dependency
const dep = helpers.taskMaster('add-task', ['Dependency', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
// Create sequential tasks
const ids = [];
for (let i = 0; i < 5; i++) {
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
ids.push(helpers.extractTaskId(result.stdout));
}
// Add dependency to range
const result = helpers.taskMaster('add-dependency', [
'--from',
ids[1],
'--to',
ids[3],
depId
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('3 tasks updated');
// Verify middle tasks have dependency
for (let i = 1; i <= 3; i++) {
const showResult = helpers.taskMaster('show', [ids[i]]);
expect(showResult.stdout).toContain(depId);
}
// Verify edge tasks don't have dependency
const show0 = helpers.taskMaster('show', [ids[0]]);
expect(show0.stdout).not.toContain(`Dependencies:.*${depId}`);
});
});
describe('Complex dependency graphs', () => {
it('should handle diamond dependency pattern', () => {
// Create diamond: A depends on B and C, both B and C depend on D
const taskD = helpers.taskMaster('add-task', ['Task D (base)', '-m']);
const idD = helpers.extractTaskId(taskD.stdout);
const taskB = helpers.taskMaster('add-task', ['Task B', '-m']);
const idB = helpers.extractTaskId(taskB.stdout);
helpers.taskMaster('add-dependency', [idB, idD]);
const taskC = helpers.taskMaster('add-task', ['Task C', '-m']);
const idC = helpers.extractTaskId(taskC.stdout);
helpers.taskMaster('add-dependency', [idC, idD]);
const taskA = helpers.taskMaster('add-task', ['Task A (top)', '-m']);
const idA = helpers.extractTaskId(taskA.stdout);
// Add both dependencies to create diamond
const result = helpers.taskMaster('add-dependency', [
idA,
`${idB},${idC}`
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('2 dependencies added');
// Verify the structure
const showResult = helpers.taskMaster('show', [idA]);
expect(showResult.stdout).toContain(idB);
expect(showResult.stdout).toContain(idC);
});
it('should show transitive dependencies', () => {
// Create chain A -> B -> C -> D
const taskD = helpers.taskMaster('add-task', ['Task D', '-m']);
const idD = helpers.extractTaskId(taskD.stdout);
const taskC = helpers.taskMaster('add-task', ['Task C', '-m']);
const idC = helpers.extractTaskId(taskC.stdout);
helpers.taskMaster('add-dependency', [idC, idD]);
const taskB = helpers.taskMaster('add-task', ['Task B', '-m']);
const idB = helpers.extractTaskId(taskB.stdout);
helpers.taskMaster('add-dependency', [idB, idC]);
const taskA = helpers.taskMaster('add-task', ['Task A', '-m']);
const idA = helpers.extractTaskId(taskA.stdout);
helpers.taskMaster('add-dependency', [idA, idB]);
// Show should indicate full dependency chain
const result = helpers.taskMaster('show', [idA]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Dependencies:');
expect(result.stdout).toContain(idB);
// May also show transitive dependencies in some views
});
});
describe('Tag context', () => {
it('should add dependencies within a tag', () => {
// Create tag
helpers.taskMaster('add-tag', ['feature']);
helpers.taskMaster('use-tag', ['feature']);
// Create tasks in feature tag
const dep = helpers.taskMaster('add-task', ['Feature dependency', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
const task = helpers.taskMaster('add-task', ['Feature task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
// Add dependency with tag context
const result = helpers.taskMaster('add-dependency', [
taskId,
depId,
'--tag',
'feature'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('[feature]');
});
it('should prevent cross-tag dependencies by default', () => {
// Create tasks in different tags
const masterTask = helpers.taskMaster('add-task', ['Master task', '-m']);
const masterId = helpers.extractTaskId(masterTask.stdout);
helpers.taskMaster('add-tag', ['feature']);
helpers.taskMaster('use-tag', ['feature']);
const featureTask = helpers.taskMaster('add-task', [
'Feature task',
'-m'
]);
const featureId = helpers.extractTaskId(featureTask.stdout);
// Try to add cross-tag dependency
const result = helpers.taskMaster(
'add-dependency',
[featureId, masterId, '--tag', 'feature'],
{ allowFailure: true }
);
// Depending on implementation, this might warn or fail
});
});
describe('Error handling', () => {
it('should handle non-existent task IDs', () => {
const task = helpers.taskMaster('add-task', ['Task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
const result = helpers.taskMaster('add-dependency', [taskId, '999'], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toMatch(/Task.*999.*not found/i);
});
it('should handle invalid task ID format', () => {
const result = helpers.taskMaster('add-dependency', ['invalid-id', '1'], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid task ID');
});
it('should require both task and dependency IDs', () => {
const result = helpers.taskMaster('add-dependency', ['1'], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('required');
});
});
describe('Output options', () => {
it('should support quiet mode', () => {
const dep = helpers.taskMaster('add-task', ['Dep', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
const task = helpers.taskMaster('add-task', ['Task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
const result = helpers.taskMaster('add-dependency', [
taskId,
depId,
'-q'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout.split('\n').length).toBeLessThan(3);
});
it('should support JSON output', () => {
const dep = helpers.taskMaster('add-task', ['Dep', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
const task = helpers.taskMaster('add-task', ['Task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
const result = helpers.taskMaster('add-dependency', [
taskId,
depId,
'--json'
]);
expect(result).toHaveExitCode(0);
const json = JSON.parse(result.stdout);
expect(json.task.id).toBe(parseInt(taskId));
expect(json.task.dependencies).toContain(parseInt(depId));
expect(json.added).toBe(1);
});
});
describe('Visualization', () => {
it('should show dependency graph after adding', () => {
// Create simple dependency chain
const task1 = helpers.taskMaster('add-task', ['Base task', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', ['Middle task', '-m']);
const id2 = helpers.extractTaskId(task2.stdout);
const task3 = helpers.taskMaster('add-task', ['Top task', '-m']);
const id3 = helpers.extractTaskId(task3.stdout);
// Build chain
helpers.taskMaster('add-dependency', [id2, id1]);
const result = helpers.taskMaster('add-dependency', [id3, id2]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Dependency chain:');
expect(result.stdout).toMatch(/→|depends on/);
});
});
});

View File

@@ -3,7 +3,14 @@
* Tests all aspects of task creation including AI and manual modes
*/
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
const path = require('path');
@@ -15,11 +22,11 @@ describe('add-task command', () => {
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-add-task-'));
// Initialize test helpers
const context = global.createTestContext('add-task');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
@@ -27,11 +34,13 @@ describe('add-task command', () => {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
// Ensure tasks.json exists (bug workaround)
const tasksJsonPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (!existsSync(tasksJsonPath)) {
@@ -54,13 +63,15 @@ describe('add-task command', () => {
['--prompt', 'Create a user authentication system with JWT tokens'],
{ cwd: testDir, timeout: 30000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContainTaskId();
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
// AI generated task should contain a title and description
expect(showResult.stdout).toContain('Title:');
expect(showResult.stdout).toContain('Description:');
@@ -68,25 +79,28 @@ describe('add-task command', () => {
}, 45000); // 45 second timeout for this test
it('should handle very long prompts', async () => {
const longPrompt = 'Create a comprehensive system that ' + 'handles many features '.repeat(50);
const longPrompt =
'Create a comprehensive system that ' +
'handles many features '.repeat(50);
const result = await helpers.taskMaster(
'add-task',
['--prompt', longPrompt],
{ cwd: testDir, timeout: 30000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContainTaskId();
}, 45000);
it('should handle special characters in prompt', async () => {
const specialPrompt = 'Implement feature: User data and settings with special chars';
const specialPrompt =
'Implement feature: User data and settings with special chars';
const result = await helpers.taskMaster(
'add-task',
['--prompt', specialPrompt],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContainTaskId();
});
@@ -94,14 +108,19 @@ describe('add-task command', () => {
it('should verify AI generates reasonable output', async () => {
const result = await helpers.taskMaster(
'add-task',
['--prompt', 'Build a responsive navigation menu with dropdown support'],
[
'--prompt',
'Build a responsive navigation menu with dropdown support'
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
// Verify AI generated task has proper structure
expect(showResult.stdout).toContain('Title:');
expect(showResult.stdout).toContain('Status:');
@@ -115,52 +134,65 @@ describe('add-task command', () => {
const result = await helpers.taskMaster(
'add-task',
[
'--title', 'Setup database connection',
'--description', 'Configure PostgreSQL connection with connection pooling'
'--title',
'Setup database connection',
'--description',
'Configure PostgreSQL connection with connection pooling'
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContainTaskId();
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
// Check that at least part of our title and description are shown
expect(showResult.stdout).toContain('Setup');
expect(showResult.stdout).toContain('Configure');
});
it('should create task with manual details', async () => {
const result = await helpers.taskMaster(
'add-task',
[
'--title', 'Implement caching layer',
'--description', 'Add Redis caching to improve performance',
'--details', 'Use Redis for session storage and API response caching'
'--title',
'Implement caching layer',
'--description',
'Add Redis caching to improve performance',
'--details',
'Use Redis for session storage and API response caching'
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContainTaskId();
});
});
describe('Task creation with options', () => {
it('should create task with priority', async () => {
const result = await helpers.taskMaster(
'add-task',
['--prompt', 'Fix critical security vulnerability', '--priority', 'high'],
[
'--prompt',
'Fix critical security vulnerability',
'--priority',
'high'
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('high');
});
@@ -168,22 +200,29 @@ describe('add-task command', () => {
// Create dependency task first
const depResult = await helpers.taskMaster(
'add-task',
['--title', 'Setup environment', '--description', 'Initial environment setup'],
[
'--title',
'Setup environment',
'--description',
'Initial environment setup'
],
{ cwd: testDir }
);
const depTaskId = helpers.extractTaskId(depResult.stdout);
// Create task with dependency
const result = await helpers.taskMaster(
'add-task',
['--prompt', 'Deploy application', '--dependencies', depTaskId],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain(depTaskId);
});
@@ -195,25 +234,32 @@ describe('add-task command', () => {
{ cwd: testDir }
);
const depId1 = helpers.extractTaskId(dep1.stdout);
const dep2 = await helpers.taskMaster(
'add-task',
['--prompt', 'Configure database'],
{ cwd: testDir }
);
const depId2 = helpers.extractTaskId(dep2.stdout);
// Create task with multiple dependencies
const result = await helpers.taskMaster(
'add-task',
['--prompt', 'Deploy application', '--dependencies', `${depId1},${depId2}`],
[
'--prompt',
'Deploy application',
'--dependencies',
`${depId1},${depId2}`
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain(depId1);
expect(showResult.stdout).toContain(depId2);
});
@@ -222,33 +268,43 @@ describe('add-task command', () => {
// Setup
const depResult = await helpers.taskMaster(
'add-task',
['--title', 'Prerequisite task', '--description', 'Task that must be completed first'],
[
'--title',
'Prerequisite task',
'--description',
'Task that must be completed first'
],
{ cwd: testDir }
);
const depTaskId = helpers.extractTaskId(depResult.stdout);
await helpers.taskMaster(
'add-tag',
['feature-complete', '--description', 'Complete feature test'],
{ cwd: testDir }
);
// Create task with all options
const result = await helpers.taskMaster(
'add-task',
[
'--prompt', 'Comprehensive task with all features',
'--priority', 'medium',
'--dependencies', depTaskId
'--prompt',
'Comprehensive task with all features',
'--priority',
'medium',
'--dependencies',
depTaskId
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const taskId = helpers.extractTaskId(result.stdout);
// Verify all options
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('medium');
expect(showResult.stdout).toContain(depTaskId);
});
@@ -256,23 +312,24 @@ describe('add-task command', () => {
describe('Error handling', () => {
it('should fail without prompt or title+description', async () => {
const result = await helpers.taskMaster(
'add-task',
[],
{ cwd: testDir, allowFailure: true }
);
const result = await helpers.taskMaster('add-task', [], {
cwd: testDir,
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Either --prompt or both --title and --description must be provided');
expect(result.stderr).toContain(
'Either --prompt or both --title and --description must be provided'
);
});
it('should fail with only title (missing description)', async () => {
const result = await helpers.taskMaster(
'add-task',
['--title', 'Incomplete task'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
});
@@ -282,15 +339,17 @@ describe('add-task command', () => {
['--prompt', 'Test task', '--priority', 'invalid'],
{ cwd: testDir }
);
// Should succeed but use default priority and show warning
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Invalid priority "invalid"');
expect(result.stdout).toContain('Using default priority "medium"');
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('Priority: │ medium');
});
@@ -301,7 +360,7 @@ describe('add-task command', () => {
['--prompt', 'Test task', '--dependencies', '99999'],
{ cwd: testDir }
);
// Should succeed but with warning
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('do not exist');
@@ -320,9 +379,9 @@ describe('add-task command', () => {
)
);
}
const results = await Promise.all(promises);
results.forEach((result) => {
expect(result).toHaveExitCode(0);
expect(result.stdout).toContainTaskId();
@@ -335,100 +394,116 @@ describe('add-task command', () => {
const result = await helpers.taskMaster(
'add-task',
[
'--prompt', 'Research best practices for implementing OAuth2 authentication',
'--prompt',
'Research best practices for implementing OAuth2 authentication',
'--research'
],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContainTaskId();
// Verify task was created
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
// Verify task was created with research mode (should have more detailed output)
expect(showResult.stdout).toContain('Title:');
expect(showResult.stdout).toContain('Implementation Details:');
}, 60000);
});
describe('File path handling', () => {
it('should use custom tasks file path', async () => {
// Create custom tasks file
const customPath = join(testDir, 'custom-tasks.json');
writeFileSync(customPath, JSON.stringify({ master: { tasks: [] } }));
const result = await helpers.taskMaster(
'add-task',
[
'--file', customPath,
'--prompt', 'Task in custom file'
],
['--file', customPath, '--prompt', 'Task in custom file'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify task was added to custom file
const customContent = JSON.parse(readFileSync(customPath, 'utf8'));
expect(customContent.master.tasks.length).toBe(1);
});
});
describe('Priority validation', () => {
it('should accept all valid priority values', async () => {
const priorities = ['high', 'medium', 'low'];
for (const priority of priorities) {
const result = await helpers.taskMaster(
'add-task',
['--prompt', `Task with ${priority} priority`, '--priority', priority],
[
'--prompt',
`Task with ${priority} priority`,
'--priority',
priority
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain(priority);
}
});
it('should accept priority values case-insensitively', async () => {
const priorities = ['HIGH', 'Medium', 'LoW'];
const expected = ['high', 'medium', 'low'];
for (let i = 0; i < priorities.length; i++) {
const result = await helpers.taskMaster(
'add-task',
['--prompt', `Task with ${priorities[i]} priority`, '--priority', priorities[i]],
[
'--prompt',
`Task with ${priorities[i]} priority`,
'--priority',
priorities[i]
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain(`Priority: │ ${expected[i]}`);
}
});
it('should default to medium priority when not specified', async () => {
const result = await helpers.taskMaster(
'add-task',
['--prompt', 'Task without explicit priority'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('medium');
});
});
describe('AI dependency suggestions', () => {
it('should let AI suggest dependencies based on context', async () => {
// Create some existing tasks that AI might reference
@@ -438,14 +513,14 @@ describe('add-task command', () => {
['--prompt', 'Setup authentication system'],
{ cwd: testDir }
);
// Create a task that should logically depend on auth
const result = await helpers.taskMaster(
'add-task',
['--prompt', 'Implement user profile page with authentication checks'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Check if AI suggested dependencies
if (result.stdout.includes('AI suggested')) {
@@ -453,25 +528,26 @@ describe('add-task command', () => {
}
}, 60000);
});
describe('Tag support', () => {
it('should add task to specific tag', async () => {
// Create a new tag
await helpers.taskMaster('add-tag', ['feature-branch', '--description', 'Feature branch tag'], { cwd: testDir });
await helpers.taskMaster(
'add-tag',
['feature-branch', '--description', 'Feature branch tag'],
{ cwd: testDir }
);
// Add task to specific tag
const result = await helpers.taskMaster(
'add-task',
[
'--prompt', 'Task for feature branch',
'--tag', 'feature-branch'
],
['--prompt', 'Task for feature branch', '--tag', 'feature-branch'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContainTaskId();
// Verify task is in the correct tag
const taskId = helpers.extractTaskId(result.stdout);
const showResult = await helpers.taskMaster(
@@ -481,50 +557,48 @@ describe('add-task command', () => {
);
expect(showResult).toHaveExitCode(0);
});
it('should add to master tag by default', async () => {
const result = await helpers.taskMaster(
'add-task',
['--prompt', 'Task for master tag'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify task is in master tag
const tasksContent = JSON.parse(readFileSync(join(testDir, '.taskmaster/tasks/tasks.json'), 'utf8'));
const tasksContent = JSON.parse(
readFileSync(join(testDir, '.taskmaster/tasks/tasks.json'), 'utf8')
);
expect(tasksContent.master.tasks.length).toBeGreaterThan(0);
});
});
describe('AI fallback behavior', () => {
it('should handle invalid model gracefully', async () => {
// Set an invalid model
await helpers.taskMaster(
'models',
['--set-main', 'invalid-model-xyz'],
{ cwd: testDir }
);
await helpers.taskMaster('models', ['--set-main', 'invalid-model-xyz'], {
cwd: testDir
});
const result = await helpers.taskMaster(
'add-task',
['--prompt', 'Test fallback behavior'],
{ cwd: testDir, allowFailure: true }
);
// Should either use fallback or fail gracefully
if (result.exitCode === 0) {
expect(result.stdout).toContainTaskId();
} else {
expect(result.stderr).toBeTruthy();
}
// Reset to valid model for other tests
await helpers.taskMaster(
'models',
['--set-main', 'gpt-3.5-turbo'],
{ cwd: testDir }
);
await helpers.taskMaster('models', ['--set-main', 'gpt-3.5-turbo'], {
cwd: testDir
});
});
});
});
});

View File

@@ -3,7 +3,14 @@
* Tests all aspects of complexity analysis including research mode and output formats
*/
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
const { execSync } = require('child_process');
@@ -17,12 +24,12 @@ describe('analyze-complexity command', () => {
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-analyze-complexity-'));
// Initialize test helpers
const context = global.createTestContext('analyze-complexity');
helpers = context.helpers;
logger = context.logger;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
@@ -30,14 +37,16 @@ describe('analyze-complexity command', () => {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
// Setup test tasks for analysis
taskIds = [];
// Create simple task
const simple = await helpers.taskMaster(
'add-task',
@@ -45,19 +54,22 @@ describe('analyze-complexity command', () => {
{ cwd: testDir }
);
taskIds.push(helpers.extractTaskId(simple.stdout));
// Create complex task with subtasks
const complex = await helpers.taskMaster(
'add-task',
['--prompt', 'Build a complete e-commerce platform with payment processing'],
[
'--prompt',
'Build a complete e-commerce platform with payment processing'
],
{ cwd: testDir }
);
const complexId = helpers.extractTaskId(complex.stdout);
taskIds.push(complexId);
// Expand complex task to add subtasks
await helpers.taskMaster('expand', [complexId], { cwd: testDir });
// Create task with dependencies
const withDeps = await helpers.taskMaster(
'add-task',
@@ -76,12 +88,10 @@ describe('analyze-complexity command', () => {
describe('Basic complexity analysis', () => {
it('should analyze complexity without flags', async () => {
const result = await helpers.taskMaster(
'analyze-complexity',
[],
{ cwd: testDir }
);
const result = await helpers.taskMaster('analyze-complexity', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toContain('complexity');
});
@@ -92,7 +102,7 @@ describe('analyze-complexity command', () => {
['--research'],
{ cwd: testDir, timeout: 120000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toContain('complexity');
}, 120000);
@@ -106,12 +116,12 @@ describe('analyze-complexity command', () => {
['--output', outputPath],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const fullPath = join(testDir, outputPath);
expect(existsSync(fullPath)).toBe(true);
// Verify it's valid JSON
const report = JSON.parse(readFileSync(fullPath, 'utf8'));
expect(report).toBeDefined();
@@ -124,15 +134,15 @@ describe('analyze-complexity command', () => {
['--format', 'json'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Output should be valid JSON
let parsed;
expect(() => {
parsed = JSON.parse(result.stdout);
}).not.toThrow();
expect(parsed).toBeDefined();
expect(typeof parsed).toBe('object');
});
@@ -143,13 +153,20 @@ describe('analyze-complexity command', () => {
['--detailed'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const output = result.stdout.toLowerCase();
const expectedDetails = ['subtasks', 'dependencies', 'description', 'metadata'];
const foundDetails = expectedDetails.filter(detail => output.includes(detail));
const expectedDetails = [
'subtasks',
'dependencies',
'description',
'metadata'
];
const foundDetails = expectedDetails.filter((detail) =>
output.includes(detail)
);
expect(foundDetails.length).toBeGreaterThanOrEqual(2);
});
});
@@ -161,11 +178,11 @@ describe('analyze-complexity command', () => {
['--tasks', taskIds.join(',')],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Should analyze only specified tasks
taskIds.forEach(taskId => {
taskIds.forEach((taskId) => {
expect(result.stdout).toContain(taskId);
});
});
@@ -179,27 +196,29 @@ describe('analyze-complexity command', () => {
{ cwd: testDir }
);
const taggedId = helpers.extractTaskId(taggedResult.stdout);
const result = await helpers.taskMaster(
'analyze-complexity',
['--tag', 'complex-tag'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain(taggedId);
});
it('should filter by status', async () => {
// Set one task to completed
await helpers.taskMaster('set-status', [taskIds[0], 'completed'], { cwd: testDir });
await helpers.taskMaster('set-status', [taskIds[0], 'completed'], {
cwd: testDir
});
const result = await helpers.taskMaster(
'analyze-complexity',
['--status', 'pending'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Should not include completed task
expect(result.stdout).not.toContain(taskIds[0]);
@@ -210,12 +229,19 @@ describe('analyze-complexity command', () => {
it('should use custom thresholds', async () => {
const result = await helpers.taskMaster(
'analyze-complexity',
['--low-threshold', '3', '--medium-threshold', '7', '--high-threshold', '10'],
[
'--low-threshold',
'3',
'--medium-threshold',
'7',
'--high-threshold',
'10'
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const output = result.stdout.toLowerCase();
expect(output).toContain('low');
expect(output).toContain('medium');
@@ -228,7 +254,7 @@ describe('analyze-complexity command', () => {
['--low-threshold', '-1'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
});
});
@@ -237,16 +263,14 @@ describe('analyze-complexity command', () => {
it('should handle empty project', async () => {
// Create a new temp directory
const emptyDir = mkdtempSync(join(tmpdir(), 'task-master-empty-'));
try {
await helpers.taskMaster('init', ['-y'], { cwd: emptyDir });
const result = await helpers.taskMaster(
'analyze-complexity',
[],
{ cwd: emptyDir }
);
const result = await helpers.taskMaster('analyze-complexity', [], {
cwd: emptyDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toMatch(/no tasks|0/);
} finally {
@@ -260,7 +284,7 @@ describe('analyze-complexity command', () => {
['--output', '/invalid/path/report.json'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
});
});
@@ -279,15 +303,13 @@ describe('analyze-complexity command', () => {
);
}
await Promise.all(promises);
const startTime = Date.now();
const result = await helpers.taskMaster(
'analyze-complexity',
[],
{ cwd: testDir }
);
const result = await helpers.taskMaster('analyze-complexity', [], {
cwd: testDir
});
const duration = Date.now() - startTime;
expect(result).toHaveExitCode(0);
expect(duration).toBeLessThan(10000); // Should complete in less than 10 seconds
});
@@ -300,13 +322,13 @@ describe('analyze-complexity command', () => {
['--format', 'json'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const analysis = JSON.parse(result.stdout);
const simpleTask = analysis.tasks?.find(t => t.id === taskIds[0]);
const complexTask = analysis.tasks?.find(t => t.id === taskIds[1]);
const simpleTask = analysis.tasks?.find((t) => t.id === taskIds[0]);
const complexTask = analysis.tasks?.find((t) => t.id === taskIds[1]);
expect(simpleTask).toBeDefined();
expect(complexTask).toBeDefined();
expect(complexTask.complexity).toBeGreaterThan(simpleTask.complexity);
@@ -321,15 +343,15 @@ describe('analyze-complexity command', () => {
['--output', '.taskmaster/reports/task-complexity-report.json'],
{ cwd: testDir }
);
const result = await helpers.taskMaster(
'complexity-report',
[],
{ cwd: testDir }
);
const result = await helpers.taskMaster('complexity-report', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toMatch(/complexity report|complexity/);
expect(result.stdout.toLowerCase()).toMatch(
/complexity report|complexity/
);
});
});
});
});

View File

@@ -3,7 +3,14 @@
* Tests all aspects of task expansion including single, multiple, and recursive expansion
*/
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
@@ -17,11 +24,11 @@ describe('expand-task command', () => {
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-expand-task-'));
// Initialize test helpers
const context = global.createTestContext('expand-task');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
@@ -29,18 +36,20 @@ describe('expand-task command', () => {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
// Ensure tasks.json exists (bug workaround)
const tasksJsonPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (!existsSync(tasksJsonPath)) {
mkdirSync(join(testDir, '.taskmaster/tasks'), { recursive: true });
writeFileSync(tasksJsonPath, JSON.stringify({ master: { tasks: [] } }));
}
// Create simple task for expansion
const simpleResult = await helpers.taskMaster(
'add-task',
@@ -48,19 +57,27 @@ describe('expand-task command', () => {
{ cwd: testDir }
);
simpleTaskId = helpers.extractTaskId(simpleResult.stdout);
// Create complex task for expansion
const complexResult = await helpers.taskMaster(
'add-task',
['--prompt', 'Build a full-stack web application with React frontend and Node.js backend'],
[
'--prompt',
'Build a full-stack web application with React frontend and Node.js backend'
],
{ cwd: testDir }
);
complexTaskId = helpers.extractTaskId(complexResult.stdout);
// Create manual task (no AI prompt)
const manualResult = await helpers.taskMaster(
'add-task',
['--title', 'Manual task for expansion', '--description', 'This is a manually created task'],
[
'--title',
'Manual task for expansion',
'--description',
'This is a manually created task'
],
{ cwd: testDir }
);
manualTaskId = helpers.extractTaskId(manualResult.stdout);
@@ -80,12 +97,14 @@ describe('expand-task command', () => {
['--id', simpleTaskId],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Expanded');
// Verify subtasks were created
const showResult = await helpers.taskMaster('show', [simpleTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [simpleTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('Subtasks:');
}, 60000);
@@ -95,11 +114,13 @@ describe('expand-task command', () => {
['--id', complexTaskId, '--num', '3'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Check that we got approximately 3 subtasks
const showResult = await helpers.taskMaster('show', [complexTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [complexTaskId], {
cwd: testDir
});
const subtaskMatches = showResult.stdout.match(/\d+\.\d+/g);
expect(subtaskMatches).toBeTruthy();
expect(subtaskMatches.length).toBeGreaterThanOrEqual(2);
@@ -112,7 +133,7 @@ describe('expand-task command', () => {
['--id', simpleTaskId, '--research'],
{ cwd: testDir, timeout: 60000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('research');
}, 90000);
@@ -120,14 +141,21 @@ describe('expand-task command', () => {
it('should expand with additional context', async () => {
const result = await helpers.taskMaster(
'expand',
['--id', manualTaskId, '--prompt', 'Focus on security best practices and testing'],
[
'--id',
manualTaskId,
'--prompt',
'Focus on security best practices and testing'
],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Verify context was used
const showResult = await helpers.taskMaster('show', [manualTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [manualTaskId], {
cwd: testDir
});
const outputLower = showResult.stdout.toLowerCase();
expect(outputLower).toMatch(/security|test/);
}, 60000);
@@ -135,39 +163,37 @@ describe('expand-task command', () => {
describe('Bulk expansion', () => {
it('should expand all tasks', async () => {
const result = await helpers.taskMaster(
'expand',
['--all'],
{ cwd: testDir, timeout: 120000 }
);
const result = await helpers.taskMaster('expand', ['--all'], {
cwd: testDir,
timeout: 120000
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Expanding all');
// Verify all tasks have subtasks
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasksData = JSON.parse(readFileSync(tasksPath, 'utf8'));
const tasks = tasksData.master.tasks;
const tasksWithSubtasks = tasks.filter(t => t.subtasks && t.subtasks.length > 0);
const tasksWithSubtasks = tasks.filter(
(t) => t.subtasks && t.subtasks.length > 0
);
expect(tasksWithSubtasks.length).toBeGreaterThanOrEqual(2);
}, 150000);
it('should expand all with force flag', async () => {
// First expand one task
await helpers.taskMaster(
'expand',
['--id', simpleTaskId],
{ cwd: testDir }
);
await helpers.taskMaster('expand', ['--id', simpleTaskId], {
cwd: testDir
});
// Then expand all with force
const result = await helpers.taskMaster(
'expand',
['--all', '--force'],
{ cwd: testDir, timeout: 120000 }
);
const result = await helpers.taskMaster('expand', ['--all', '--force'], {
cwd: testDir,
timeout: 120000
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('force');
}, 150000);
@@ -176,30 +202,32 @@ describe('expand-task command', () => {
describe('Specific task ranges', () => {
it('should expand tasks by ID range', async () => {
// Create more tasks
await helpers.taskMaster(
'add-task',
['--prompt', 'Additional task 1'],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-task',
['--prompt', 'Additional task 2'],
{ cwd: testDir }
);
await helpers.taskMaster('add-task', ['--prompt', 'Additional task 1'], {
cwd: testDir
});
await helpers.taskMaster('add-task', ['--prompt', 'Additional task 2'], {
cwd: testDir
});
const result = await helpers.taskMaster(
'expand',
['--from', '2', '--to', '4'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
// Verify tasks 2-4 were expanded
const showResult2 = await helpers.taskMaster('show', ['2'], { cwd: testDir });
const showResult3 = await helpers.taskMaster('show', ['3'], { cwd: testDir });
const showResult4 = await helpers.taskMaster('show', ['4'], { cwd: testDir });
const showResult2 = await helpers.taskMaster('show', ['2'], {
cwd: testDir
});
const showResult3 = await helpers.taskMaster('show', ['3'], {
cwd: testDir
});
const showResult4 = await helpers.taskMaster('show', ['4'], {
cwd: testDir
});
expect(showResult2.stdout).toContain('Subtasks:');
expect(showResult3.stdout).toContain('Subtasks:');
expect(showResult4.stdout).toContain('Subtasks:');
@@ -211,13 +239,17 @@ describe('expand-task command', () => {
['--id', `${simpleTaskId},${complexTaskId}`],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
// Both tasks should have subtasks
const showResult1 = await helpers.taskMaster('show', [simpleTaskId], { cwd: testDir });
const showResult2 = await helpers.taskMaster('show', [complexTaskId], { cwd: testDir });
const showResult1 = await helpers.taskMaster('show', [simpleTaskId], {
cwd: testDir
});
const showResult2 = await helpers.taskMaster('show', [complexTaskId], {
cwd: testDir
});
expect(showResult1.stdout).toContain('Subtasks:');
expect(showResult2.stdout).toContain('Subtasks:');
}, 120000);
@@ -225,31 +257,28 @@ describe('expand-task command', () => {
describe('Error handling', () => {
it('should fail for non-existent task ID', async () => {
const result = await helpers.taskMaster(
'expand',
['--id', '99999'],
{ cwd: testDir, allowFailure: true }
);
const result = await helpers.taskMaster('expand', ['--id', '99999'], {
cwd: testDir,
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('not found');
});
it('should skip already expanded tasks without force', async () => {
// First expansion
await helpers.taskMaster(
'expand',
['--id', simpleTaskId],
{ cwd: testDir }
);
await helpers.taskMaster('expand', ['--id', simpleTaskId], {
cwd: testDir
});
// Second expansion without force
const result = await helpers.taskMaster(
'expand',
['--id', simpleTaskId],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toMatch(/already|skip/);
});
@@ -260,7 +289,7 @@ describe('expand-task command', () => {
['--id', simpleTaskId, '--num', '-1'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
});
});
@@ -269,22 +298,22 @@ describe('expand-task command', () => {
it('should expand tasks in specific tag', async () => {
// Create tag and tagged task
await helpers.taskMaster('add-tag', ['feature-tag'], { cwd: testDir });
const taggedResult = await helpers.taskMaster(
'add-task',
['--prompt', 'Tagged task for expansion', '--tag', 'feature-tag'],
{ cwd: testDir }
);
const taggedId = helpers.extractTaskId(taggedResult.stdout);
const result = await helpers.taskMaster(
'expand',
['--id', taggedId, '--tag', 'feature-tag'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Verify expansion in correct tag
const showResult = await helpers.taskMaster(
'show',
@@ -302,27 +331,27 @@ describe('expand-task command', () => {
['--id', simpleTaskId, '--model', 'gpt-3.5-turbo'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
}, 60000);
});
describe('Output validation', () => {
it('should create valid subtask structure', async () => {
await helpers.taskMaster(
'expand',
['--id', complexTaskId],
{ cwd: testDir }
);
await helpers.taskMaster('expand', ['--id', complexTaskId], {
cwd: testDir
});
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasksData = JSON.parse(readFileSync(tasksPath, 'utf8'));
const task = tasksData.master.tasks.find(t => t.id === parseInt(complexTaskId));
const task = tasksData.master.tasks.find(
(t) => t.id === parseInt(complexTaskId)
);
expect(task.subtasks).toBeDefined();
expect(Array.isArray(task.subtasks)).toBe(true);
expect(task.subtasks.length).toBeGreaterThan(0);
// Validate subtask structure
task.subtasks.forEach((subtask, index) => {
expect(subtask.id).toBe(`${complexTaskId}.${index + 1}`);
@@ -340,17 +369,15 @@ describe('expand-task command', () => {
{ cwd: testDir }
);
const depTaskId = helpers.extractTaskId(depResult.stdout);
// Expand the task
await helpers.taskMaster(
'expand',
['--id', depTaskId],
{ cwd: testDir }
);
await helpers.taskMaster('expand', ['--id', depTaskId], { cwd: testDir });
// Check dependencies are preserved
const showResult = await helpers.taskMaster('show', [depTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [depTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain(`Dependencies: ${simpleTaskId}`);
});
});
});
});

View File

@@ -0,0 +1,804 @@
/**
* Comprehensive E2E tests for list command
* Tests all aspects of task listing including filtering and display options
*/
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
const path = require('path');
describe('list command', () => {
let testDir;
let helpers;
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-list-'));
// Initialize test helpers
const context = global.createTestContext('list');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
if (existsSync(mainEnvPath)) {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
// Ensure tasks.json exists (bug workaround)
const tasksJsonPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (!existsSync(tasksJsonPath)) {
mkdirSync(join(testDir, '.taskmaster/tasks'), { recursive: true });
writeFileSync(tasksJsonPath, JSON.stringify({ master: { tasks: [] } }));
}
});
afterEach(() => {
// Clean up test directory
if (testDir && existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
describe('Basic listing', () => {
it('should list all tasks', async () => {
// Create some test tasks
await helpers.taskMaster(
'add-task',
['--title', 'Task 1', '--description', 'First task'],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-task',
['--title', 'Task 2', '--description', 'Second task'],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Task 1');
expect(result.stdout).toContain('Task 2');
expect(result.stdout).toContain('Project Dashboard');
expect(result.stdout).toContain('ID');
expect(result.stdout).toContain('Title');
expect(result.stdout).toContain('Status');
expect(result.stdout).toContain('Priority');
expect(result.stdout).toContain('Dependencies');
});
it('should show empty list message when no tasks exist', async () => {
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('No tasks found');
});
it('should display task progress dashboard', async () => {
// Create tasks with different statuses
const task1 = await helpers.taskMaster(
'add-task',
['--title', 'Completed task', '--description', 'Done'],
{ cwd: testDir }
);
const taskId1 = helpers.extractTaskId(task1.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId1, '--status', 'done'],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-task',
['--title', 'In progress task', '--description', 'Working on it'],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Project Dashboard');
expect(result.stdout).toContain('Tasks Progress:');
expect(result.stdout).toContain('Done:');
expect(result.stdout).toContain('In Progress:');
expect(result.stdout).toContain('Pending:');
});
});
describe('Status filtering', () => {
beforeEach(async () => {
// Create tasks with different statuses
const task1 = await helpers.taskMaster(
'add-task',
['--title', 'Pending task', '--description', 'Not started'],
{ cwd: testDir }
);
const task2 = await helpers.taskMaster(
'add-task',
['--title', 'In progress task', '--description', 'Working on it'],
{ cwd: testDir }
);
const taskId2 = helpers.extractTaskId(task2.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId2, '--status', 'in-progress'],
{ cwd: testDir }
);
const task3 = await helpers.taskMaster(
'add-task',
['--title', 'Done task', '--description', 'Completed'],
{ cwd: testDir }
);
const taskId3 = helpers.extractTaskId(task3.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId3, '--status', 'done'],
{ cwd: testDir }
);
const task4 = await helpers.taskMaster(
'add-task',
['--title', 'Blocked task', '--description', 'Blocked by dependency'],
{ cwd: testDir }
);
const taskId4 = helpers.extractTaskId(task4.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId4, '--status', 'blocked'],
{ cwd: testDir }
);
const task5 = await helpers.taskMaster(
'add-task',
['--title', 'Deferred task', '--description', 'Postponed'],
{ cwd: testDir }
);
const taskId5 = helpers.extractTaskId(task5.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId5, '--status', 'deferred'],
{ cwd: testDir }
);
const task6 = await helpers.taskMaster(
'add-task',
['--title', 'Cancelled task', '--description', 'No longer needed'],
{ cwd: testDir }
);
const taskId6 = helpers.extractTaskId(task6.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId6, '--status', 'cancelled'],
{ cwd: testDir }
);
});
it('should filter by pending status', async () => {
const result = await helpers.taskMaster('list', ['--status', 'pending'], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Pending task');
expect(result.stdout).not.toContain('In progress task');
expect(result.stdout).not.toContain('Done task');
expect(result.stdout).toContain('Filtered by status: pending');
});
it('should filter by in-progress status', async () => {
const result = await helpers.taskMaster(
'list',
['--status', 'in-progress'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('In progress task');
expect(result.stdout).not.toContain('Pending task');
expect(result.stdout).not.toContain('Done task');
});
it('should filter by done status', async () => {
const result = await helpers.taskMaster('list', ['--status', 'done'], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Done task');
expect(result.stdout).not.toContain('Pending task');
expect(result.stdout).not.toContain('In progress task');
});
it('should filter by blocked status', async () => {
const result = await helpers.taskMaster('list', ['--status', 'blocked'], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Blocked task');
expect(result.stdout).not.toContain('Pending task');
});
it('should filter by deferred status', async () => {
const result = await helpers.taskMaster(
'list',
['--status', 'deferred'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Deferred task');
expect(result.stdout).not.toContain('Pending task');
});
it('should filter by cancelled status', async () => {
const result = await helpers.taskMaster(
'list',
['--status', 'cancelled'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Cancelled task');
expect(result.stdout).not.toContain('Pending task');
});
it('should handle multiple statuses with comma separation', async () => {
const result = await helpers.taskMaster(
'list',
['--status', 'pending,in-progress'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Pending task');
expect(result.stdout).toContain('In progress task');
expect(result.stdout).not.toContain('Done task');
expect(result.stdout).not.toContain('Blocked task');
});
it('should show empty message for non-existent status filter', async () => {
const result = await helpers.taskMaster(
'list',
['--status', 'invalid-status'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain(
"No tasks with status 'invalid-status' found"
);
});
});
describe('Priority display', () => {
it('should display task priorities correctly', async () => {
// Create tasks with different priorities
await helpers.taskMaster(
'add-task',
[
'--title',
'High priority task',
'--description',
'Urgent',
'--priority',
'high'
],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-task',
[
'--title',
'Medium priority task',
'--description',
'Normal',
'--priority',
'medium'
],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-task',
[
'--title',
'Low priority task',
'--description',
'Can wait',
'--priority',
'low'
],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toMatch(/high/i);
expect(result.stdout).toMatch(/medium/i);
expect(result.stdout).toMatch(/low/i);
// Check priority breakdown
expect(result.stdout).toContain('Priority Breakdown:');
expect(result.stdout).toContain('High priority:');
expect(result.stdout).toContain('Medium priority:');
expect(result.stdout).toContain('Low priority:');
});
});
describe('Subtasks display', () => {
let parentTaskId;
beforeEach(async () => {
// Create a parent task with subtasks
const parentResult = await helpers.taskMaster(
'add-task',
['--title', 'Parent task', '--description', 'Has subtasks'],
{ cwd: testDir }
);
parentTaskId = helpers.extractTaskId(parentResult.stdout);
// Add subtasks
await helpers.taskMaster(
'add-subtask',
[
'--parent',
parentTaskId,
'--title',
'Subtask 1',
'--description',
'First subtask'
],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-subtask',
[
'--parent',
parentTaskId,
'--title',
'Subtask 2',
'--description',
'Second subtask'
],
{ cwd: testDir }
);
});
it('should not show subtasks by default', async () => {
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Parent task');
expect(result.stdout).not.toContain('Subtask 1');
expect(result.stdout).not.toContain('Subtask 2');
});
it('should show subtasks with --with-subtasks flag', async () => {
const result = await helpers.taskMaster('list', ['--with-subtasks'], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Parent task');
expect(result.stdout).toContain('Subtask 1');
expect(result.stdout).toContain('Subtask 2');
expect(result.stdout).toContain(`${parentTaskId}.1`);
expect(result.stdout).toContain(`${parentTaskId}.2`);
expect(result.stdout).toContain('└─');
});
it('should include subtasks in progress calculation', async () => {
const result = await helpers.taskMaster('list', ['--with-subtasks'], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Subtasks Progress:');
expect(result.stdout).toMatch(/Completed:\s*0\/2/);
});
});
describe('Tag filtering', () => {
beforeEach(async () => {
// Create a new tag
await helpers.taskMaster(
'add-tag',
['feature-branch', '--description', 'Feature branch tasks'],
{ cwd: testDir }
);
// Add tasks to master tag
await helpers.taskMaster(
'add-task',
['--title', 'Master task 1', '--description', 'In master tag'],
{ cwd: testDir }
);
// Switch to feature tag and add tasks
await helpers.taskMaster('use-tag', ['feature-branch'], { cwd: testDir });
await helpers.taskMaster(
'add-task',
[
'--title',
'Feature task 1',
'--description',
'In feature tag',
'--tag',
'feature-branch'
],
{ cwd: testDir }
);
});
it('should list tasks from specific tag', async () => {
const result = await helpers.taskMaster(
'list',
['--tag', 'feature-branch'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Feature task 1');
expect(result.stdout).not.toContain('Master task 1');
expect(result.stdout).toContain('[feature-branch]');
});
it('should list tasks from master tag by default', async () => {
// Switch back to master tag
await helpers.taskMaster('use-tag', ['master'], { cwd: testDir });
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Master task 1');
expect(result.stdout).not.toContain('Feature task 1');
});
});
describe('Dependencies display', () => {
it('should show task dependencies correctly', async () => {
// Create dependency tasks
const dep1 = await helpers.taskMaster(
'add-task',
['--title', 'Dependency 1', '--description', 'First dependency'],
{ cwd: testDir }
);
const depId1 = helpers.extractTaskId(dep1.stdout);
const dep2 = await helpers.taskMaster(
'add-task',
['--title', 'Dependency 2', '--description', 'Second dependency'],
{ cwd: testDir }
);
const depId2 = helpers.extractTaskId(dep2.stdout);
// Create task with dependencies
await helpers.taskMaster(
'add-task',
[
'--title',
'Task with dependencies',
'--description',
'Depends on other tasks',
'--dependencies',
`${depId1},${depId2}`
],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain(depId1);
expect(result.stdout).toContain(depId2);
});
it('should show dependency status with colors', async () => {
// Create dependency task
const dep = await helpers.taskMaster(
'add-task',
['--title', 'Completed dependency', '--description', 'Done'],
{ cwd: testDir }
);
const depId = helpers.extractTaskId(dep.stdout);
// Mark dependency as done
await helpers.taskMaster(
'set-status',
['--id', depId, '--status', 'done'],
{ cwd: testDir }
);
// Create task with dependency
await helpers.taskMaster(
'add-task',
[
'--title',
'Task with completed dependency',
'--description',
'Has satisfied dependency',
'--dependencies',
depId
],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
// The done dependency should be shown (implementation uses color coding)
expect(result.stdout).toContain(depId);
});
it('should show dependency dashboard', async () => {
// Create some tasks with dependencies
const task1 = await helpers.taskMaster(
'add-task',
['--title', 'Independent task', '--description', 'No dependencies'],
{ cwd: testDir }
);
const task2 = await helpers.taskMaster(
'add-task',
['--title', 'Dependency task', '--description', 'Will be depended on'],
{ cwd: testDir }
);
const taskId2 = helpers.extractTaskId(task2.stdout);
await helpers.taskMaster(
'add-task',
[
'--title',
'Dependent task',
'--description',
'Depends on task 2',
'--dependencies',
taskId2
],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Dependency Status & Next Task');
expect(result.stdout).toContain('Tasks with no dependencies:');
expect(result.stdout).toContain('Tasks ready to work on:');
expect(result.stdout).toContain('Tasks blocked by dependencies:');
});
});
describe('Complexity display', () => {
it('should show complexity scores when available', async () => {
// Create tasks
await helpers.taskMaster(
'add-task',
['--prompt', 'Build a complex authentication system'],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-task',
['--prompt', 'Create a simple hello world endpoint'],
{ cwd: testDir }
);
// Run complexity analysis
const analyzeResult = await helpers.taskMaster('analyze-complexity', [], {
cwd: testDir,
timeout: 60000
});
if (analyzeResult.exitCode === 0) {
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Complexity');
}
});
});
describe('Next task recommendation', () => {
it('should show next task recommendation', async () => {
// Create tasks with different priorities and dependencies
const task1 = await helpers.taskMaster(
'add-task',
[
'--title',
'High priority task',
'--description',
'Should be done first',
'--priority',
'high'
],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Next Task to Work On');
expect(result.stdout).toContain('Start working:');
expect(result.stdout).toContain('task-master set-status');
expect(result.stdout).toContain('View details:');
expect(result.stdout).toContain('task-master show');
});
it('should show no eligible task when all are blocked', async () => {
// Create blocked task
const task1 = await helpers.taskMaster(
'add-task',
['--title', 'Prerequisite', '--description', 'Must be done first'],
{ cwd: testDir }
);
const taskId1 = helpers.extractTaskId(task1.stdout);
// Create task depending on it
await helpers.taskMaster(
'add-task',
[
'--title',
'Blocked task',
'--description',
'Waiting for prerequisite',
'--dependencies',
taskId1
],
{ cwd: testDir }
);
// Mark first task as done
await helpers.taskMaster(
'set-status',
['--id', taskId1, '--status', 'done'],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
// Should recommend the unblocked task
expect(result.stdout).toContain('Next Task to Work On');
expect(result.stdout).toContain('Blocked task');
});
});
describe('File path handling', () => {
it('should use custom tasks file path', async () => {
// Create custom tasks file
const customPath = join(testDir, 'custom-tasks.json');
writeFileSync(
customPath,
JSON.stringify({
master: {
tasks: [
{
id: 1,
title: 'Custom file task',
description: 'Task in custom file',
status: 'pending',
priority: 'medium',
dependencies: []
}
]
}
})
);
const result = await helpers.taskMaster('list', ['--file', customPath], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Custom file task');
expect(result.stdout).toContain(`Listing tasks from: ${customPath}`);
});
});
describe('Error handling', () => {
it('should handle missing tasks file gracefully', async () => {
const nonExistentPath = join(testDir, 'non-existent.json');
const result = await helpers.taskMaster(
'list',
['--file', nonExistentPath],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Error');
});
it('should handle invalid JSON in tasks file', async () => {
const invalidPath = join(testDir, 'invalid.json');
writeFileSync(invalidPath, '{ invalid json }');
const result = await helpers.taskMaster('list', ['--file', invalidPath], {
cwd: testDir,
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
});
});
describe('Performance with many tasks', () => {
it('should handle listing 50+ tasks efficiently', async () => {
// Create many tasks
const promises = [];
for (let i = 1; i <= 50; i++) {
promises.push(
helpers.taskMaster(
'add-task',
['--title', `Task ${i}`, '--description', `Description ${i}`],
{ cwd: testDir }
)
);
}
await Promise.all(promises);
const startTime = Date.now();
const result = await helpers.taskMaster('list', [], { cwd: testDir });
const endTime = Date.now();
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Task 1');
expect(result.stdout).toContain('Task 50');
// Should complete within reasonable time (5 seconds)
expect(endTime - startTime).toBeLessThan(5000);
});
});
describe('Display formatting', () => {
it('should truncate long titles appropriately', async () => {
const longTitle =
'This is a very long task title that should be truncated in the display to fit within the table column width constraints';
await helpers.taskMaster(
'add-task',
['--title', longTitle, '--description', 'Task with long title'],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
// Should contain at least part of the title
expect(result.stdout).toContain('This is a very long task title');
});
it('should show suggested next steps', async () => {
await helpers.taskMaster(
'add-task',
['--title', 'Sample task', '--description', 'For testing'],
{ cwd: testDir }
);
const result = await helpers.taskMaster('list', [], { cwd: testDir });
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Suggested Next Steps:');
expect(result.stdout).toContain('task-master next');
expect(result.stdout).toContain('task-master expand');
expect(result.stdout).toContain('task-master set-status');
});
});
});

View File

@@ -3,7 +3,14 @@
* Tests all aspects of PRD parsing including task generation, research mode, and various formats
*/
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
@@ -14,11 +21,11 @@ describe('parse-prd command', () => {
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-parse-prd-'));
// Initialize test helpers
const context = global.createTestContext('parse-prd');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
@@ -26,9 +33,11 @@ describe('parse-prd command', () => {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
});
@@ -49,23 +58,22 @@ describe('parse-prd command', () => {
- Login with JWT tokens
- Password reset functionality
- User profile management`;
const prdPath = join(testDir, 'test-prd.txt');
writeFileSync(prdPath, prdContent);
const result = await helpers.taskMaster(
'parse-prd',
[prdPath],
{ cwd: testDir, timeout: 45000 }
);
const result = await helpers.taskMaster('parse-prd', [prdPath], {
cwd: testDir,
timeout: 45000
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Tasks generated successfully');
// Verify tasks.json was created
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
expect(existsSync(tasksPath)).toBe(true);
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
expect(tasks.master.tasks.length).toBeGreaterThan(0);
}, 60000);
@@ -76,13 +84,12 @@ describe('parse-prd command', () => {
const defaultPrdPath = join(testDir, '.taskmaster/prd.txt');
mkdirSync(join(testDir, '.taskmaster'), { recursive: true });
writeFileSync(defaultPrdPath, prdContent);
const result = await helpers.taskMaster(
'parse-prd',
[],
{ cwd: testDir, timeout: 45000 }
);
const result = await helpers.taskMaster('parse-prd', [], {
cwd: testDir,
timeout: 45000
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Using default PRD file');
}, 60000);
@@ -91,13 +98,13 @@ describe('parse-prd command', () => {
const prdContent = 'Create a REST API for blog management';
const prdPath = join(testDir, 'api-prd.txt');
writeFileSync(prdPath, prdContent);
const result = await helpers.taskMaster(
'parse-prd',
['--input', prdPath],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Tasks generated successfully');
}, 60000);
@@ -105,18 +112,19 @@ describe('parse-prd command', () => {
describe('Task generation options', () => {
it('should generate custom number of tasks', async () => {
const prdContent = 'Build a comprehensive e-commerce platform with all features';
const prdContent =
'Build a comprehensive e-commerce platform with all features';
const prdPath = join(testDir, 'ecommerce-prd.txt');
writeFileSync(prdPath, prdContent);
const result = await helpers.taskMaster(
'parse-prd',
[prdPath, '--num-tasks', '5'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
// AI might generate slightly more or less, but should be close to 5
@@ -128,18 +136,18 @@ describe('parse-prd command', () => {
const prdContent = 'Build a chat application';
const prdPath = join(testDir, 'chat-prd.txt');
writeFileSync(prdPath, prdContent);
const customOutput = join(testDir, 'custom-tasks.json');
const result = await helpers.taskMaster(
'parse-prd',
[prdPath, '--output', customOutput],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(existsSync(customOutput)).toBe(true);
const tasks = JSON.parse(readFileSync(customOutput, 'utf8'));
expect(tasks.master.tasks.length).toBeGreaterThan(0);
}, 60000);
@@ -151,21 +159,21 @@ describe('parse-prd command', () => {
const initialPrd = 'Build feature A';
const prdPath1 = join(testDir, 'initial.txt');
writeFileSync(prdPath1, initialPrd);
await helpers.taskMaster('parse-prd', [prdPath1], { cwd: testDir });
// Create new PRD
const newPrd = 'Build feature B';
const prdPath2 = join(testDir, 'new.txt');
writeFileSync(prdPath2, newPrd);
// Parse with force flag
const result = await helpers.taskMaster(
'parse-prd',
[prdPath2, '--force'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).not.toContain('overwrite existing tasks?');
}, 90000);
@@ -175,59 +183,62 @@ describe('parse-prd command', () => {
const initialPrd = 'Build authentication system';
const prdPath1 = join(testDir, 'auth-prd.txt');
writeFileSync(prdPath1, initialPrd);
await helpers.taskMaster('parse-prd', [prdPath1], { cwd: testDir });
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const initialTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
const initialCount = initialTasks.master.tasks.length;
// Create additional PRD
const additionalPrd = 'Build user profile features';
const prdPath2 = join(testDir, 'profile-prd.txt');
writeFileSync(prdPath2, additionalPrd);
// Parse with append flag
const result = await helpers.taskMaster(
'parse-prd',
[prdPath2, '--append'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Appending to existing tasks');
const finalTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
expect(finalTasks.master.tasks.length).toBeGreaterThan(initialCount);
// Verify IDs are sequential
const maxId = Math.max(...finalTasks.master.tasks.map(t => t.id));
const maxId = Math.max(...finalTasks.master.tasks.map((t) => t.id));
expect(maxId).toBe(finalTasks.master.tasks.length);
}, 90000);
});
describe('Research mode', () => {
it('should use research mode with --research flag', async () => {
const prdContent = 'Build a machine learning pipeline for recommendation system';
const prdContent =
'Build a machine learning pipeline for recommendation system';
const prdPath = join(testDir, 'ml-prd.txt');
writeFileSync(prdPath, prdContent);
const result = await helpers.taskMaster(
'parse-prd',
[prdPath, '--research'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Using Perplexity AI for research-backed task generation');
expect(result.stdout).toContain(
'Using Perplexity AI for research-backed task generation'
);
// Research mode should produce more detailed tasks
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
// Check that tasks have detailed implementation details
const hasDetailedTasks = tasks.master.tasks.some(t =>
t.details && t.details.length > 200
const hasDetailedTasks = tasks.master.tasks.some(
(t) => t.details && t.details.length > 200
);
expect(hasDetailedTasks).toBe(true);
}, 120000);
@@ -237,22 +248,22 @@ describe('parse-prd command', () => {
it('should parse PRD to specific tag', async () => {
// Create a new tag
await helpers.taskMaster('add-tag', ['feature-x'], { cwd: testDir });
const prdContent = 'Build feature X components';
const prdPath = join(testDir, 'feature-x-prd.txt');
writeFileSync(prdPath, prdContent);
const result = await helpers.taskMaster(
'parse-prd',
[prdPath, '--tag', 'feature-x'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
expect(tasks['feature-x']).toBeDefined();
expect(tasks['feature-x'].tasks.length).toBeGreaterThan(0);
}, 60000);
@@ -274,25 +285,25 @@ Build a task management system with the following features:
- REST API backend
- React frontend
- PostgreSQL database`;
const prdPath = join(testDir, 'markdown-prd.md');
writeFileSync(prdPath, prdContent);
const result = await helpers.taskMaster(
'parse-prd',
[prdPath],
{ cwd: testDir, timeout: 45000 }
);
const result = await helpers.taskMaster('parse-prd', [prdPath], {
cwd: testDir,
timeout: 45000
});
expect(result).toHaveExitCode(0);
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
// Should parse technical requirements into tasks
const hasApiTask = tasks.master.tasks.some(t =>
t.title.toLowerCase().includes('api') ||
t.description.toLowerCase().includes('api')
const hasApiTask = tasks.master.tasks.some(
(t) =>
t.title.toLowerCase().includes('api') ||
t.description.toLowerCase().includes('api')
);
expect(hasApiTask).toBe(true);
}, 60000);
@@ -310,26 +321,26 @@ DELETE /api/users/:id - Delete user
\`\`\`
Each endpoint should have proper error handling and validation.`;
const prdPath = join(testDir, 'api-prd.txt');
writeFileSync(prdPath, prdContent);
const result = await helpers.taskMaster(
'parse-prd',
[prdPath],
{ cwd: testDir, timeout: 45000 }
);
const result = await helpers.taskMaster('parse-prd', [prdPath], {
cwd: testDir,
timeout: 45000
});
expect(result).toHaveExitCode(0);
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
// Should create tasks for API endpoints
const hasEndpointTasks = tasks.master.tasks.some(t =>
t.title.includes('endpoint') ||
t.description.includes('endpoint') ||
t.details.includes('/api/')
const hasEndpointTasks = tasks.master.tasks.some(
(t) =>
t.title.includes('endpoint') ||
t.description.includes('endpoint') ||
t.details.includes('/api/')
);
expect(hasEndpointTasks).toBe(true);
}, 60000);
@@ -342,7 +353,7 @@ Each endpoint should have proper error handling and validation.`;
['non-existent-file.txt'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('not found');
});
@@ -350,23 +361,20 @@ Each endpoint should have proper error handling and validation.`;
it('should fail with empty PRD file', async () => {
const emptyPrdPath = join(testDir, 'empty.txt');
writeFileSync(emptyPrdPath, '');
const result = await helpers.taskMaster(
'parse-prd',
[emptyPrdPath],
{ cwd: testDir, allowFailure: true }
);
const result = await helpers.taskMaster('parse-prd', [emptyPrdPath], {
cwd: testDir,
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
});
it('should show help when no PRD specified and no default exists', async () => {
const result = await helpers.taskMaster(
'parse-prd',
[],
{ cwd: testDir }
);
const result = await helpers.taskMaster('parse-prd', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Parse PRD Help');
expect(result.stdout).toContain('No PRD file specified');
@@ -384,10 +392,10 @@ Each endpoint should have proper error handling and validation.`;
largePrd += `- Requirement B for feature ${i}\n`;
largePrd += `- Integration with feature ${i - 1}\n\n`;
}
const prdPath = join(testDir, 'large-prd.txt');
writeFileSync(prdPath, largePrd);
const startTime = Date.now();
const result = await helpers.taskMaster(
'parse-prd',
@@ -395,10 +403,10 @@ Each endpoint should have proper error handling and validation.`;
{ cwd: testDir, timeout: 120000 }
);
const duration = Date.now() - startTime;
expect(result).toHaveExitCode(0);
expect(duration).toBeLessThan(120000); // Should complete within 2 minutes
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
expect(tasks.master.tasks.length).toBeGreaterThan(10);
@@ -411,22 +419,21 @@ Build a system with:
- UTF-8 support: ñáéíóú αβγδε 中文字符
- Special symbols: @#$%^&*()_+{}[]|\\:;"'<>,.?/
- Emoji support: 🚀 📊 💻 ✅`;
const prdPath = join(testDir, 'special-chars-prd.txt');
writeFileSync(prdPath, prdContent, 'utf8');
const result = await helpers.taskMaster(
'parse-prd',
[prdPath],
{ cwd: testDir, timeout: 45000 }
);
const result = await helpers.taskMaster('parse-prd', [prdPath], {
cwd: testDir,
timeout: 45000
});
expect(result).toHaveExitCode(0);
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasksContent = readFileSync(tasksPath, 'utf8');
const tasks = JSON.parse(tasksContent);
// Verify special characters are preserved
expect(tasksContent).toContain('UTF-8');
}, 60000);
@@ -437,13 +444,13 @@ Build a system with:
const prdContent = 'Build a simple blog system';
const prdPath = join(testDir, 'blog-prd.txt');
writeFileSync(prdPath, prdContent);
// Parse PRD
await helpers.taskMaster('parse-prd', [prdPath], { cwd: testDir });
// List tasks
const listResult = await helpers.taskMaster('list', [], { cwd: testDir });
expect(listResult).toHaveExitCode(0);
expect(listResult.stdout).toContain('ID');
expect(listResult.stdout).toContain('Title');
@@ -454,19 +461,18 @@ Build a system with:
const prdContent = 'Build user authentication';
const prdPath = join(testDir, 'auth-prd.txt');
writeFileSync(prdPath, prdContent);
// Parse PRD
await helpers.taskMaster('parse-prd', [prdPath], { cwd: testDir });
// Expand first task
const expandResult = await helpers.taskMaster(
'expand',
['--id', '1'],
{ cwd: testDir, timeout: 45000 }
);
const expandResult = await helpers.taskMaster('expand', ['--id', '1'], {
cwd: testDir,
timeout: 45000
});
expect(expandResult).toHaveExitCode(0);
expect(expandResult.stdout).toContain('Expanded task');
}, 90000);
});
});
});

View File

@@ -0,0 +1,516 @@
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { mkdtempSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { rmSync, existsSync, readFileSync } from 'fs';
describe('task-master remove-task', () => {
let testDir;
let helpers;
beforeEach(() => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'tm-test-remove-'));
process.chdir(testDir);
// Get helpers from global context
helpers = global.testHelpers;
// Copy .env if exists
const envPath = join(process.cwd(), '../../.env');
if (existsSync(envPath)) {
const envContent = readFileSync(envPath, 'utf-8');
helpers.writeFile('.env', envContent);
}
// Initialize task-master project
const initResult = helpers.taskMaster('init', ['-y']);
expect(initResult).toHaveExitCode(0);
// Ensure tasks.json exists
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (!helpers.fileExists(tasksPath)) {
helpers.writeFile(tasksPath, JSON.stringify({ tasks: [] }, null, 2));
}
});
afterEach(() => {
// Clean up test directory
process.chdir('..');
rmSync(testDir, { recursive: true, force: true });
});
describe('Basic removal', () => {
it('should remove a single task', () => {
// Create task
const addResult = helpers.taskMaster('add-task', [
'Task to remove',
'-m'
]);
expect(addResult).toHaveExitCode(0);
const taskId = helpers.extractTaskId(addResult.stdout);
// Remove task
const result = helpers.taskMaster('remove-task', [taskId, '-y']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Task removed successfully');
expect(result.stdout).toContain(taskId);
// Verify task is gone
const showResult = helpers.taskMaster('show', [taskId], {
allowFailure: true
});
expect(showResult.exitCode).not.toBe(0);
});
it('should prompt for confirmation without -y flag', () => {
// Create task
const addResult = helpers.taskMaster('add-task', ['Test task', '-m']);
const taskId = helpers.extractTaskId(addResult.stdout);
// Try to remove without confirmation (should fail or prompt)
const result = helpers.taskMaster('remove-task', [taskId], {
input: 'n\n' // Simulate saying "no" to confirmation
});
// Task should still exist
const showResult = helpers.taskMaster('show', [taskId]);
expect(showResult).toHaveExitCode(0);
});
it('should remove task with subtasks', () => {
// Create parent task
const parentResult = helpers.taskMaster('add-task', [
'Parent task',
'-m'
]);
const parentId = helpers.extractTaskId(parentResult.stdout);
// Add subtasks
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
timeout: 60000
});
// Remove parent task
const result = helpers.taskMaster('remove-task', [parentId, '-y']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('and 3 subtasks');
// Verify all are gone
const showResult = helpers.taskMaster('show', [parentId], {
allowFailure: true
});
expect(showResult.exitCode).not.toBe(0);
});
});
describe('Bulk removal', () => {
it('should remove multiple tasks', () => {
// Create multiple tasks
const ids = [];
for (let i = 0; i < 3; i++) {
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
ids.push(helpers.extractTaskId(result.stdout));
}
// Remove all
const result = helpers.taskMaster('remove-task', [ids.join(','), '-y']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('3 tasks removed');
// Verify all are gone
for (const id of ids) {
const showResult = helpers.taskMaster('show', [id], {
allowFailure: true
});
expect(showResult.exitCode).not.toBe(0);
}
});
it('should remove tasks by range', () => {
// Create sequential tasks
const ids = [];
for (let i = 0; i < 5; i++) {
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
ids.push(helpers.extractTaskId(result.stdout));
}
// Remove middle range
const result = helpers.taskMaster('remove-task', [
'--from',
ids[1],
'--to',
ids[3],
'-y'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('3 tasks removed');
// Verify edge tasks still exist
const show0 = helpers.taskMaster('show', [ids[0]]);
expect(show0).toHaveExitCode(0);
const show4 = helpers.taskMaster('show', [ids[4]]);
expect(show4).toHaveExitCode(0);
// Verify middle tasks are gone
for (let i = 1; i <= 3; i++) {
const showResult = helpers.taskMaster('show', [ids[i]], {
allowFailure: true
});
expect(showResult.exitCode).not.toBe(0);
}
});
it('should remove all tasks with --all flag', () => {
// Create multiple tasks
for (let i = 0; i < 3; i++) {
helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
}
// Remove all
const result = helpers.taskMaster('remove-task', ['--all', '-y']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('All tasks removed');
// Verify empty
const listResult = helpers.taskMaster('list');
expect(listResult.stdout).toContain('No tasks found');
});
});
describe('Dependency handling', () => {
it('should warn when removing task with dependents', () => {
// Create dependency chain
const task1 = helpers.taskMaster('add-task', ['Base task', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', [
'Dependent task',
'-m',
'-d',
id1
]);
const id2 = helpers.extractTaskId(task2.stdout);
// Try to remove base task
const result = helpers.taskMaster('remove-task', [id1, '-y']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Warning');
expect(result.stdout).toContain('dependent tasks');
expect(result.stdout).toContain(id2);
});
it('should handle cascade removal with --cascade', () => {
// Create dependency chain
const task1 = helpers.taskMaster('add-task', ['Base task', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', [
'Dependent 1',
'-m',
'-d',
id1
]);
const id2 = helpers.extractTaskId(task2.stdout);
const task3 = helpers.taskMaster('add-task', [
'Dependent 2',
'-m',
'-d',
id2
]);
const id3 = helpers.extractTaskId(task3.stdout);
// Remove with cascade
const result = helpers.taskMaster('remove-task', [
id1,
'--cascade',
'-y'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('3 tasks removed');
expect(result.stdout).toContain('cascade');
// Verify all are gone
for (const id of [id1, id2, id3]) {
const showResult = helpers.taskMaster('show', [id], {
allowFailure: true
});
expect(showResult.exitCode).not.toBe(0);
}
});
it('should update dependencies when removing task', () => {
// Create chain: task1 -> task2 -> task3
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m', '-d', id1]);
const id2 = helpers.extractTaskId(task2.stdout);
const task3 = helpers.taskMaster('add-task', ['Task 3', '-m', '-d', id2]);
const id3 = helpers.extractTaskId(task3.stdout);
// Remove middle task
const result = helpers.taskMaster('remove-task', [id2, '-y']);
expect(result).toHaveExitCode(0);
// Task 3 should now depend directly on task 1
const showResult = helpers.taskMaster('show', [id3]);
expect(showResult).toHaveExitCode(0);
expect(showResult.stdout).toContain('Dependencies:');
expect(showResult.stdout).toContain(id1);
expect(showResult.stdout).not.toContain(id2);
});
});
describe('Status filtering', () => {
it('should remove only completed tasks', () => {
// Create tasks with different statuses
const pending = helpers.taskMaster('add-task', ['Pending task', '-m']);
const pendingId = helpers.extractTaskId(pending.stdout);
const done1 = helpers.taskMaster('add-task', ['Done task 1', '-m']);
const doneId1 = helpers.extractTaskId(done1.stdout);
helpers.taskMaster('set-status', [doneId1, 'done']);
const done2 = helpers.taskMaster('add-task', ['Done task 2', '-m']);
const doneId2 = helpers.extractTaskId(done2.stdout);
helpers.taskMaster('set-status', [doneId2, 'done']);
// Remove only done tasks
const result = helpers.taskMaster('remove-task', [
'--status',
'done',
'-y'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('2 tasks removed');
// Verify pending task still exists
const showResult = helpers.taskMaster('show', [pendingId]);
expect(showResult).toHaveExitCode(0);
// Verify done tasks are gone
for (const id of [doneId1, doneId2]) {
const show = helpers.taskMaster('show', [id], {
allowFailure: true
});
expect(show.exitCode).not.toBe(0);
}
});
it('should remove cancelled and deferred tasks', () => {
// Create tasks
const cancelled = helpers.taskMaster('add-task', ['Cancelled', '-m']);
const cancelledId = helpers.extractTaskId(cancelled.stdout);
helpers.taskMaster('set-status', [cancelledId, 'cancelled']);
const deferred = helpers.taskMaster('add-task', ['Deferred', '-m']);
const deferredId = helpers.extractTaskId(deferred.stdout);
helpers.taskMaster('set-status', [deferredId, 'deferred']);
const active = helpers.taskMaster('add-task', ['Active', '-m']);
const activeId = helpers.extractTaskId(active.stdout);
// Remove cancelled and deferred
const result = helpers.taskMaster('remove-task', [
'--status',
'cancelled,deferred',
'-y'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('2 tasks removed');
// Verify active task remains
const showResult = helpers.taskMaster('show', [activeId]);
expect(showResult).toHaveExitCode(0);
});
});
describe('Tag context', () => {
it('should remove tasks from specific tag', () => {
// Create tag
helpers.taskMaster('add-tag', ['feature']);
// Add tasks to different tags
const master = helpers.taskMaster('add-task', ['Master task', '-m']);
const masterId = helpers.extractTaskId(master.stdout);
helpers.taskMaster('use-tag', ['feature']);
const feature = helpers.taskMaster('add-task', ['Feature task', '-m']);
const featureId = helpers.extractTaskId(feature.stdout);
// Remove from feature tag
const result = helpers.taskMaster('remove-task', [
featureId,
'--tag',
'feature',
'-y'
]);
expect(result).toHaveExitCode(0);
// Verify master task still exists
helpers.taskMaster('use-tag', ['master']);
const showResult = helpers.taskMaster('show', [masterId]);
expect(showResult).toHaveExitCode(0);
});
});
describe('Undo functionality', () => {
it('should create backup before removal', () => {
// Create task
const task = helpers.taskMaster('add-task', ['Task to backup', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
// Remove task
const result = helpers.taskMaster('remove-task', [taskId, '-y']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Backup created');
// Check for backup file
const backupDir = join(testDir, '.taskmaster/backups');
expect(existsSync(backupDir)).toBe(true);
});
it('should show undo instructions', () => {
// Create and remove task
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
const result = helpers.taskMaster('remove-task', [taskId, '-y']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('To undo this operation');
});
});
describe('Subtask removal', () => {
it('should remove individual subtask', () => {
// Create parent with subtasks
const parent = helpers.taskMaster('add-task', ['Parent', '-m']);
const parentId = helpers.extractTaskId(parent.stdout);
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
timeout: 60000
});
// Remove middle subtask
const subtaskId = `${parentId}.2`;
const result = helpers.taskMaster('remove-task', [subtaskId, '-y']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Subtask removed');
// Verify parent still has 2 subtasks
const showResult = helpers.taskMaster('show', [parentId]);
expect(showResult).toHaveExitCode(0);
expect(showResult.stdout).toContain('Subtasks (2)');
});
it('should renumber remaining subtasks', () => {
// Create parent with subtasks
const parent = helpers.taskMaster('add-task', ['Parent', '-m']);
const parentId = helpers.extractTaskId(parent.stdout);
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
timeout: 60000
});
// Remove first subtask
const result = helpers.taskMaster('remove-task', [`${parentId}.1`, '-y']);
expect(result).toHaveExitCode(0);
// Check remaining subtasks are renumbered
const showResult = helpers.taskMaster('show', [parentId]);
expect(showResult).toHaveExitCode(0);
expect(showResult.stdout).toContain(`${parentId}.1`);
expect(showResult.stdout).toContain(`${parentId}.2`);
expect(showResult.stdout).not.toContain(`${parentId}.3`);
});
});
describe('Error handling', () => {
it('should handle non-existent task ID', () => {
const result = helpers.taskMaster('remove-task', ['999', '-y'], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toMatch(/Task.*not found/i);
});
it('should handle invalid task ID format', () => {
const result = helpers.taskMaster('remove-task', ['invalid-id', '-y'], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid task ID');
});
it('should prevent removing all tasks without confirmation', () => {
// Create tasks
helpers.taskMaster('add-task', ['Task 1', '-m']);
helpers.taskMaster('add-task', ['Task 2', '-m']);
// Try to remove all without -y
const result = helpers.taskMaster('remove-task', ['--all'], {
input: 'n\n'
});
// Tasks should still exist
const listResult = helpers.taskMaster('list');
expect(listResult.stdout).not.toContain('No tasks found');
});
});
describe('Performance', () => {
it('should handle bulk removal efficiently', () => {
// Create many tasks
const ids = [];
for (let i = 0; i < 50; i++) {
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
ids.push(helpers.extractTaskId(result.stdout));
}
// Remove all at once
const startTime = Date.now();
const result = helpers.taskMaster('remove-task', ['--all', '-y']);
const endTime = Date.now();
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('50 tasks removed');
expect(endTime - startTime).toBeLessThan(5000);
});
});
describe('Output options', () => {
it('should support quiet mode', () => {
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
const result = helpers.taskMaster('remove-task', [taskId, '-y', '-q']);
expect(result).toHaveExitCode(0);
expect(result.stdout.split('\n').length).toBeLessThan(3);
});
it('should support JSON output', () => {
// Create tasks
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m']);
const id2 = helpers.extractTaskId(task2.stdout);
const result = helpers.taskMaster('remove-task', [
`${id1},${id2}`,
'-y',
'--json'
]);
expect(result).toHaveExitCode(0);
const json = JSON.parse(result.stdout);
expect(json.removed).toBe(2);
expect(json.tasks).toHaveLength(2);
expect(json.backup).toBeDefined();
});
});
});

View File

@@ -37,13 +37,13 @@ export default async function testResearchSave(logger, helpers, context) {
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Verify file was created
const outputPath = `${testDir}/oauth-guide.md`;
if (!helpers.fileExists(outputPath)) {
throw new Error('Research output file was not created');
}
// Check file content
const content = helpers.readFile(outputPath);
if (!content.includes('OAuth') || !content.includes('Node.js')) {
@@ -60,22 +60,28 @@ export default async function testResearchSave(logger, helpers, context) {
{ cwd: testDir }
);
const taskId = helpers.extractTaskId(taskResult.stdout);
const result = await helpers.taskMaster(
'research-save',
['--task', taskId, 'JWT vs OAuth comparison for REST APIs', '--output', 'auth-research.md'],
[
'--task',
taskId,
'JWT vs OAuth comparison for REST APIs',
'--output',
'auth-research.md'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check saved content includes task context
const content = helpers.readFile(`${testDir}/auth-research.md`);
if (!content.includes('JWT') || !content.includes('OAuth')) {
throw new Error('Research does not cover requested topics');
}
// Should reference the task
if (!content.includes(taskId) && !content.includes('Task #')) {
throw new Error('Saved research does not reference the task context');
@@ -86,25 +92,30 @@ export default async function testResearchSave(logger, helpers, context) {
await runTest('Save to knowledge base', async () => {
const result = await helpers.taskMaster(
'research-save',
['Database indexing strategies', '--knowledge-base', '--category', 'database'],
[
'Database indexing strategies',
'--knowledge-base',
'--category',
'database'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check knowledge base directory
const kbPath = `${testDir}/.taskmaster/knowledge-base/database`;
if (!helpers.fileExists(kbPath)) {
throw new Error('Knowledge base category directory not created');
}
// Should create a file with timestamp or ID
const files = helpers.listFiles(kbPath);
if (files.length === 0) {
throw new Error('No files created in knowledge base');
}
// Verify content
const savedFile = files[0];
const content = helpers.readFile(`${kbPath}/${savedFile}`);
@@ -117,13 +128,19 @@ export default async function testResearchSave(logger, helpers, context) {
await runTest('Save with custom format', async () => {
const result = await helpers.taskMaster(
'research-save',
['React performance optimization', '--output', 'react-perf.json', '--format', 'json'],
[
'React performance optimization',
'--output',
'react-perf.json',
'--format',
'json'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Verify JSON format
const content = helpers.readFile(`${testDir}/react-perf.json`);
let parsed;
@@ -132,14 +149,16 @@ export default async function testResearchSave(logger, helpers, context) {
} catch (e) {
throw new Error('Output is not valid JSON');
}
// Check JSON structure
if (!parsed.topic || !parsed.content || !parsed.timestamp) {
throw new Error('JSON output missing expected fields');
}
if (!parsed.content.toLowerCase().includes('react') ||
!parsed.content.toLowerCase().includes('performance')) {
if (
!parsed.content.toLowerCase().includes('react') ||
!parsed.content.toLowerCase().includes('performance')
) {
throw new Error('JSON content not relevant to query');
}
});
@@ -150,26 +169,33 @@ export default async function testResearchSave(logger, helpers, context) {
'research-save',
[
'Microservices communication patterns',
'--output', 'microservices.md',
'--metadata', 'author=TaskMaster',
'--metadata', 'tags=architecture,microservices',
'--metadata', 'version=1.0'
'--output',
'microservices.md',
'--metadata',
'author=TaskMaster',
'--metadata',
'tags=architecture,microservices',
'--metadata',
'version=1.0'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check file content for metadata
const content = helpers.readFile(`${testDir}/microservices.md`);
// Should include metadata in frontmatter or header
if (!content.includes('author') && !content.includes('Author')) {
throw new Error('Metadata not included in saved file');
}
if (!content.includes('microservice') || !content.includes('communication')) {
if (
!content.includes('microservice') ||
!content.includes('communication')
) {
throw new Error('Research content not relevant');
}
});
@@ -177,18 +203,24 @@ export default async function testResearchSave(logger, helpers, context) {
// Test 6: Append to existing file
await runTest('Append to existing research file', async () => {
// Create initial file
const initialContent = '# API Research\n\n## Previous Research\n\nInitial content here.\n\n';
const initialContent =
'# API Research\n\n## Previous Research\n\nInitial content here.\n\n';
helpers.writeFile(`${testDir}/api-research.md`, initialContent);
const result = await helpers.taskMaster(
'research-save',
['GraphQL schema design best practices', '--output', 'api-research.md', '--append'],
[
'GraphQL schema design best practices',
'--output',
'api-research.md',
'--append'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check file was appended
const content = helpers.readFile(`${testDir}/api-research.md`);
if (!content.includes('Previous Research')) {
@@ -203,24 +235,30 @@ export default async function testResearchSave(logger, helpers, context) {
await runTest('Save with source references', async () => {
const result = await helpers.taskMaster(
'research-save',
['TypeScript decorators guide', '--output', 'decorators.md', '--include-references'],
[
'TypeScript decorators guide',
'--output',
'decorators.md',
'--include-references'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check for references section
const content = helpers.readFile(`${testDir}/decorators.md`);
if (!content.includes('TypeScript') || !content.includes('decorator')) {
throw new Error('Research content not relevant');
}
// Should include references or sources
const hasReferences = content.includes('Reference') ||
content.includes('Source') ||
content.includes('Further reading') ||
content.includes('Links');
const hasReferences =
content.includes('Reference') ||
content.includes('Source') ||
content.includes('Further reading') ||
content.includes('Links');
if (!hasReferences) {
throw new Error('No references section included');
}
@@ -233,7 +271,7 @@ export default async function testResearchSave(logger, helpers, context) {
'Kubernetes deployment strategies',
'CI/CD pipeline setup'
];
const result = await helpers.taskMaster(
'research-save',
['--batch', '--output-dir', 'devops-research', ...topics],
@@ -242,28 +280,37 @@ export default async function testResearchSave(logger, helpers, context) {
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check directory was created
const outputDir = `${testDir}/devops-research`;
if (!helpers.fileExists(outputDir)) {
throw new Error('Output directory not created');
}
// Should have files for each topic
const files = helpers.listFiles(outputDir);
if (files.length < topics.length) {
throw new Error(`Expected ${topics.length} files, found ${files.length}`);
throw new Error(
`Expected ${topics.length} files, found ${files.length}`
);
}
// Verify each file has relevant content
let foundDocker = false, foundK8s = false, foundCICD = false;
files.forEach(file => {
let foundDocker = false,
foundK8s = false,
foundCICD = false;
files.forEach((file) => {
const content = helpers.readFile(`${outputDir}/${file}`).toLowerCase();
if (content.includes('docker')) foundDocker = true;
if (content.includes('kubernetes')) foundK8s = true;
if (content.includes('ci') || content.includes('cd') || content.includes('pipeline')) foundCICD = true;
if (
content.includes('ci') ||
content.includes('cd') ||
content.includes('pipeline')
)
foundCICD = true;
});
if (!foundDocker || !foundK8s || !foundCICD) {
throw new Error('Not all topics were researched and saved');
}
@@ -290,21 +337,24 @@ Category: {{CATEGORY}}
{{NOTES}}
`;
helpers.writeFile(`${testDir}/research-template.md`, template);
const result = await helpers.taskMaster(
'research-save',
[
'Redis caching strategies',
'--output', 'redis-research.md',
'--template', 'research-template.md',
'--category', 'performance'
'--output',
'redis-research.md',
'--template',
'research-template.md',
'--category',
'performance'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check template was used
const content = helpers.readFile(`${testDir}/redis-research.md`);
if (!content.includes('Redis caching strategies')) {
@@ -313,7 +363,10 @@ Category: {{CATEGORY}}
if (!content.includes('Category: performance')) {
throw new Error('Template category not filled');
}
if (!content.includes('Key Takeaways') || !content.includes('Implementation Notes')) {
if (
!content.includes('Key Takeaways') ||
!content.includes('Implementation Notes')
) {
throw new Error('Template structure not preserved');
}
});
@@ -339,13 +392,15 @@ Category: {{CATEGORY}}
{ cwd: testDir }
);
const taskId = helpers.extractTaskId(taskResult.stdout);
const result = await helpers.taskMaster(
'research-save',
[
'--task', taskId,
'--task',
taskId,
'Caching strategies comparison',
'--output', 'caching-research.md',
'--output',
'caching-research.md',
'--link-to-task'
],
{ cwd: testDir, timeout: 120000 }
@@ -353,11 +408,15 @@ Category: {{CATEGORY}}
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check task was updated with research link
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
if (!showResult.stdout.includes('caching-research.md') &&
!showResult.stdout.includes('Research')) {
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
if (
!showResult.stdout.includes('caching-research.md') &&
!showResult.stdout.includes('Research')
) {
throw new Error('Task not updated with research link');
}
});
@@ -368,7 +427,8 @@ Category: {{CATEGORY}}
'research-save',
[
'Comprehensive guide to distributed systems',
'--output', 'dist-systems.md.gz',
'--output',
'dist-systems.md.gz',
'--compress'
],
{ cwd: testDir, timeout: 120000 }
@@ -376,7 +436,7 @@ Category: {{CATEGORY}}
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check compressed file exists
const compressedPath = `${testDir}/dist-systems.md.gz`;
if (!helpers.fileExists(compressedPath)) {
@@ -392,21 +452,28 @@ Category: {{CATEGORY}}
['API design patterns', '--output', 'api-patterns.md', '--version'],
{ cwd: testDir, timeout: 120000 }
);
// Save updated version
const result = await helpers.taskMaster(
'research-save',
['API design patterns - updated', '--output', 'api-patterns.md', '--version'],
[
'API design patterns - updated',
'--output',
'api-patterns.md',
'--version'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check for version files
const files = helpers.listFiles(testDir);
const versionFiles = files.filter(f => f.includes('api-patterns') && f.includes('.v'));
const versionFiles = files.filter(
(f) => f.includes('api-patterns') && f.includes('.v')
);
if (versionFiles.length === 0) {
throw new Error('No version files created');
}
@@ -418,18 +485,20 @@ Category: {{CATEGORY}}
'research-save',
[
'Testing strategies overview',
'--output', 'testing',
'--formats', 'md,json,txt'
'--output',
'testing',
'--formats',
'md,json,txt'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check all format files exist
const formats = ['md', 'json', 'txt'];
formats.forEach(format => {
formats.forEach((format) => {
const filePath = `${testDir}/testing.${format}`;
if (!helpers.fileExists(filePath)) {
throw new Error(`${format} format file not created`);
@@ -443,32 +512,46 @@ Category: {{CATEGORY}}
'research-save',
[
'Machine learning deployment strategies',
'--output', 'ml-deployment.md',
'--output',
'ml-deployment.md',
'--include-summary',
'--summary-length', '200'
'--summary-length',
'200'
],
{ cwd: testDir, timeout: 120000 }
);
if (result.exitCode !== 0) {
throw new Error(`Command failed: ${result.stderr}`);
}
// Check for summary section
const content = helpers.readFile(`${testDir}/ml-deployment.md`);
if (!content.includes('Summary') && !content.includes('TL;DR') && !content.includes('Overview')) {
if (
!content.includes('Summary') &&
!content.includes('TL;DR') &&
!content.includes('Overview')
) {
throw new Error('No summary section found');
}
// Content should be about ML deployment
if (!content.includes('machine learning') && !content.includes('ML') && !content.includes('deployment')) {
if (
!content.includes('machine learning') &&
!content.includes('ML') &&
!content.includes('deployment')
) {
throw new Error('Research content not relevant to query');
}
});
// Calculate summary
const totalTests = results.tests.length;
const passedTests = results.tests.filter(t => t.status === 'passed').length;
const failedTests = results.tests.filter(t => t.status === 'failed').length;
const passedTests = results.tests.filter(
(t) => t.status === 'passed'
).length;
const failedTests = results.tests.filter(
(t) => t.status === 'failed'
).length;
logger.info('\n=== Research-Save Test Summary ===');
logger.info(`Total tests: ${totalTests}`);
@@ -481,7 +564,6 @@ Category: {{CATEGORY}}
} else {
logger.success('\n✅ All research-save tests passed!');
}
} catch (error) {
results.status = 'failed';
results.errors.push({
@@ -493,4 +575,4 @@ Category: {{CATEGORY}}
}
return results;
}
}

View File

@@ -3,7 +3,14 @@
* Tests all aspects of AI-powered research functionality
*/
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
@@ -14,11 +21,11 @@ describe('research command', () => {
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-research-'));
// Initialize test helpers
const context = global.createTestContext('research');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
@@ -26,9 +33,11 @@ describe('research command', () => {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
});
@@ -43,15 +52,18 @@ describe('research command', () => {
it('should perform research on a topic', async () => {
const result = await helpers.taskMaster(
'research',
['What are the best practices for implementing OAuth 2.0 authentication?'],
[
'What are the best practices for implementing OAuth 2.0 authentication?'
],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Research Results');
// Should contain relevant OAuth information
const hasOAuthInfo = result.stdout.toLowerCase().includes('oauth') ||
const hasOAuthInfo =
result.stdout.toLowerCase().includes('oauth') ||
result.stdout.toLowerCase().includes('authentication');
expect(hasOAuthInfo).toBe(true);
}, 120000);
@@ -62,12 +74,13 @@ describe('research command', () => {
['--topic', 'React performance optimization techniques'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Research Results');
// Should contain React-related information
const hasReactInfo = result.stdout.toLowerCase().includes('react') ||
const hasReactInfo =
result.stdout.toLowerCase().includes('react') ||
result.stdout.toLowerCase().includes('performance');
expect(hasReactInfo).toBe(true);
}, 120000);
@@ -78,11 +91,12 @@ describe('research command', () => {
['Compare PostgreSQL vs MongoDB for a real-time analytics application'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
// Should contain database comparison
const hasDatabaseInfo = result.stdout.toLowerCase().includes('postgresql') ||
const hasDatabaseInfo =
result.stdout.toLowerCase().includes('postgresql') ||
result.stdout.toLowerCase().includes('mongodb');
expect(hasDatabaseInfo).toBe(true);
}, 120000);
@@ -97,10 +111,10 @@ describe('research command', () => {
{ cwd: testDir, timeout: 60000 }
);
const duration = Date.now() - startTime;
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Research Results');
// Quick research should be faster
expect(duration).toBeLessThan(60000);
}, 90000);
@@ -111,15 +125,16 @@ describe('research command', () => {
['--topic', 'Microservices architecture patterns', '--detailed'],
{ cwd: testDir, timeout: 120000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Research Results');
// Detailed research should have more content
expect(result.stdout.length).toBeGreaterThan(500);
// Should contain comprehensive information
const hasPatterns = result.stdout.toLowerCase().includes('pattern') ||
const hasPatterns =
result.stdout.toLowerCase().includes('pattern') ||
result.stdout.toLowerCase().includes('architecture');
expect(hasPatterns).toBe(true);
}, 150000);
@@ -132,12 +147,13 @@ describe('research command', () => {
['--topic', 'GraphQL best practices', '--sources'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Research Results');
// Should include source references
const hasSources = result.stdout.includes('Source:') ||
const hasSources =
result.stdout.includes('Source:') ||
result.stdout.includes('Reference:') ||
result.stdout.includes('http');
expect(hasSources).toBe(true);
@@ -147,19 +163,19 @@ describe('research command', () => {
describe('Research output options', () => {
it('should save research to file with --save flag', async () => {
const outputPath = join(testDir, 'research-output.md');
const result = await helpers.taskMaster(
'research',
['--topic', 'Docker container security', '--save', outputPath],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Research saved to');
// Verify file was created
expect(existsSync(outputPath)).toBe(true);
// Verify file contains research content
const content = readFileSync(outputPath, 'utf8');
expect(content).toContain('Docker');
@@ -172,9 +188,9 @@ describe('research command', () => {
['--topic', 'WebSocket implementation', '--output', 'json'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
// Output should be valid JSON
const jsonOutput = JSON.parse(result.stdout);
expect(jsonOutput.topic).toBeDefined();
@@ -188,11 +204,12 @@ describe('research command', () => {
['--topic', 'CI/CD pipeline best practices'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
// Should contain markdown formatting
const hasMarkdown = result.stdout.includes('#') ||
const hasMarkdown =
result.stdout.includes('#') ||
result.stdout.includes('*') ||
result.stdout.includes('-');
expect(hasMarkdown).toBe(true);
@@ -203,10 +220,15 @@ describe('research command', () => {
it('should research coding patterns', async () => {
const result = await helpers.taskMaster(
'research',
['--topic', 'Singleton pattern in JavaScript', '--category', 'patterns'],
[
'--topic',
'Singleton pattern in JavaScript',
'--category',
'patterns'
],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toContain('singleton');
expect(result.stdout.toLowerCase()).toContain('pattern');
@@ -218,7 +240,7 @@ describe('research command', () => {
['--topic', 'OWASP Top 10 vulnerabilities', '--category', 'security'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toContain('security');
expect(result.stdout.toUpperCase()).toContain('OWASP');
@@ -230,7 +252,7 @@ describe('research command', () => {
['--topic', 'Database query optimization', '--category', 'performance'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toContain('optimization');
expect(result.stdout.toLowerCase()).toContain('performance');
@@ -246,14 +268,14 @@ describe('research command', () => {
{ cwd: testDir }
);
const taskId = helpers.extractTaskId(addResult.stdout);
// Research for the task
const result = await helpers.taskMaster(
'research',
['--task', taskId, '--topic', 'WebSocket vs Server-Sent Events'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Research Results');
expect(result.stdout.toLowerCase()).toContain('websocket');
@@ -267,19 +289,27 @@ describe('research command', () => {
{ cwd: testDir }
);
const taskId = helpers.extractTaskId(addResult.stdout);
// Research and append to task
const result = await helpers.taskMaster(
'research',
['--task', taskId, '--topic', 'Prometheus vs ELK stack', '--append-to-task'],
[
'--task',
taskId,
'--topic',
'Prometheus vs ELK stack',
'--append-to-task'
],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Research appended to task');
// Verify task has research notes
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('prometheus');
}, 120000);
});
@@ -292,13 +322,12 @@ describe('research command', () => {
['--topic', 'GraphQL subscriptions'],
{ cwd: testDir, timeout: 60000 }
);
await helpers.taskMaster(
'research',
['--topic', 'Redis pub/sub'],
{ cwd: testDir, timeout: 60000 }
);
await helpers.taskMaster('research', ['--topic', 'Redis pub/sub'], {
cwd: testDir,
timeout: 60000
});
// Check research history
const historyPath = join(testDir, '.taskmaster/research-history.json');
if (existsSync(historyPath)) {
@@ -314,14 +343,12 @@ describe('research command', () => {
['--topic', 'Kubernetes deployment strategies'],
{ cwd: testDir, timeout: 60000 }
);
// List history
const result = await helpers.taskMaster(
'research',
['--history'],
{ cwd: testDir }
);
const result = await helpers.taskMaster('research', ['--history'], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Research History');
}, 90000);
@@ -329,12 +356,11 @@ describe('research command', () => {
describe('Error handling', () => {
it('should fail without topic', async () => {
const result = await helpers.taskMaster(
'research',
[],
{ cwd: testDir, allowFailure: true }
);
const result = await helpers.taskMaster('research', [], {
cwd: testDir,
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('topic');
});
@@ -345,7 +371,7 @@ describe('research command', () => {
['--topic', 'Test topic', '--output', 'invalid-format'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid output format');
});
@@ -358,7 +384,7 @@ describe('research command', () => {
['--topic', 'Test with potential network issues'],
{ cwd: testDir, timeout: 30000, allowFailure: true }
);
// Should either succeed or fail gracefully
if (result.exitCode !== 0) {
expect(result.stderr).toBeTruthy();
@@ -372,10 +398,15 @@ describe('research command', () => {
it('should research implementation details', async () => {
const result = await helpers.taskMaster(
'research',
['--topic', 'JWT implementation in Node.js', '--focus', 'implementation'],
[
'--topic',
'JWT implementation in Node.js',
'--focus',
'implementation'
],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toContain('implementation');
expect(result.stdout.toLowerCase()).toContain('code');
@@ -387,7 +418,7 @@ describe('research command', () => {
['--topic', 'REST API versioning', '--focus', 'best-practices'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toContain('best practice');
}, 120000);
@@ -398,7 +429,7 @@ describe('research command', () => {
['--topic', 'Vue vs React vs Angular', '--focus', 'comparison'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
const output = result.stdout.toLowerCase();
expect(output).toContain('vue');
@@ -414,7 +445,7 @@ describe('research command', () => {
['--topic', 'Machine learning basics', '--max-length', '500'],
{ cwd: testDir, timeout: 60000 }
);
expect(result).toHaveExitCode(0);
// Research output should be concise
expect(result.stdout.length).toBeLessThan(2000); // Accounting for formatting
@@ -426,10 +457,11 @@ describe('research command', () => {
['--topic', 'Latest JavaScript features', '--year', '2024'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
// Should focus on recent content
const hasRecentInfo = result.stdout.includes('2024') ||
const hasRecentInfo =
result.stdout.includes('2024') ||
result.stdout.toLowerCase().includes('latest') ||
result.stdout.toLowerCase().includes('recent');
expect(hasRecentInfo).toBe(true);
@@ -439,27 +471,25 @@ describe('research command', () => {
describe('Research caching', () => {
it('should cache and reuse research results', async () => {
const topic = 'Redis caching strategies';
// First research
const startTime1 = Date.now();
const result1 = await helpers.taskMaster(
'research',
['--topic', topic],
{ cwd: testDir, timeout: 90000 }
);
const result1 = await helpers.taskMaster('research', ['--topic', topic], {
cwd: testDir,
timeout: 90000
});
const duration1 = Date.now() - startTime1;
expect(result1).toHaveExitCode(0);
// Second research (should be cached)
const startTime2 = Date.now();
const result2 = await helpers.taskMaster(
'research',
['--topic', topic],
{ cwd: testDir, timeout: 30000 }
);
const result2 = await helpers.taskMaster('research', ['--topic', topic], {
cwd: testDir,
timeout: 30000
});
const duration2 = Date.now() - startTime2;
expect(result2).toHaveExitCode(0);
// Cached result should be much faster
if (result2.stdout.includes('(cached)')) {
expect(duration2).toBeLessThan(duration1 / 2);
@@ -468,23 +498,22 @@ describe('research command', () => {
it('should bypass cache with --no-cache flag', async () => {
const topic = 'Docker best practices';
// First research
await helpers.taskMaster(
'research',
['--topic', topic],
{ cwd: testDir, timeout: 60000 }
);
await helpers.taskMaster('research', ['--topic', topic], {
cwd: testDir,
timeout: 60000
});
// Second research without cache
const result = await helpers.taskMaster(
'research',
['--topic', topic, '--no-cache'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).not.toContain('(cached)');
}, 180000);
});
});
});

View File

@@ -0,0 +1,466 @@
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { mkdtempSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { rmSync, existsSync, readFileSync } from 'fs';
describe('task-master set-status', () => {
let testDir;
let helpers;
beforeEach(() => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'tm-test-set-status-'));
process.chdir(testDir);
// Get helpers from global context
helpers = global.testHelpers;
// Copy .env if exists
const envPath = join(process.cwd(), '../../.env');
if (existsSync(envPath)) {
const envContent = readFileSync(envPath, 'utf-8');
helpers.writeFile('.env', envContent);
}
// Initialize task-master project
const initResult = helpers.taskMaster('init', ['-y']);
expect(initResult).toHaveExitCode(0);
// Ensure tasks.json exists
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (!helpers.fileExists(tasksPath)) {
helpers.writeFile(tasksPath, JSON.stringify({ tasks: [] }, null, 2));
}
});
afterEach(() => {
// Clean up test directory
process.chdir('..');
rmSync(testDir, { recursive: true, force: true });
});
describe('Basic status changes', () => {
it('should change task status to in-progress', () => {
// Create a test task
const addResult = helpers.taskMaster('add-task', ['Test task', '-m']);
expect(addResult).toHaveExitCode(0);
const taskId = helpers.extractTaskId(addResult.stdout);
// Set status to in-progress
const result = helpers.taskMaster('set-status', [taskId, 'in-progress']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Status updated');
expect(result.stdout).toContain('in-progress');
// Verify status changed
const showResult = helpers.taskMaster('show', [taskId]);
expect(showResult.stdout).toContain('Status: in-progress');
});
it('should change task status to done', () => {
// Create task
const addResult = helpers.taskMaster('add-task', [
'Task to complete',
'-m'
]);
const taskId = helpers.extractTaskId(addResult.stdout);
// Set status to done
const result = helpers.taskMaster('set-status', [taskId, 'done']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('✓ Completed');
// Verify
const showResult = helpers.taskMaster('show', [taskId]);
expect(showResult.stdout).toContain('Status: done');
});
it('should support all valid statuses', () => {
const statuses = [
'pending',
'in-progress',
'done',
'blocked',
'deferred',
'cancelled'
];
for (const status of statuses) {
const addResult = helpers.taskMaster('add-task', [
`Task for ${status}`,
'-m'
]);
const taskId = helpers.extractTaskId(addResult.stdout);
const result = helpers.taskMaster('set-status', [taskId, status]);
expect(result).toHaveExitCode(0);
expect(result.stdout.toLowerCase()).toContain(status);
}
});
});
describe('Subtask status changes', () => {
it('should change subtask status', () => {
// Create parent task with subtasks
const parentResult = helpers.taskMaster('add-task', [
'Parent task',
'-m'
]);
const parentId = helpers.extractTaskId(parentResult.stdout);
// Expand to add subtasks
const expandResult = helpers.taskMaster(
'expand',
['-i', parentId, '-n', '2'],
{ timeout: 60000 }
);
expect(expandResult).toHaveExitCode(0);
// Set subtask status
const subtaskId = `${parentId}.1`;
const result = helpers.taskMaster('set-status', [subtaskId, 'done']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Subtask completed');
// Verify parent task shows progress
const showResult = helpers.taskMaster('show', [parentId]);
expect(showResult.stdout).toMatch(/Progress:.*1\/2/);
});
it('should update parent status when all subtasks complete', () => {
// Create parent task with subtasks
const parentResult = helpers.taskMaster('add-task', [
'Parent task',
'-m'
]);
const parentId = helpers.extractTaskId(parentResult.stdout);
// Add subtasks
helpers.taskMaster('expand', ['-i', parentId, '-n', '2'], {
timeout: 60000
});
// Complete all subtasks
helpers.taskMaster('set-status', [`${parentId}.1`, 'done']);
const result = helpers.taskMaster('set-status', [
`${parentId}.2`,
'done'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('All subtasks completed');
expect(result.stdout).toContain(
'Parent task automatically marked as done'
);
// Verify parent is done
const showResult = helpers.taskMaster('show', [parentId]);
expect(showResult.stdout).toContain('Status: done');
});
});
describe('Bulk status updates', () => {
it('should update status for multiple tasks', () => {
// Create multiple tasks
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m']);
const id2 = helpers.extractTaskId(task2.stdout);
const task3 = helpers.taskMaster('add-task', ['Task 3', '-m']);
const id3 = helpers.extractTaskId(task3.stdout);
// Update multiple tasks
const result = helpers.taskMaster('set-status', [
`${id1},${id2},${id3}`,
'in-progress'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('3 tasks updated');
// Verify all changed
for (const id of [id1, id2, id3]) {
const showResult = helpers.taskMaster('show', [id]);
expect(showResult.stdout).toContain('Status: in-progress');
}
});
it('should update all pending tasks', () => {
// Create tasks with mixed statuses
const task1 = helpers.taskMaster('add-task', ['Pending 1', '-m']);
const task2 = helpers.taskMaster('add-task', ['Pending 2', '-m']);
const task3 = helpers.taskMaster('add-task', ['Already done', '-m']);
const id3 = helpers.extractTaskId(task3.stdout);
helpers.taskMaster('set-status', [id3, 'done']);
// Update all pending tasks
const result = helpers.taskMaster('set-status', [
'--all',
'in-progress',
'--filter-status',
'pending'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('2 tasks updated');
// Verify already done task unchanged
const showResult = helpers.taskMaster('show', [id3]);
expect(showResult.stdout).toContain('Status: done');
});
});
describe('Dependency handling', () => {
it('should warn when setting blocked task to in-progress', () => {
// Create dependency
const dep = helpers.taskMaster('add-task', ['Dependency', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
// Create blocked task
const task = helpers.taskMaster('add-task', [
'Blocked task',
'-m',
'-d',
depId
]);
const taskId = helpers.extractTaskId(task.stdout);
// Try to set to in-progress
const result = helpers.taskMaster('set-status', [taskId, 'in-progress']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Warning');
expect(result.stdout).toContain('has incomplete dependencies');
});
it('should unblock dependent tasks when dependency completes', () => {
// Create dependency chain
const task1 = helpers.taskMaster('add-task', ['First task', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', [
'Dependent task',
'-m',
'-d',
id1
]);
const id2 = helpers.extractTaskId(task2.stdout);
// Complete first task
const result = helpers.taskMaster('set-status', [id1, 'done']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Unblocked tasks:');
expect(result.stdout).toContain(`${id2} - Dependent task`);
});
it('should handle force flag for blocked tasks', () => {
// Create blocked task
const dep = helpers.taskMaster('add-task', ['Incomplete dep', '-m']);
const depId = helpers.extractTaskId(dep.stdout);
const task = helpers.taskMaster('add-task', [
'Force complete',
'-m',
'-d',
depId
]);
const taskId = helpers.extractTaskId(task.stdout);
// Force complete despite dependencies
const result = helpers.taskMaster('set-status', [
taskId,
'done',
'--force'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Force completing');
expect(result.stdout).not.toContain('Warning');
// Verify it's done
const showResult = helpers.taskMaster('show', [taskId]);
expect(showResult.stdout).toContain('Status: done');
});
});
describe('Status transitions', () => {
it('should prevent invalid status transitions', () => {
// Create completed task
const task = helpers.taskMaster('add-task', ['Completed task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
helpers.taskMaster('set-status', [taskId, 'done']);
// Try to set back to pending
const result = helpers.taskMaster('set-status', [taskId, 'pending']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Warning');
expect(result.stdout).toContain('Unusual status transition');
});
it('should allow reopening cancelled tasks', () => {
// Create and cancel task
const task = helpers.taskMaster('add-task', ['Cancelled task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
helpers.taskMaster('set-status', [taskId, 'cancelled']);
// Reopen task
const result = helpers.taskMaster('set-status', [taskId, 'pending']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Task reopened');
});
});
describe('Tag context', () => {
it('should update status for task in specific tag', () => {
// Create tag and task
helpers.taskMaster('add-tag', ['feature']);
helpers.taskMaster('use-tag', ['feature']);
const task = helpers.taskMaster('add-task', ['Feature task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
// Update status with tag context
const result = helpers.taskMaster('set-status', [
taskId,
'done',
'--tag',
'feature'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('[feature]');
expect(result.stdout).toContain('Status updated');
});
});
describe('Interactive features', () => {
it('should show next task suggestion after completing', () => {
// Create multiple tasks
helpers.taskMaster('add-task', ['Task 1', '-m', '-p', 'high']);
const task2 = helpers.taskMaster('add-task', [
'Task 2',
'-m',
'-p',
'high'
]);
const id2 = helpers.extractTaskId(task2.stdout);
// Complete first task
const result = helpers.taskMaster('set-status', [id2, 'done']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Next suggested task:');
expect(result.stdout).toContain('Task 1');
});
it('should provide time tracking prompts', () => {
// Create task
const task = helpers.taskMaster('add-task', ['Timed task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
// Start task
const startResult = helpers.taskMaster('set-status', [
taskId,
'in-progress'
]);
expect(startResult).toHaveExitCode(0);
expect(startResult.stdout).toContain('Started at:');
// Complete task
const endResult = helpers.taskMaster('set-status', [taskId, 'done']);
expect(endResult).toHaveExitCode(0);
expect(endResult.stdout).toContain('Time spent:');
});
});
describe('Error handling', () => {
it('should handle invalid task ID', () => {
const result = helpers.taskMaster('set-status', ['999', 'done'], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toMatch(/Task.*not found/i);
});
it('should handle invalid status value', () => {
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
const result = helpers.taskMaster(
'set-status',
[taskId, 'invalid-status'],
{ allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid status');
expect(result.stderr).toContain('pending, in-progress, done');
});
it('should handle missing required arguments', () => {
const result = helpers.taskMaster('set-status', [], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('required');
});
});
describe('Batch operations', () => {
it('should handle range-based updates', () => {
// Create sequential tasks
const ids = [];
for (let i = 0; i < 5; i++) {
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
ids.push(helpers.extractTaskId(result.stdout));
}
// Update range
const result = helpers.taskMaster('set-status', [
'--from',
ids[1],
'--to',
ids[3],
'in-progress'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('3 tasks updated');
// Verify middle tasks updated
for (let i = 1; i <= 3; i++) {
const showResult = helpers.taskMaster('show', [ids[i]]);
expect(showResult.stdout).toContain('Status: in-progress');
}
// Verify edge tasks not updated
const show0 = helpers.taskMaster('show', [ids[0]]);
expect(show0.stdout).toContain('Status: pending');
});
});
describe('Output options', () => {
it('should support quiet mode', () => {
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
const result = helpers.taskMaster('set-status', [taskId, 'done', '-q']);
expect(result).toHaveExitCode(0);
// Quiet mode should have minimal output
expect(result.stdout.split('\n').length).toBeLessThan(3);
});
it('should support JSON output', () => {
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
const taskId = helpers.extractTaskId(task.stdout);
const result = helpers.taskMaster('set-status', [
taskId,
'done',
'--json'
]);
expect(result).toHaveExitCode(0);
const json = JSON.parse(result.stdout);
expect(json.updated).toBe(1);
expect(json.tasks[0].id).toBe(parseInt(taskId));
expect(json.tasks[0].status).toBe('done');
});
});
});

View File

@@ -0,0 +1,411 @@
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { mkdtempSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { rmSync, existsSync, readFileSync } from 'fs';
describe('task-master show', () => {
let testDir;
let helpers;
beforeEach(() => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'tm-test-show-'));
process.chdir(testDir);
// Get helpers from global context
helpers = global.testHelpers;
// Copy .env if exists
const envPath = join(process.cwd(), '../../.env');
if (existsSync(envPath)) {
const envContent = readFileSync(envPath, 'utf-8');
helpers.writeFile('.env', envContent);
}
// Initialize task-master project
const initResult = helpers.taskMaster('init', ['-y']);
expect(initResult).toHaveExitCode(0);
// Ensure tasks.json exists
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (!helpers.fileExists(tasksPath)) {
helpers.writeFile(tasksPath, JSON.stringify({ tasks: [] }, null, 2));
}
});
afterEach(() => {
// Clean up test directory
process.chdir('..');
rmSync(testDir, { recursive: true, force: true });
});
describe('Basic functionality', () => {
it('should show task details by ID', () => {
// Create a test task
const addResult = helpers.taskMaster('add-task', [
'Test task for show command',
'-m',
'-p',
'high'
]);
expect(addResult).toHaveExitCode(0);
const taskId = helpers.extractTaskId(addResult.stdout);
// Show task details
const result = helpers.taskMaster('show', [taskId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Test task for show command');
expect(result.stdout).toContain(`Task ID: ${taskId}`);
expect(result.stdout).toContain('Priority: high');
expect(result.stdout).toContain('Status: pending');
});
it('should show error for non-existent task ID', () => {
const result = helpers.taskMaster('show', ['999'], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toMatch(/Task.*not found|does not exist/i);
});
it('should show task with all metadata', () => {
// Create task with dependencies and tags
const dep1 = helpers.taskMaster('add-task', ['Dependency 1', '-m']);
const depId1 = helpers.extractTaskId(dep1.stdout);
const addResult = helpers.taskMaster('add-task', [
'Complex task',
'-m',
'-p',
'medium',
'-d',
depId1,
'--tags',
'backend,api'
]);
const taskId = helpers.extractTaskId(addResult.stdout);
const result = helpers.taskMaster('show', [taskId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Dependencies:');
expect(result.stdout).toContain(depId1);
expect(result.stdout).toContain('Tags: backend, api');
});
});
describe('Subtask display', () => {
it('should show task with subtasks', () => {
// Create parent task
const parentResult = helpers.taskMaster('add-task', [
'Parent task with subtasks',
'-m'
]);
const parentId = helpers.extractTaskId(parentResult.stdout);
// Expand to add subtasks
const expandResult = helpers.taskMaster(
'expand',
['-i', parentId, '-n', '3'],
{ timeout: 60000 }
);
expect(expandResult).toHaveExitCode(0);
// Show parent task
const result = helpers.taskMaster('show', [parentId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Subtasks (3):');
expect(result.stdout).toMatch(/\d+\.1.*pending/);
expect(result.stdout).toMatch(/\d+\.2.*pending/);
expect(result.stdout).toMatch(/\d+\.3.*pending/);
});
it('should show subtask details directly', () => {
// Create parent task with subtasks
const parentResult = helpers.taskMaster('add-task', [
'Parent task',
'-m'
]);
const parentId = helpers.extractTaskId(parentResult.stdout);
const expandResult = helpers.taskMaster(
'expand',
['-i', parentId, '-n', '2'],
{ timeout: 60000 }
);
expect(expandResult).toHaveExitCode(0);
// Show specific subtask
const subtaskId = `${parentId}.1`;
const result = helpers.taskMaster('show', [subtaskId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain(`Subtask ID: ${subtaskId}`);
expect(result.stdout).toContain(`Parent Task: ${parentId}`);
});
});
describe('Dependency visualization', () => {
it('should show dependency graph', () => {
// Create dependency chain
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m', '-d', id1]);
const id2 = helpers.extractTaskId(task2.stdout);
const task3 = helpers.taskMaster('add-task', [
'Task 3',
'-m',
'-d',
`${id1},${id2}`
]);
const id3 = helpers.extractTaskId(task3.stdout);
// Show task with dependencies
const result = helpers.taskMaster('show', [id3]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Dependencies:');
expect(result.stdout).toContain(`${id1} - Task 1`);
expect(result.stdout).toContain(`${id2} - Task 2`);
expect(result.stdout).toMatch(/Status:.*pending/);
});
it('should show tasks depending on current task', () => {
// Create dependency chain
const task1 = helpers.taskMaster('add-task', ['Base task', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', [
'Dependent task',
'-m',
'-d',
id1
]);
const id2 = helpers.extractTaskId(task2.stdout);
// Show base task
const result = helpers.taskMaster('show', [id1]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Tasks depending on this:');
expect(result.stdout).toContain(`${id2} - Dependent task`);
});
});
describe('Status and progress', () => {
it('should show task progress for parent with subtasks', () => {
// Create parent task with subtasks
const parentResult = helpers.taskMaster('add-task', [
'Parent task',
'-m'
]);
const parentId = helpers.extractTaskId(parentResult.stdout);
// Expand to add subtasks
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
timeout: 60000
});
// Mark one subtask as done
helpers.taskMaster('set-status', [`${parentId}.1`, 'done']);
// Show parent task
const result = helpers.taskMaster('show', [parentId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toMatch(/Progress:.*1\/3.*33%/);
expect(result.stdout).toContain('└─ ✓'); // Done subtask indicator
});
it('should show different status indicators', () => {
// Create tasks with different statuses
const tasks = [
{ status: 'pending', title: 'Pending task' },
{ status: 'in-progress', title: 'In progress task' },
{ status: 'done', title: 'Done task' },
{ status: 'blocked', title: 'Blocked task' },
{ status: 'deferred', title: 'Deferred task' },
{ status: 'cancelled', title: 'Cancelled task' }
];
for (const { status, title } of tasks) {
const addResult = helpers.taskMaster('add-task', [title, '-m']);
const taskId = helpers.extractTaskId(addResult.stdout);
if (status !== 'pending') {
helpers.taskMaster('set-status', [taskId, status]);
}
const showResult = helpers.taskMaster('show', [taskId]);
expect(showResult).toHaveExitCode(0);
expect(showResult.stdout).toContain(`Status: ${status}`);
}
});
});
describe('Complexity information', () => {
it('should show complexity score when available', () => {
// Create a complex task
const addResult = helpers.taskMaster('add-task', [
'Build a distributed microservices architecture with Kubernetes',
'-m'
]);
const taskId = helpers.extractTaskId(addResult.stdout);
// Analyze complexity
const analyzeResult = helpers.taskMaster(
'analyze-complexity',
['-i', taskId],
{ timeout: 60000 }
);
if (analyzeResult.exitCode === 0) {
const result = helpers.taskMaster('show', [taskId]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toMatch(/Complexity Score:.*\d+/);
expect(result.stdout).toContain('Recommended subtasks:');
}
});
});
describe('Research and documentation', () => {
it('should show research notes if available', () => {
// Create task
const addResult = helpers.taskMaster('add-task', ['Research task', '-m']);
const taskId = helpers.extractTaskId(addResult.stdout);
// Add research notes (would normally be done via research command)
// For now, we'll check that the section appears
const result = helpers.taskMaster('show', [taskId]);
expect(result).toHaveExitCode(0);
// The show command should have a section for research notes
// even if empty
});
});
describe('Tag context', () => {
it('should show task from specific tag', () => {
// Create a new tag
helpers.taskMaster('add-tag', ['feature-branch']);
// Add task to feature tag
helpers.taskMaster('use-tag', ['feature-branch']);
const addResult = helpers.taskMaster('add-task', ['Feature task', '-m']);
const taskId = helpers.extractTaskId(addResult.stdout);
// Show task with tag context
const result = helpers.taskMaster('show', [
taskId,
'--tag',
'feature-branch'
]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Feature task');
expect(result.stdout).toContain('[feature-branch]');
});
});
describe('Output formats', () => {
it('should show task in JSON format', () => {
// Create task
const addResult = helpers.taskMaster('add-task', [
'JSON format test',
'-m'
]);
const taskId = helpers.extractTaskId(addResult.stdout);
// Show in JSON format
const result = helpers.taskMaster('show', [taskId, '--json']);
expect(result).toHaveExitCode(0);
// Parse JSON output
const jsonOutput = JSON.parse(result.stdout);
expect(jsonOutput.id).toBe(parseInt(taskId));
expect(jsonOutput.title).toBe('JSON format test');
expect(jsonOutput.status).toBe('pending');
});
it('should show minimal output with quiet flag', () => {
// Create task
const addResult = helpers.taskMaster('add-task', [
'Quiet mode test',
'-m'
]);
const taskId = helpers.extractTaskId(addResult.stdout);
// Show in quiet mode
const result = helpers.taskMaster('show', [taskId, '-q']);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Quiet mode test');
// Should have less output than normal
expect(result.stdout.split('\n').length).toBeLessThan(20);
});
});
describe('Navigation suggestions', () => {
it('should show next/previous task suggestions', () => {
// Create multiple tasks
const task1 = helpers.taskMaster('add-task', ['First task', '-m']);
const id1 = helpers.extractTaskId(task1.stdout);
const task2 = helpers.taskMaster('add-task', ['Second task', '-m']);
const id2 = helpers.extractTaskId(task2.stdout);
const task3 = helpers.taskMaster('add-task', ['Third task', '-m']);
const id3 = helpers.extractTaskId(task3.stdout);
// Show middle task
const result = helpers.taskMaster('show', [id2]);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Navigation:');
expect(result.stdout).toContain(`Previous: ${id1}`);
expect(result.stdout).toContain(`Next: ${id3}`);
});
});
describe('Error handling', () => {
it('should handle invalid task ID format', () => {
const result = helpers.taskMaster('show', ['invalid-id'], {
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid task ID');
});
it('should handle missing tasks file', () => {
const result = helpers.taskMaster(
'show',
['1', '--file', 'non-existent.json'],
{ allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Error');
});
});
describe('Performance', () => {
it('should show task with many subtasks efficiently', () => {
// Create parent task
const parentResult = helpers.taskMaster('add-task', [
'Large parent task',
'-m'
]);
const parentId = helpers.extractTaskId(parentResult.stdout);
// Expand with many subtasks
const expandResult = helpers.taskMaster(
'expand',
['-i', parentId, '-n', '10'],
{ timeout: 120000 }
);
expect(expandResult).toHaveExitCode(0);
// Show should handle many subtasks efficiently
const startTime = Date.now();
const result = helpers.taskMaster('show', [parentId]);
const endTime = Date.now();
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Subtasks (10):');
expect(endTime - startTime).toBeLessThan(2000); // Should be fast
});
});
});

View File

@@ -3,7 +3,14 @@
* Tests all aspects of subtask updates including AI-powered updates
*/
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
@@ -16,11 +23,11 @@ describe('update-subtask command', () => {
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-update-subtask-'));
// Initialize test helpers
const context = global.createTestContext('update-subtask');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
@@ -28,11 +35,13 @@ describe('update-subtask command', () => {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
// Create a parent task with subtask
const parentResult = await helpers.taskMaster(
'add-task',
@@ -40,7 +49,7 @@ describe('update-subtask command', () => {
{ cwd: testDir }
);
parentTaskId = helpers.extractTaskId(parentResult.stdout);
// Create a subtask
const subtaskResult = await helpers.taskMaster(
'add-subtask',
@@ -66,12 +75,14 @@ describe('update-subtask command', () => {
[subtaskId, 'Updated subtask title'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated subtask');
// Verify update
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('Updated subtask title');
});
@@ -81,11 +92,13 @@ describe('update-subtask command', () => {
[subtaskId, '--notes', 'Implementation details: Use async/await'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify notes were added
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('async/await');
});
@@ -95,11 +108,13 @@ describe('update-subtask command', () => {
[subtaskId, '--status', 'completed'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify status update
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('completed');
});
});
@@ -111,16 +126,18 @@ describe('update-subtask command', () => {
[subtaskId, '--prompt', 'Add implementation steps and best practices'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated subtask');
// Verify AI enhanced the subtask
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
const parentTask = tasks.master.tasks.find(t => t.id === parseInt(parentTaskId));
const subtask = parentTask.subtasks.find(s => s.id === subtaskId);
const parentTask = tasks.master.tasks.find(
(t) => t.id === parseInt(parentTaskId)
);
const subtask = parentTask.subtasks.find((s) => s.id === subtaskId);
// Should have more detailed content
expect(subtask.title.length).toBeGreaterThan(20);
}, 60000);
@@ -128,15 +145,22 @@ describe('update-subtask command', () => {
it('should enhance subtask with technical details', async () => {
const result = await helpers.taskMaster(
'update-subtask',
[subtaskId, '--prompt', 'Add technical requirements and edge cases to consider'],
[
subtaskId,
'--prompt',
'Add technical requirements and edge cases to consider'
],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Check that subtask was enhanced
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const hasEnhancement = showResult.stdout.toLowerCase().includes('requirement') ||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
const hasEnhancement =
showResult.stdout.toLowerCase().includes('requirement') ||
showResult.stdout.toLowerCase().includes('edge case') ||
showResult.stdout.toLowerCase().includes('consider');
expect(hasEnhancement).toBe(true);
@@ -147,17 +171,21 @@ describe('update-subtask command', () => {
'update-subtask',
[
subtaskId,
'--prompt', 'Add industry best practices for error handling',
'--prompt',
'Add industry best practices for error handling',
'--research'
],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
// Research mode should add comprehensive content
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const hasResearchContent = showResult.stdout.toLowerCase().includes('error') ||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
const hasResearchContent =
showResult.stdout.toLowerCase().includes('error') ||
showResult.stdout.toLowerCase().includes('handling') ||
showResult.stdout.toLowerCase().includes('practice');
expect(hasResearchContent).toBe(true);
@@ -174,25 +202,27 @@ describe('update-subtask command', () => {
);
const match = subtask2Result.stdout.match(/subtask #?(\d+\.\d+)/i);
const subtaskId2 = match ? match[1] : '1.2';
// Update first subtask
await helpers.taskMaster(
'update-subtask',
[subtaskId, 'First subtask updated'],
{ cwd: testDir }
);
// Update second subtask
const result = await helpers.taskMaster(
'update-subtask',
[subtaskId2, 'Second subtask updated'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify both updates
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('First subtask updated');
expect(showResult.stdout).toContain('Second subtask updated');
});
@@ -205,11 +235,13 @@ describe('update-subtask command', () => {
[subtaskId, '--priority', 'high'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify priority was set
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('high');
});
@@ -219,11 +251,13 @@ describe('update-subtask command', () => {
[subtaskId, '--estimated-time', '2h'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify estimated time was set
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('2h');
});
@@ -233,11 +267,13 @@ describe('update-subtask command', () => {
[subtaskId, '--assignee', 'john.doe@example.com'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify assignee was set
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('john.doe');
});
});
@@ -249,15 +285,18 @@ describe('update-subtask command', () => {
[
subtaskId,
'New comprehensive title',
'--notes', 'Additional implementation details'
'--notes',
'Additional implementation details'
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify both updates
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('New comprehensive title');
expect(showResult.stdout).toContain('Additional implementation details');
});
@@ -267,18 +306,23 @@ describe('update-subtask command', () => {
'update-subtask',
[
subtaskId,
'--status', 'in_progress',
'--prompt', 'Add acceptance criteria'
'--status',
'in_progress',
'--prompt',
'Add acceptance criteria'
],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Verify both manual and AI updates
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('in_progress');
const hasAcceptanceCriteria = showResult.stdout.toLowerCase().includes('acceptance') ||
const hasAcceptanceCriteria =
showResult.stdout.toLowerCase().includes('acceptance') ||
showResult.stdout.toLowerCase().includes('criteria');
expect(hasAcceptanceCriteria).toBe(true);
}, 60000);
@@ -292,18 +336,20 @@ describe('update-subtask command', () => {
[subtaskId, '--notes', 'Initial notes.'],
{ cwd: testDir }
);
// Then append
const result = await helpers.taskMaster(
'update-subtask',
[subtaskId, '--append-notes', '\nAdditional considerations.'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify notes were appended
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('Initial notes');
expect(showResult.stdout).toContain('Additional considerations');
});
@@ -319,18 +365,20 @@ describe('update-subtask command', () => {
);
const match = nestedResult.stdout.match(/subtask #?(\d+\.\d+\.\d+)/i);
const nestedId = match ? match[1] : '1.1.1';
// Update nested subtask
const result = await helpers.taskMaster(
'update-subtask',
[nestedId, 'Updated nested subtask'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify update
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('Updated nested subtask');
});
});
@@ -339,7 +387,7 @@ describe('update-subtask command', () => {
it('should update subtask in specific tag', async () => {
// Create a tag and add task to it
await helpers.taskMaster('add-tag', ['feature-y'], { cwd: testDir });
// Create task in tag
const tagTaskResult = await helpers.taskMaster(
'add-task',
@@ -347,7 +395,7 @@ describe('update-subtask command', () => {
{ cwd: testDir }
);
const tagTaskId = helpers.extractTaskId(tagTaskResult.stdout);
// Add subtask to tagged task
const tagSubtaskResult = await helpers.taskMaster(
'add-subtask',
@@ -356,16 +404,16 @@ describe('update-subtask command', () => {
);
const match = tagSubtaskResult.stdout.match(/subtask #?(\d+\.\d+)/i);
const tagSubtaskId = match ? match[1] : '1.1';
// Update subtask in specific tag
const result = await helpers.taskMaster(
'update-subtask',
[tagSubtaskId, 'Updated in feature tag', '--tag', 'feature-y'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify update in correct tag
const showResult = await helpers.taskMaster(
'show',
@@ -383,9 +431,9 @@ describe('update-subtask command', () => {
[subtaskId, 'JSON test update', '--output', 'json'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Output should be valid JSON
const jsonOutput = JSON.parse(result.stdout);
expect(jsonOutput.success).toBe(true);
@@ -402,7 +450,7 @@ describe('update-subtask command', () => {
['99.99', 'This should fail'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('not found');
});
@@ -413,7 +461,7 @@ describe('update-subtask command', () => {
['invalid-id', 'This should fail'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid subtask ID');
});
@@ -424,7 +472,7 @@ describe('update-subtask command', () => {
[subtaskId, '--priority', 'invalid-priority'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid priority');
});
@@ -435,7 +483,7 @@ describe('update-subtask command', () => {
[subtaskId, '--status', 'invalid-status'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid status');
});
@@ -444,53 +492,60 @@ describe('update-subtask command', () => {
describe('Performance and edge cases', () => {
it('should handle very long subtask titles', async () => {
const longTitle = 'This is a very detailed subtask title. '.repeat(10);
const result = await helpers.taskMaster(
'update-subtask',
[subtaskId, longTitle],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify long title was saved
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
const parentTask = tasks.master.tasks.find(t => t.id === parseInt(parentTaskId));
const subtask = parentTask.subtasks.find(s => s.id === subtaskId);
const parentTask = tasks.master.tasks.find(
(t) => t.id === parseInt(parentTaskId)
);
const subtask = parentTask.subtasks.find((s) => s.id === subtaskId);
expect(subtask.title).toBe(longTitle);
});
it('should update subtask without affecting parent task', async () => {
const originalParentTitle = 'Parent task';
// Update subtask
const result = await helpers.taskMaster(
'update-subtask',
[subtaskId, 'Completely different subtask'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify parent task remains unchanged
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain(originalParentTitle);
});
it('should handle subtask updates with special characters', async () => {
const specialTitle = 'Subtask with special chars: @#$% & "quotes" \'apostrophes\'';
const specialTitle =
'Subtask with special chars: @#$% & "quotes" \'apostrophes\'';
const result = await helpers.taskMaster(
'update-subtask',
[subtaskId, specialTitle],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify special characters were preserved
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('@#$%');
});
});
@@ -502,13 +557,15 @@ describe('update-subtask command', () => {
[subtaskId, 'Dry run test', '--dry-run'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('DRY RUN');
expect(result.stdout).toContain('Would update');
// Verify subtask was NOT actually updated
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout).not.toContain('Dry run test');
expect(showResult.stdout).toContain('Initial subtask');
});
@@ -522,44 +579,47 @@ describe('update-subtask command', () => {
[subtaskId, '--prompt', 'Add detailed implementation steps'],
{ cwd: testDir, timeout: 45000 }
);
// Expand parent task
const expandResult = await helpers.taskMaster(
'expand',
['--id', parentTaskId],
{ cwd: testDir, timeout: 45000 }
);
expect(expandResult).toHaveExitCode(0);
expect(expandResult.stdout).toContain('Expanded task');
// Should include updated subtask information
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const hasImplementationSteps = showResult.stdout.toLowerCase().includes('implementation') ||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
const hasImplementationSteps =
showResult.stdout.toLowerCase().includes('implementation') ||
showResult.stdout.toLowerCase().includes('step');
expect(hasImplementationSteps).toBe(true);
}, 90000);
it('should update subtask after parent task status change', async () => {
// Change parent task status
await helpers.taskMaster(
'set-status',
[parentTaskId, 'in_progress'],
{ cwd: testDir }
);
await helpers.taskMaster('set-status', [parentTaskId, 'in_progress'], {
cwd: testDir
});
// Update subtask
const result = await helpers.taskMaster(
'update-subtask',
[subtaskId, '--status', 'in_progress'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify both statuses
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [parentTaskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('in_progress');
});
});
});
});

View File

@@ -3,7 +3,14 @@
* Tests all aspects of single task updates including AI-powered updates
*/
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
@@ -15,11 +22,11 @@ describe('update-task command', () => {
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-update-task-'));
// Initialize test helpers
const context = global.createTestContext('update-task');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
@@ -27,11 +34,13 @@ describe('update-task command', () => {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
// Create a test task for updates
const addResult = await helpers.taskMaster(
'add-task',
@@ -55,12 +64,14 @@ describe('update-task command', () => {
[taskId, '--description', 'Updated task description with more details'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated task');
// Verify update
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('Updated task description');
});
@@ -70,11 +81,13 @@ describe('update-task command', () => {
[taskId, '--title', 'Completely new title'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify update
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('Completely new title');
});
@@ -84,11 +97,13 @@ describe('update-task command', () => {
[taskId, '--priority', 'high'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify update
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('high');
});
@@ -98,11 +113,13 @@ describe('update-task command', () => {
[taskId, '--details', 'Implementation notes: Use async/await pattern'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify update
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('async/await');
});
});
@@ -114,13 +131,16 @@ describe('update-task command', () => {
[taskId, '--prompt', 'Add security considerations and best practices'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated task');
// Verify AI added security content
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const hasSecurityInfo = showResult.stdout.toLowerCase().includes('security') ||
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
const hasSecurityInfo =
showResult.stdout.toLowerCase().includes('security') ||
showResult.stdout.toLowerCase().includes('practice');
expect(hasSecurityInfo).toBe(true);
}, 60000);
@@ -128,17 +148,23 @@ describe('update-task command', () => {
it('should enhance task with AI suggestions', async () => {
const result = await helpers.taskMaster(
'update-task',
[taskId, '--prompt', 'Break this down into subtasks and add implementation details'],
[
taskId,
'--prompt',
'Break this down into subtasks and add implementation details'
],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Check that task was enhanced
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
const updatedTask = tasks.master.tasks.find(t => t.id === parseInt(taskId));
const updatedTask = tasks.master.tasks.find(
(t) => t.id === parseInt(taskId)
);
// Should have more detailed content
expect(updatedTask.details.length).toBeGreaterThan(50);
}, 60000);
@@ -147,17 +173,20 @@ describe('update-task command', () => {
const result = await helpers.taskMaster(
'update-task',
[
taskId,
'--prompt', 'Add current industry best practices for authentication',
taskId,
'--prompt',
'Add current industry best practices for authentication',
'--research'
],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
// Research mode should add comprehensive content
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.length).toBeGreaterThan(500);
}, 120000);
});
@@ -168,18 +197,24 @@ describe('update-task command', () => {
'update-task',
[
taskId,
'--title', 'New comprehensive title',
'--description', 'New detailed description',
'--priority', 'high',
'--details', 'Additional implementation notes'
'--title',
'New comprehensive title',
'--description',
'New detailed description',
'--priority',
'high',
'--details',
'Additional implementation notes'
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify all updates
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('New comprehensive title');
expect(showResult.stdout).toContain('New detailed description');
expect(showResult.stdout.toLowerCase()).toContain('high');
@@ -191,18 +226,23 @@ describe('update-task command', () => {
'update-task',
[
taskId,
'--priority', 'high',
'--prompt', 'Add technical requirements and dependencies'
'--priority',
'high',
'--prompt',
'Add technical requirements and dependencies'
],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Verify both manual and AI updates
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('high');
const hasTechnicalInfo = showResult.stdout.toLowerCase().includes('requirement') ||
const hasTechnicalInfo =
showResult.stdout.toLowerCase().includes('requirement') ||
showResult.stdout.toLowerCase().includes('dependenc');
expect(hasTechnicalInfo).toBe(true);
}, 60000);
@@ -215,11 +255,13 @@ describe('update-task command', () => {
[taskId, '--add-tags', 'backend,api,urgent'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify tags were added
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('backend');
expect(showResult.stdout).toContain('api');
expect(showResult.stdout).toContain('urgent');
@@ -232,18 +274,20 @@ describe('update-task command', () => {
[taskId, '--add-tags', 'frontend,ui,design'],
{ cwd: testDir }
);
// Then remove some
const result = await helpers.taskMaster(
'update-task',
[taskId, '--remove-tags', 'ui,design'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify tags were removed
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('frontend');
expect(showResult.stdout).not.toContain('ui');
expect(showResult.stdout).not.toContain('design');
@@ -253,17 +297,19 @@ describe('update-task command', () => {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 7);
const dateStr = futureDate.toISOString().split('T')[0];
const result = await helpers.taskMaster(
'update-task',
[taskId, '--due-date', dateStr],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify due date was set
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain(dateStr);
});
@@ -273,11 +319,13 @@ describe('update-task command', () => {
[taskId, '--estimated-time', '4h'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify estimated time was set
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('4h');
});
});
@@ -289,11 +337,13 @@ describe('update-task command', () => {
[taskId, '--status', 'in_progress'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify status change
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('in_progress');
});
@@ -303,25 +353,35 @@ describe('update-task command', () => {
[taskId, '--status', 'completed'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify completion
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('completed');
});
it('should mark task as blocked with reason', async () => {
const result = await helpers.taskMaster(
'update-task',
[taskId, '--status', 'blocked', '--blocked-reason', 'Waiting for API access'],
[
taskId,
'--status',
'blocked',
'--blocked-reason',
'Waiting for API access'
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify blocked status and reason
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout.toLowerCase()).toContain('blocked');
expect(showResult.stdout).toContain('Waiting for API access');
});
@@ -334,11 +394,13 @@ describe('update-task command', () => {
[taskId, '--append-description', '\nAdditional requirements added.'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify description was appended
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('Task to be updated');
expect(showResult.stdout).toContain('Additional requirements added');
});
@@ -350,18 +412,20 @@ describe('update-task command', () => {
[taskId, '--details', 'Initial implementation notes.'],
{ cwd: testDir }
);
// Then append
const result = await helpers.taskMaster(
'update-task',
[taskId, '--append-details', '\nPerformance considerations added.'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify details were appended
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain('Initial implementation notes');
expect(showResult.stdout).toContain('Performance considerations added');
});
@@ -376,20 +440,30 @@ describe('update-task command', () => {
['--prompt', 'Task in feature-x', '--tag', 'feature-x'],
{ cwd: testDir }
);
// Get task ID from feature-x tag
const listResult = await helpers.taskMaster('list', ['--tag', 'feature-x'], { cwd: testDir });
const listResult = await helpers.taskMaster(
'list',
['--tag', 'feature-x'],
{ cwd: testDir }
);
const featureTaskId = helpers.extractTaskId(listResult.stdout);
// Update task in specific tag
const result = await helpers.taskMaster(
'update-task',
[featureTaskId, '--description', 'Updated in feature tag', '--tag', 'feature-x'],
[
featureTaskId,
'--description',
'Updated in feature tag',
'--tag',
'feature-x'
],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify update in correct tag
const showResult = await helpers.taskMaster(
'show',
@@ -407,9 +481,9 @@ describe('update-task command', () => {
[taskId, '--description', 'JSON test update', '--output', 'json'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Output should be valid JSON
const jsonOutput = JSON.parse(result.stdout);
expect(jsonOutput.success).toBe(true);
@@ -425,7 +499,7 @@ describe('update-task command', () => {
['99999', '--description', 'This should fail'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('not found');
});
@@ -436,7 +510,7 @@ describe('update-task command', () => {
[taskId, '--priority', 'invalid-priority'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid priority');
});
@@ -447,18 +521,17 @@ describe('update-task command', () => {
[taskId, '--status', 'invalid-status'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid status');
});
it('should fail without any update parameters', async () => {
const result = await helpers.taskMaster(
'update-task',
[taskId],
{ cwd: testDir, allowFailure: true }
);
const result = await helpers.taskMaster('update-task', [taskId], {
cwd: testDir,
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('No updates specified');
});
@@ -466,20 +539,24 @@ describe('update-task command', () => {
describe('Performance and edge cases', () => {
it('should handle very long descriptions', async () => {
const longDescription = 'This is a very detailed description. '.repeat(50);
const longDescription = 'This is a very detailed description. '.repeat(
50
);
const result = await helpers.taskMaster(
'update-task',
[taskId, '--description', longDescription],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify long description was saved
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
const updatedTask = tasks.master.tasks.find(t => t.id === parseInt(taskId));
const updatedTask = tasks.master.tasks.find(
(t) => t.id === parseInt(taskId)
);
expect(updatedTask.description).toBe(longDescription);
});
@@ -491,24 +568,26 @@ describe('update-task command', () => {
{ cwd: testDir }
);
const depId = helpers.extractTaskId(depResult.stdout);
await helpers.taskMaster(
'add-dependency',
['--id', taskId, '--depends-on', depId],
{ cwd: testDir }
);
// Update the task
const result = await helpers.taskMaster(
'update-task',
[taskId, '--description', 'Updated with dependencies intact'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
// Verify dependency is preserved
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).toContain(depId);
});
});
@@ -520,13 +599,15 @@ describe('update-task command', () => {
[taskId, '--description', 'Dry run test', '--dry-run'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('DRY RUN');
expect(result.stdout).toContain('Would update');
// Verify task was NOT actually updated
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
expect(showResult.stdout).not.toContain('Dry run test');
});
});
@@ -539,16 +620,16 @@ describe('update-task command', () => {
[taskId, '--prompt', 'Add implementation steps'],
{ cwd: testDir, timeout: 45000 }
);
// Then expand it
const expandResult = await helpers.taskMaster(
'expand',
['--id', taskId],
{ cwd: testDir, timeout: 45000 }
);
expect(expandResult).toHaveExitCode(0);
expect(expandResult.stdout).toContain('Expanded task');
}, 90000);
});
});
});

View File

@@ -3,7 +3,14 @@
* Tests all aspects of bulk task updates including AI-powered updates
*/
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
@@ -14,11 +21,11 @@ describe('update-tasks command', () => {
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-update-tasks-'));
// Initialize test helpers
const context = global.createTestContext('update-tasks');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
@@ -26,11 +33,13 @@ describe('update-tasks command', () => {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
// Create some test tasks for bulk updates
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasksData = {
@@ -38,27 +47,27 @@ describe('update-tasks command', () => {
tasks: [
{
id: 1,
title: "Setup authentication",
description: "Implement user authentication",
priority: "medium",
status: "pending",
details: "Basic auth implementation"
title: 'Setup authentication',
description: 'Implement user authentication',
priority: 'medium',
status: 'pending',
details: 'Basic auth implementation'
},
{
id: 2,
title: "Create database schema",
description: "Design database structure",
priority: "high",
status: "pending",
details: "PostgreSQL schema"
title: 'Create database schema',
description: 'Design database structure',
priority: 'high',
status: 'pending',
details: 'PostgreSQL schema'
},
{
id: 3,
title: "Build API endpoints",
description: "RESTful API development",
priority: "medium",
status: "in_progress",
details: "Express.js endpoints"
title: 'Build API endpoints',
description: 'RESTful API development',
priority: 'medium',
status: 'in_progress',
details: 'Express.js endpoints'
}
]
}
@@ -81,18 +90,18 @@ describe('update-tasks command', () => {
['--prompt', 'Add security considerations to all tasks'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated');
expect(result.stdout).toContain('task');
// Verify tasks were updated
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
// Check that tasks have been modified (details should mention security)
const hasSecurityUpdates = tasks.master.tasks.some(t =>
t.details && t.details.toLowerCase().includes('security')
const hasSecurityUpdates = tasks.master.tasks.some(
(t) => t.details && t.details.toLowerCase().includes('security')
);
expect(hasSecurityUpdates).toBe(true);
}, 60000);
@@ -103,7 +112,7 @@ describe('update-tasks command', () => {
['--ids', '1,3', '--prompt', 'Add performance optimization notes'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated 2 task');
}, 60000);
@@ -114,18 +123,24 @@ describe('update-tasks command', () => {
['--status', 'pending', '--prompt', 'Add estimated time requirements'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Should update tasks 1 and 2 (pending status)
expect(result.stdout).toContain('Updated 2 task');
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
// Verify only pending tasks were updated
const pendingTasks = tasks.master.tasks.filter(t => t.status === 'pending');
const hasTimeEstimates = pendingTasks.some(t =>
t.details && (t.details.includes('time') || t.details.includes('hour') || t.details.includes('day'))
const pendingTasks = tasks.master.tasks.filter(
(t) => t.status === 'pending'
);
const hasTimeEstimates = pendingTasks.some(
(t) =>
t.details &&
(t.details.includes('time') ||
t.details.includes('hour') ||
t.details.includes('day'))
);
expect(hasTimeEstimates).toBe(true);
}, 60000);
@@ -136,7 +151,7 @@ describe('update-tasks command', () => {
['--priority', 'medium', '--prompt', 'Add testing requirements'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Should update tasks 1 and 3 (medium priority)
expect(result.stdout).toContain('Updated 2 task');
@@ -147,25 +162,22 @@ describe('update-tasks command', () => {
it('should update tasks with research-backed information', async () => {
const result = await helpers.taskMaster(
'update-tasks',
[
'--ids', '1',
'--prompt', 'Add OAuth2 best practices',
'--research'
],
['--ids', '1', '--prompt', 'Add OAuth2 best practices', '--research'],
{ cwd: testDir, timeout: 90000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated');
// Research mode should produce more detailed updates
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
const authTask = tasks.master.tasks.find(t => t.id === 1);
const authTask = tasks.master.tasks.find((t) => t.id === 1);
// Check for detailed OAuth2 information
expect(authTask.details.length).toBeGreaterThan(100);
const hasOAuthInfo = authTask.details.toLowerCase().includes('oauth') ||
const hasOAuthInfo =
authTask.details.toLowerCase().includes('oauth') ||
authTask.details.toLowerCase().includes('authorization');
expect(hasOAuthInfo).toBe(true);
}, 120000);
@@ -179,34 +191,37 @@ describe('update-tasks command', () => {
currentTasks.master.tasks.push(
{
id: 4,
title: "Security audit",
description: "Perform security review",
priority: "high",
status: "pending",
details: "Initial security check"
title: 'Security audit',
description: 'Perform security review',
priority: 'high',
status: 'pending',
details: 'Initial security check'
},
{
id: 5,
title: "Performance testing",
description: "Load testing",
priority: "high",
status: "in_progress",
details: "Using JMeter"
title: 'Performance testing',
description: 'Load testing',
priority: 'high',
status: 'in_progress',
details: 'Using JMeter'
}
);
writeFileSync(tasksPath, JSON.stringify(currentTasks, null, 2));
// Update only high priority pending tasks
const result = await helpers.taskMaster(
'update-tasks',
[
'--status', 'pending',
'--priority', 'high',
'--prompt', 'Add compliance requirements'
'--status',
'pending',
'--priority',
'high',
'--prompt',
'Add compliance requirements'
],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Should only update task 2 and 4
expect(result.stdout).toContain('Updated 2 task');
@@ -216,12 +231,14 @@ describe('update-tasks command', () => {
const result = await helpers.taskMaster(
'update-tasks',
[
'--status', 'completed',
'--prompt', 'This should not update anything'
'--status',
'completed',
'--prompt',
'This should not update anything'
],
{ cwd: testDir, timeout: 30000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('No tasks found matching the criteria');
}, 45000);
@@ -231,32 +248,29 @@ describe('update-tasks command', () => {
it('should update tasks in specific tag', async () => {
// Create a new tag with tasks
await helpers.taskMaster('add-tag', ['feature-x'], { cwd: testDir });
// Add task to the tag
await helpers.taskMaster(
'add-task',
['--prompt', 'Feature X implementation', '--tag', 'feature-x'],
{ cwd: testDir }
);
const result = await helpers.taskMaster(
'update-tasks',
[
'--tag', 'feature-x',
'--prompt', 'Add deployment considerations'
],
['--tag', 'feature-x', '--prompt', 'Add deployment considerations'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated');
// Verify task in tag was updated
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
const featureXTasks = tasks['feature-x'].tasks;
const hasDeploymentInfo = featureXTasks.some(t =>
t.details && t.details.toLowerCase().includes('deploy')
const hasDeploymentInfo = featureXTasks.some(
(t) => t.details && t.details.toLowerCase().includes('deploy')
);
expect(hasDeploymentInfo).toBe(true);
}, 60000);
@@ -265,14 +279,14 @@ describe('update-tasks command', () => {
// Create multiple tags
await helpers.taskMaster('add-tag', ['backend'], { cwd: testDir });
await helpers.taskMaster('add-tag', ['frontend'], { cwd: testDir });
// Update all tasks across all tags
const result = await helpers.taskMaster(
'update-tasks',
['--prompt', 'Add error handling strategies'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated');
}, 60000);
@@ -283,15 +297,18 @@ describe('update-tasks command', () => {
const result = await helpers.taskMaster(
'update-tasks',
[
'--ids', '1',
'--prompt', 'Add monitoring requirements',
'--output', 'json'
'--ids',
'1',
'--prompt',
'Add monitoring requirements',
'--output',
'json'
],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Output should be valid JSON
const jsonOutput = JSON.parse(result.stdout);
expect(jsonOutput.success).toBe(true);
@@ -302,12 +319,11 @@ describe('update-tasks command', () => {
describe('Error handling', () => {
it('should fail without prompt', async () => {
const result = await helpers.taskMaster(
'update-tasks',
['--ids', '1'],
{ cwd: testDir, allowFailure: true }
);
const result = await helpers.taskMaster('update-tasks', ['--ids', '1'], {
cwd: testDir,
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('prompt');
});
@@ -318,7 +334,7 @@ describe('update-tasks command', () => {
['--ids', '999,1000', '--prompt', 'Update non-existent tasks'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('No tasks found');
});
@@ -329,7 +345,7 @@ describe('update-tasks command', () => {
['--status', 'invalid-status', '--prompt', 'Test invalid status'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid status');
});
@@ -340,7 +356,7 @@ describe('update-tasks command', () => {
['--priority', 'urgent', '--prompt', 'Test invalid priority'],
{ cwd: testDir, allowFailure: true }
);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Invalid priority');
});
@@ -351,7 +367,7 @@ describe('update-tasks command', () => {
// Add many tasks
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const currentTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
for (let i = 4; i <= 20; i++) {
currentTasks.master.tasks.push({
id: i,
@@ -363,7 +379,7 @@ describe('update-tasks command', () => {
});
}
writeFileSync(tasksPath, JSON.stringify(currentTasks, null, 2));
const startTime = Date.now();
const result = await helpers.taskMaster(
'update-tasks',
@@ -371,7 +387,7 @@ describe('update-tasks command', () => {
{ cwd: testDir, timeout: 120000 }
);
const duration = Date.now() - startTime;
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Updated 20 task');
expect(duration).toBeLessThan(120000); // Should complete within 2 minutes
@@ -384,15 +400,15 @@ describe('update-tasks command', () => {
currentTasks.master.tasks[1].dependencies = [1];
currentTasks.master.tasks[2].dependencies = [1, 2];
writeFileSync(tasksPath, JSON.stringify(currentTasks, null, 2));
const result = await helpers.taskMaster(
'update-tasks',
['--prompt', 'Clarify implementation order'],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
// Verify dependencies are preserved
const updatedTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
expect(updatedTasks.master.tasks[1].dependencies).toEqual([1]);
@@ -405,22 +421,24 @@ describe('update-tasks command', () => {
const result = await helpers.taskMaster(
'update-tasks',
[
'--ids', '1,2',
'--prompt', 'Add test coverage requirements',
'--ids',
'1,2',
'--prompt',
'Add test coverage requirements',
'--dry-run'
],
{ cwd: testDir, timeout: 45000 }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('DRY RUN');
expect(result.stdout).toContain('Would update');
// Verify tasks were NOT actually updated
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
const hasTestCoverage = tasks.master.tasks.some(t =>
t.details && t.details.toLowerCase().includes('test coverage')
const hasTestCoverage = tasks.master.tasks.some(
(t) => t.details && t.details.toLowerCase().includes('test coverage')
);
expect(hasTestCoverage).toBe(false);
}, 60000);
@@ -434,16 +452,15 @@ describe('update-tasks command', () => {
['--ids', '1', '--prompt', 'Add detailed specifications'],
{ cwd: testDir, timeout: 45000 }
);
// Then expand the updated task
const expandResult = await helpers.taskMaster(
'expand',
['--id', '1'],
{ cwd: testDir, timeout: 45000 }
);
const expandResult = await helpers.taskMaster('expand', ['--id', '1'], {
cwd: testDir,
timeout: 45000
});
expect(expandResult).toHaveExitCode(0);
expect(expandResult.stdout).toContain('Expanded task');
}, 90000);
});
});
});

View File

@@ -0,0 +1,207 @@
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '../../../..');
describe('MCP Server - get_tasks tool', () => {
let client;
let transport;
beforeAll(async () => {
// Create transport by spawning the server
transport = new StdioClientTransport({
command: 'node',
args: ['mcp-server/server.js'],
env: process.env,
cwd: projectRoot
});
// Create client
client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: {
sampling: {}
}
}
);
// Connect to server
await client.connect(transport);
});
afterAll(async () => {
if (client) {
await client.close();
}
});
it('should connect to MCP server successfully', async () => {
const tools = await client.listTools();
expect(tools.tools).toBeDefined();
expect(tools.tools.length).toBeGreaterThan(0);
const toolNames = tools.tools.map((t) => t.name);
expect(toolNames).toContain('get_tasks');
expect(toolNames).toContain('initialize_project');
});
it('should initialize project successfully', async () => {
const result = await client.callTool({
name: 'initialize_project',
arguments: {
projectRoot: projectRoot
}
});
expect(result.content).toBeDefined();
expect(result.content[0].type).toBe('text');
expect(result.content[0].text).toContain(
'Project initialized successfully'
);
});
it('should handle missing tasks file gracefully', async () => {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/non-existent-tasks.json'
}
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Error');
});
it('should get tasks with fixture data', async () => {
// Create a temporary tasks file with proper structure
const testTasksPath = join(projectRoot, '.taskmaster/test-tasks.json');
const testTasks = {
tasks: [
{
id: 'test-001',
description: 'Test task 1',
status: 'pending',
priority: 'high',
estimatedMinutes: 30,
actualMinutes: 0,
dependencies: [],
tags: ['test'],
subtasks: [
{
id: 'test-001-1',
description: 'Test subtask 1.1',
status: 'pending',
priority: 'medium',
estimatedMinutes: 15,
actualMinutes: 0
}
]
},
{
id: 'test-002',
description: 'Test task 2',
status: 'in_progress',
priority: 'medium',
estimatedMinutes: 60,
actualMinutes: 15,
dependencies: ['test-001'],
tags: ['test', 'demo'],
subtasks: []
}
]
};
// Write test tasks file
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
try {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/test-tasks.json',
withSubtasks: true
}
});
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toContain('2 tasks found');
expect(result.content[0].text).toContain('Test task 1');
expect(result.content[0].text).toContain('Test task 2');
expect(result.content[0].text).toContain('Test subtask 1.1');
} finally {
// Cleanup
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
}
});
it('should filter tasks by status', async () => {
// Create a temporary tasks file
const testTasksPath = join(
projectRoot,
'.taskmaster/test-status-tasks.json'
);
const testTasks = {
tasks: [
{
id: 'status-001',
description: 'Pending task',
status: 'pending',
priority: 'high',
estimatedMinutes: 30,
actualMinutes: 0,
dependencies: [],
tags: ['test'],
subtasks: []
},
{
id: 'status-002',
description: 'Done task',
status: 'done',
priority: 'medium',
estimatedMinutes: 60,
actualMinutes: 60,
dependencies: [],
tags: ['test'],
subtasks: []
}
]
};
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
try {
// Test filtering by 'done' status
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/test-status-tasks.json',
status: 'done'
}
});
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toContain('1 task found');
expect(result.content[0].text).toContain('Done task');
expect(result.content[0].text).not.toContain('Pending task');
} finally {
// Cleanup
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
}
});
});

View File

@@ -0,0 +1,146 @@
import { mcpTest } from 'mcp-jest';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '../../../..');
// Create test tasks file for testing
const testTasksPath = join(projectRoot, '.taskmaster/test-mcp-tasks.json');
const testTasks = {
tasks: [
{
id: 'mcp-test-001',
description: 'MCP Test task 1',
status: 'pending',
priority: 'high',
estimatedMinutes: 30,
actualMinutes: 0,
dependencies: [],
tags: ['test'],
subtasks: [
{
id: 'mcp-test-001-1',
description: 'MCP Test subtask 1.1',
status: 'pending',
priority: 'medium',
estimatedMinutes: 15,
actualMinutes: 0
}
]
},
{
id: 'mcp-test-002',
description: 'MCP Test task 2',
status: 'done',
priority: 'medium',
estimatedMinutes: 60,
actualMinutes: 60,
dependencies: ['mcp-test-001'],
tags: ['test', 'demo'],
subtasks: []
}
]
};
// Setup test data
fs.mkdirSync(join(projectRoot, '.taskmaster'), { recursive: true });
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
// Run MCP Jest tests
async function runTests() {
try {
const results = await mcpTest(
{
command: 'node',
args: [join(projectRoot, 'mcp-server/server.js')],
env: process.env
},
{
tools: {
initialize_project: {
args: { projectRoot: projectRoot },
expect: (result) =>
result.content[0].text.includes(
'Project initialized successfully'
)
},
get_tasks: [
{
name: 'get all tasks with subtasks',
args: {
projectRoot: projectRoot,
file: '.taskmaster/test-mcp-tasks.json',
withSubtasks: true
},
expect: (result) => {
const text = result.content[0].text;
return (
!result.isError &&
text.includes('2 tasks found') &&
text.includes('MCP Test task 1') &&
text.includes('MCP Test task 2') &&
text.includes('MCP Test subtask 1.1')
);
}
},
{
name: 'filter by done status',
args: {
projectRoot: projectRoot,
file: '.taskmaster/test-mcp-tasks.json',
status: 'done'
},
expect: (result) => {
const text = result.content[0].text;
return (
!result.isError &&
text.includes('1 task found') &&
text.includes('MCP Test task 2') &&
!text.includes('MCP Test task 1')
);
}
},
{
name: 'handle non-existent file',
args: {
projectRoot: projectRoot,
file: '.taskmaster/non-existent.json'
},
expect: (result) =>
result.isError && result.content[0].text.includes('Error')
}
]
}
}
);
console.log('\nTest Results:');
console.log('=============');
console.log(`✅ Passed: ${results.passed}/${results.total}`);
if (results.failed > 0) {
console.error(`❌ Failed: ${results.failed}`);
console.error('\nDetailed Results:');
console.log(JSON.stringify(results, null, 2));
}
// Cleanup
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
// Exit with appropriate code
process.exit(results.failed > 0 ? 1 : 0);
} catch (error) {
console.error('Test execution failed:', error);
// Cleanup on error
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
process.exit(1);
}
}
runTests();

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env node
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '../../../..');
// Create test tasks file for testing
const testTasksPath = join(projectRoot, '.taskmaster/test-tasks.json');
const testTasks = {
tasks: [
{
id: 'test-001',
description: 'Test task 1',
status: 'pending',
priority: 'high',
estimatedMinutes: 30,
actualMinutes: 0,
dependencies: [],
tags: ['test'],
subtasks: [
{
id: 'test-001-1',
description: 'Test subtask 1.1',
status: 'pending',
priority: 'medium',
estimatedMinutes: 15,
actualMinutes: 0
}
]
},
{
id: 'test-002',
description: 'Test task 2',
status: 'done',
priority: 'medium',
estimatedMinutes: 60,
actualMinutes: 60,
dependencies: ['test-001'],
tags: ['test', 'demo'],
subtasks: []
}
]
};
async function runTests() {
console.log('Starting MCP server tests...\n');
// Setup test data
fs.mkdirSync(join(projectRoot, '.taskmaster'), { recursive: true });
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
// Create transport by spawning the server
const transport = new StdioClientTransport({
command: 'node',
args: ['mcp-server/server.js'],
env: process.env,
cwd: projectRoot
});
// Create client
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: {
sampling: {}
}
}
);
let testResults = {
total: 0,
passed: 0,
failed: 0,
tests: []
};
async function runTest(name, testFn) {
testResults.total++;
try {
await testFn();
testResults.passed++;
testResults.tests.push({ name, status: 'passed' });
console.log(`${name}`);
} catch (error) {
testResults.failed++;
testResults.tests.push({ name, status: 'failed', error: error.message });
console.error(`${name}`);
console.error(` Error: ${error.message}`);
}
}
try {
// Connect to server
await client.connect(transport);
console.log('Connected to MCP server\n');
// Test 1: List available tools
await runTest('List available tools', async () => {
const tools = await client.listTools();
if (!tools.tools || tools.tools.length === 0) {
throw new Error('No tools found');
}
const toolNames = tools.tools.map((t) => t.name);
if (!toolNames.includes('get_tasks')) {
throw new Error('get_tasks tool not found');
}
console.log(` Found ${tools.tools.length} tools`);
});
// Test 2: Initialize project
await runTest('Initialize project', async () => {
const result = await client.callTool({
name: 'initialize_project',
arguments: {
projectRoot: projectRoot
}
});
if (
!result.content[0].text.includes('Project initialized successfully')
) {
throw new Error('Project initialization failed');
}
});
// Test 3: Get all tasks
await runTest('Get all tasks with subtasks', async () => {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/test-tasks.json',
withSubtasks: true
}
});
if (result.isError) {
throw new Error(`Tool returned error: ${result.content[0].text}`);
}
const text = result.content[0].text;
const data = JSON.parse(text);
if (!data.data || !data.data.tasks) {
throw new Error('Invalid response format');
}
if (data.data.tasks.length !== 2) {
throw new Error(`Expected 2 tasks, got ${data.data.tasks.length}`);
}
const taskDescriptions = data.data.tasks.map((t) => t.description);
if (
!taskDescriptions.includes('Test task 1') ||
!taskDescriptions.includes('Test task 2')
) {
throw new Error('Expected tasks not found');
}
// Check for subtask
const task1 = data.data.tasks.find((t) => t.id === 'test-001');
if (!task1.subtasks || task1.subtasks.length === 0) {
throw new Error('Subtasks not found');
}
if (task1.subtasks[0].description !== 'Test subtask 1.1') {
throw new Error('Expected subtask not found');
}
});
// Test 4: Filter by status
await runTest('Filter tasks by done status', async () => {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/test-tasks.json',
status: 'done'
}
});
if (result.isError) {
throw new Error(`Tool returned error: ${result.content[0].text}`);
}
const text = result.content[0].text;
const data = JSON.parse(text);
if (!data.data || !data.data.tasks) {
throw new Error('Invalid response format');
}
if (data.data.tasks.length !== 1) {
throw new Error(
`Expected 1 task with done status, got ${data.data.tasks.length}`
);
}
const task = data.data.tasks[0];
if (task.description !== 'Test task 2') {
throw new Error(`Expected 'Test task 2', got '${task.description}'`);
}
if (task.status !== 'done') {
throw new Error(`Expected status 'done', got '${task.status}'`);
}
});
// Test 5: Handle non-existent file
await runTest('Handle non-existent file gracefully', async () => {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/non-existent.json'
}
});
if (!result.isError) {
throw new Error('Expected error for non-existent file');
}
if (!result.content[0].text.includes('Error')) {
throw new Error('Expected error message');
}
});
} catch (error) {
console.error('\nConnection error:', error.message);
testResults.failed = testResults.total;
} finally {
// Clean up
await client.close();
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
// Print summary
console.log('\n' + '='.repeat(50));
console.log('Test Summary:');
console.log(`Total: ${testResults.total}`);
console.log(`Passed: ${testResults.passed}`);
console.log(`Failed: ${testResults.failed}`);
console.log('='.repeat(50));
// Exit with appropriate code
process.exit(testResults.failed > 0 ? 1 : 0);
}
}
runTests().catch(console.error);

View File

@@ -1,5 +1,11 @@
const { spawn } = require('child_process');
const { readFileSync, existsSync, copyFileSync, writeFileSync, readdirSync } = require('fs');
const {
readFileSync,
existsSync,
copyFileSync,
writeFileSync,
readdirSync
} = require('fs');
const { join } = require('path');
class TestHelpers {
@@ -120,9 +126,7 @@ class TestHelpers {
writeFileSync(filePath, content, 'utf8');
return true;
} catch (error) {
this.logger.error(
`Failed to write file ${filePath}: ${error.message}`
);
this.logger.error(`Failed to write file ${filePath}: ${error.message}`);
return false;
}
}
@@ -134,9 +138,7 @@ class TestHelpers {
try {
return readFileSync(filePath, 'utf8');
} catch (error) {
this.logger.error(
`Failed to read file ${filePath}: ${error.message}`
);
this.logger.error(`Failed to read file ${filePath}: ${error.message}`);
return null;
}
}
@@ -148,9 +150,7 @@ class TestHelpers {
try {
return readdirSync(dirPath);
} catch (error) {
this.logger.error(
`Failed to list files in ${dirPath}: ${error.message}`
);
this.logger.error(`Failed to list files in ${dirPath}: ${error.message}`);
return [];
}
}
@@ -243,4 +243,4 @@ class TestHelpers {
}
}
module.exports = { TestHelpers };
module.exports = { TestHelpers };