This is page 23 of 50. Use http://codebase.md/eyaltoledano/claude-task-master?page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/tests/unit/init.test.js:
--------------------------------------------------------------------------------
```javascript
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Mock external modules
jest.mock('child_process', () => ({
execSync: jest.fn()
}));
jest.mock('readline', () => ({
createInterface: jest.fn(() => ({
question: jest.fn(),
close: jest.fn()
}))
}));
// Mock figlet for banner display
jest.mock('figlet', () => ({
default: {
textSync: jest.fn(() => 'Task Master')
}
}));
// Mock console methods
jest.mock('console', () => ({
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
clear: jest.fn()
}));
describe('Windsurf Rules File Handling', () => {
let tempDir;
beforeEach(() => {
jest.clearAllMocks();
// Create a temporary directory for testing
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
// Spy on fs methods
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
if (filePath.toString().includes('.windsurfrules')) {
return 'Existing windsurf rules content';
}
return '{}';
});
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
// Mock specific file existence checks
if (filePath.toString().includes('package.json')) {
return true;
}
return false;
});
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
});
afterEach(() => {
// Clean up the temporary directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (err) {
console.error(`Error cleaning up: ${err.message}`);
}
});
// Test function that simulates the behavior of .windsurfrules handling
function mockCopyTemplateFile(templateName, targetPath) {
if (templateName === 'windsurfrules') {
const filename = path.basename(targetPath);
if (filename === '.windsurfrules') {
if (fs.existsSync(targetPath)) {
// Should append content when file exists
const existingContent = fs.readFileSync(targetPath, 'utf8');
const updatedContent =
existingContent.trim() +
'\n\n# Added by Claude Task Master - Development Workflow Rules\n\n' +
'New content';
fs.writeFileSync(targetPath, updatedContent);
return;
}
}
// If file doesn't exist, create it normally
fs.writeFileSync(targetPath, 'New content');
}
}
test('creates .windsurfrules when it does not exist', () => {
// Arrange
const targetPath = path.join(tempDir, '.windsurfrules');
// Act
mockCopyTemplateFile('windsurfrules', targetPath);
// Assert
expect(fs.writeFileSync).toHaveBeenCalledWith(targetPath, 'New content');
});
test('appends content to existing .windsurfrules', () => {
// Arrange
const targetPath = path.join(tempDir, '.windsurfrules');
const existingContent = 'Existing windsurf rules content';
// Override the existsSync mock just for this test
fs.existsSync.mockReturnValueOnce(true); // Target file exists
fs.readFileSync.mockReturnValueOnce(existingContent);
// Act
mockCopyTemplateFile('windsurfrules', targetPath);
// Assert
expect(fs.writeFileSync).toHaveBeenCalledWith(
targetPath,
expect.stringContaining(existingContent)
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
targetPath,
expect.stringContaining('Added by Claude Task Master')
);
});
test('includes .windsurfrules in project structure creation', () => {
// This test verifies the expected behavior by using a mock implementation
// that represents how createProjectStructure should work
// Mock implementation of createProjectStructure
function mockCreateProjectStructure(projectName) {
// Copy template files including .windsurfrules
mockCopyTemplateFile(
'windsurfrules',
path.join(tempDir, '.windsurfrules')
);
}
// Act - call our mock implementation
mockCreateProjectStructure('test-project');
// Assert - verify that .windsurfrules was created
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(tempDir, '.windsurfrules'),
expect.any(String)
);
});
});
// New test suite for MCP Configuration Handling
describe('MCP Configuration Handling', () => {
let tempDir;
beforeEach(() => {
jest.clearAllMocks();
// Create a temporary directory for testing
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
// Spy on fs methods
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
if (filePath.toString().includes('mcp.json')) {
return JSON.stringify({
mcpServers: {
'existing-server': {
command: 'node',
args: ['server.js']
}
}
});
}
return '{}';
});
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
// Return true for specific paths to test different scenarios
if (filePath.toString().includes('package.json')) {
return true;
}
// Default to false for other paths
return false;
});
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
});
afterEach(() => {
// Clean up the temporary directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (err) {
console.error(`Error cleaning up: ${err.message}`);
}
});
// Test function that simulates the behavior of setupMCPConfiguration
function mockSetupMCPConfiguration(targetDir, projectName) {
const mcpDirPath = path.join(targetDir, '.cursor');
const mcpJsonPath = path.join(mcpDirPath, 'mcp.json');
// Create .cursor directory if it doesn't exist
if (!fs.existsSync(mcpDirPath)) {
fs.mkdirSync(mcpDirPath, { recursive: true });
}
// New MCP config to be added - references the installed package
const newMCPServer = {
'task-master-ai': {
command: 'npx',
args: ['task-master-ai', 'mcp-server']
}
};
// Check if mcp.json already exists
if (fs.existsSync(mcpJsonPath)) {
try {
// Read existing config
const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
// Initialize mcpServers if it doesn't exist
if (!mcpConfig.mcpServers) {
mcpConfig.mcpServers = {};
}
// Add the taskmaster-ai server if it doesn't exist
if (!mcpConfig.mcpServers['task-master-ai']) {
mcpConfig.mcpServers['task-master-ai'] =
newMCPServer['task-master-ai'];
}
// Write the updated configuration
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 4));
} catch (error) {
// Create new configuration on error
const newMCPConfig = {
mcpServers: newMCPServer
};
fs.writeFileSync(mcpJsonPath, JSON.stringify(newMCPConfig, null, 4));
}
} else {
// If mcp.json doesn't exist, create it
const newMCPConfig = {
mcpServers: newMCPServer
};
fs.writeFileSync(mcpJsonPath, JSON.stringify(newMCPConfig, null, 4));
}
}
test('creates mcp.json when it does not exist', () => {
// Arrange
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
// Act
mockSetupMCPConfiguration(tempDir, 'test-project');
// Assert
expect(fs.writeFileSync).toHaveBeenCalledWith(
mcpJsonPath,
expect.stringContaining('task-master-ai')
);
// Should create a proper structure with mcpServers key
expect(fs.writeFileSync).toHaveBeenCalledWith(
mcpJsonPath,
expect.stringContaining('mcpServers')
);
// Should reference npx command
expect(fs.writeFileSync).toHaveBeenCalledWith(
mcpJsonPath,
expect.stringContaining('npx')
);
});
test('updates existing mcp.json by adding new server', () => {
// Arrange
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
// Override the existsSync mock to simulate mcp.json exists
fs.existsSync.mockImplementation((filePath) => {
if (filePath.toString().includes('mcp.json')) {
return true;
}
return false;
});
// Act
mockSetupMCPConfiguration(tempDir, 'test-project');
// Assert
// Should preserve existing server
expect(fs.writeFileSync).toHaveBeenCalledWith(
mcpJsonPath,
expect.stringContaining('existing-server')
);
// Should add our new server
expect(fs.writeFileSync).toHaveBeenCalledWith(
mcpJsonPath,
expect.stringContaining('task-master-ai')
);
});
test('handles JSON parsing errors by creating new mcp.json', () => {
// Arrange
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
// Override existsSync to say mcp.json exists
fs.existsSync.mockImplementation((filePath) => {
if (filePath.toString().includes('mcp.json')) {
return true;
}
return false;
});
// But make readFileSync return invalid JSON
fs.readFileSync.mockImplementation((filePath) => {
if (filePath.toString().includes('mcp.json')) {
return '{invalid json';
}
return '{}';
});
// Act
mockSetupMCPConfiguration(tempDir, 'test-project');
// Assert
// Should create a new valid JSON file with our server
expect(fs.writeFileSync).toHaveBeenCalledWith(
mcpJsonPath,
expect.stringContaining('task-master-ai')
);
});
test('does not modify existing server configuration if it already exists', () => {
// Arrange
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
// Override existsSync to say mcp.json exists
fs.existsSync.mockImplementation((filePath) => {
if (filePath.toString().includes('mcp.json')) {
return true;
}
return false;
});
// Return JSON that already has task-master-ai
fs.readFileSync.mockImplementation((filePath) => {
if (filePath.toString().includes('mcp.json')) {
return JSON.stringify({
mcpServers: {
'existing-server': {
command: 'node',
args: ['server.js']
},
'task-master-ai': {
command: 'custom',
args: ['custom-args']
}
}
});
}
return '{}';
});
// Spy to check what's written
const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync');
// Act
mockSetupMCPConfiguration(tempDir, 'test-project');
// Assert
// Verify the written data contains the original taskmaster configuration
const dataWritten = JSON.parse(writeFileSyncSpy.mock.calls[0][1]);
expect(dataWritten.mcpServers['task-master-ai'].command).toBe('custom');
expect(dataWritten.mcpServers['task-master-ai'].args).toContain(
'custom-args'
);
});
test('creates the .cursor directory if it doesnt exist', () => {
// Arrange
const cursorDirPath = path.join(tempDir, '.cursor');
// Make sure it looks like the directory doesn't exist
fs.existsSync.mockReturnValue(false);
// Act
mockSetupMCPConfiguration(tempDir, 'test-project');
// Assert
expect(fs.mkdirSync).toHaveBeenCalledWith(cursorDirPath, {
recursive: true
});
});
});
```
--------------------------------------------------------------------------------
/packages/tm-core/tests/integration/list-tasks.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview End-to-end integration test for listTasks functionality
*/
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
type Task,
type TaskMasterCore,
type TaskStatus,
createTaskMasterCore
} from '../../src/index';
describe('TaskMasterCore - listTasks E2E', () => {
let tmpDir: string;
let tmCore: TaskMasterCore;
// Sample tasks data
const sampleTasks: Task[] = [
{
id: '1',
title: 'Setup project',
description: 'Initialize the project structure',
status: 'done',
priority: 'high',
dependencies: [],
details: 'Create all necessary directories and config files',
testStrategy: 'Manual verification',
subtasks: [
{
id: 1,
parentId: '1',
title: 'Create directories',
description: 'Create project directories',
status: 'done',
priority: 'high',
dependencies: [],
details: 'Create src, tests, docs directories',
testStrategy: 'Check directories exist'
},
{
id: 2,
parentId: '1',
title: 'Initialize package.json',
description: 'Create package.json file',
status: 'done',
priority: 'high',
dependencies: [],
details: 'Run npm init',
testStrategy: 'Verify package.json exists'
}
],
tags: ['setup', 'infrastructure']
},
{
id: '2',
title: 'Implement core features',
description: 'Build the main functionality',
status: 'in-progress',
priority: 'high',
dependencies: ['1'],
details: 'Implement all core business logic',
testStrategy: 'Unit tests for all features',
subtasks: [],
tags: ['feature', 'core'],
assignee: 'developer1'
},
{
id: '3',
title: 'Write documentation',
description: 'Create user and developer docs',
status: 'pending',
priority: 'medium',
dependencies: ['2'],
details: 'Write comprehensive documentation',
testStrategy: 'Review by team',
subtasks: [],
tags: ['documentation'],
complexity: 'simple'
},
{
id: '4',
title: 'Performance optimization',
description: 'Optimize for speed and efficiency',
status: 'blocked',
priority: 'low',
dependencies: ['2'],
details: 'Profile and optimize bottlenecks',
testStrategy: 'Performance benchmarks',
subtasks: [],
assignee: 'developer2',
complexity: 'complex'
},
{
id: '5',
title: 'Security audit',
description: 'Review security vulnerabilities',
status: 'deferred',
priority: 'critical',
dependencies: [],
details: 'Complete security assessment',
testStrategy: 'Security scanning tools',
subtasks: [],
tags: ['security', 'audit']
}
];
beforeEach(async () => {
// Create temp directory for testing
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tm-core-test-'));
// Create .taskmaster/tasks directory
const tasksDir = path.join(tmpDir, '.taskmaster', 'tasks');
await fs.mkdir(tasksDir, { recursive: true });
// Write sample tasks.json
const tasksFile = path.join(tasksDir, 'tasks.json');
const tasksData = {
tasks: sampleTasks,
metadata: {
version: '1.0.0',
lastModified: new Date().toISOString(),
taskCount: sampleTasks.length,
completedCount: 1
}
};
await fs.writeFile(tasksFile, JSON.stringify(tasksData, null, 2));
// Create TaskMasterCore instance
tmCore = createTaskMasterCore(tmpDir);
await tmCore.initialize();
});
afterEach(async () => {
// Cleanup
if (tmCore) {
await tmCore.close();
}
// Remove temp directory
await fs.rm(tmpDir, { recursive: true, force: true });
});
describe('Basic listing', () => {
it('should list all tasks', async () => {
const result = await tmCore.listTasks();
expect(result.tasks).toHaveLength(5);
expect(result.total).toBe(5);
expect(result.filtered).toBe(5);
expect(result.tag).toBeUndefined();
});
it('should include subtasks by default', async () => {
const result = await tmCore.listTasks();
const setupTask = result.tasks.find((t) => t.id === '1');
expect(setupTask?.subtasks).toHaveLength(2);
expect(setupTask?.subtasks[0].title).toBe('Create directories');
});
it('should exclude subtasks when requested', async () => {
const result = await tmCore.listTasks({ includeSubtasks: false });
const setupTask = result.tasks.find((t) => t.id === '1');
expect(setupTask?.subtasks).toHaveLength(0);
});
});
describe('Filtering', () => {
it('should filter by status', async () => {
const result = await tmCore.listTasks({
filter: { status: 'done' }
});
expect(result.filtered).toBe(1);
expect(result.tasks[0].id).toBe('1');
});
it('should filter by multiple statuses', async () => {
const result = await tmCore.listTasks({
filter: { status: ['done', 'in-progress'] }
});
expect(result.filtered).toBe(2);
const ids = result.tasks.map((t) => t.id);
expect(ids).toContain('1');
expect(ids).toContain('2');
});
it('should filter by priority', async () => {
const result = await tmCore.listTasks({
filter: { priority: 'high' }
});
expect(result.filtered).toBe(2);
});
it('should filter by tags', async () => {
const result = await tmCore.listTasks({
filter: { tags: ['setup'] }
});
expect(result.filtered).toBe(1);
expect(result.tasks[0].id).toBe('1');
});
it('should filter by assignee', async () => {
const result = await tmCore.listTasks({
filter: { assignee: 'developer1' }
});
expect(result.filtered).toBe(1);
expect(result.tasks[0].id).toBe('2');
});
it('should filter by complexity', async () => {
const result = await tmCore.listTasks({
filter: { complexity: 'complex' }
});
expect(result.filtered).toBe(1);
expect(result.tasks[0].id).toBe('4');
});
it('should filter by search term', async () => {
const result = await tmCore.listTasks({
filter: { search: 'documentation' }
});
expect(result.filtered).toBe(1);
expect(result.tasks[0].id).toBe('3');
});
it('should filter by hasSubtasks', async () => {
const withSubtasks = await tmCore.listTasks({
filter: { hasSubtasks: true }
});
expect(withSubtasks.filtered).toBe(1);
expect(withSubtasks.tasks[0].id).toBe('1');
const withoutSubtasks = await tmCore.listTasks({
filter: { hasSubtasks: false }
});
expect(withoutSubtasks.filtered).toBe(4);
});
it('should handle combined filters', async () => {
const result = await tmCore.listTasks({
filter: {
priority: ['high', 'critical'],
status: ['pending', 'deferred']
}
});
expect(result.filtered).toBe(1);
expect(result.tasks[0].id).toBe('5'); // Critical priority, deferred status
});
});
describe('Helper methods', () => {
it('should get task by ID', async () => {
const task = await tmCore.getTask('2');
expect(task).not.toBeNull();
expect(task?.title).toBe('Implement core features');
});
it('should return null for non-existent task', async () => {
const task = await tmCore.getTask('999');
expect(task).toBeNull();
});
it('should get tasks by status', async () => {
const pendingTasks = await tmCore.getTasksByStatus('pending');
expect(pendingTasks).toHaveLength(1);
expect(pendingTasks[0].id).toBe('3');
const multipleTasks = await tmCore.getTasksByStatus(['done', 'blocked']);
expect(multipleTasks).toHaveLength(2);
});
it('should get task statistics', async () => {
const stats = await tmCore.getTaskStats();
expect(stats.total).toBe(5);
expect(stats.byStatus.done).toBe(1);
expect(stats.byStatus['in-progress']).toBe(1);
expect(stats.byStatus.pending).toBe(1);
expect(stats.byStatus.blocked).toBe(1);
expect(stats.byStatus.deferred).toBe(1);
expect(stats.byStatus.cancelled).toBe(0);
expect(stats.byStatus.review).toBe(0);
expect(stats.withSubtasks).toBe(1);
expect(stats.blocked).toBe(1);
});
});
describe('Error handling', () => {
it('should handle missing tasks file gracefully', async () => {
// Create new instance with empty directory
const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tm-empty-'));
const emptyCore = createTaskMasterCore(emptyDir);
try {
const result = await emptyCore.listTasks();
expect(result.tasks).toHaveLength(0);
expect(result.total).toBe(0);
expect(result.filtered).toBe(0);
} finally {
await emptyCore.close();
await fs.rm(emptyDir, { recursive: true, force: true });
}
});
it('should validate task entities', async () => {
// Write invalid task data
const invalidDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'tm-invalid-')
);
const tasksDir = path.join(invalidDir, '.taskmaster', 'tasks');
await fs.mkdir(tasksDir, { recursive: true });
const invalidData = {
tasks: [
{
id: '', // Invalid: empty ID
title: 'Test',
description: 'Test',
status: 'done',
priority: 'high',
dependencies: [],
details: 'Test',
testStrategy: 'Test',
subtasks: []
}
],
metadata: {
version: '1.0.0',
lastModified: new Date().toISOString(),
taskCount: 1,
completedCount: 0
}
};
await fs.writeFile(
path.join(tasksDir, 'tasks.json'),
JSON.stringify(invalidData)
);
const invalidCore = createTaskMasterCore(invalidDir);
try {
await expect(invalidCore.listTasks()).rejects.toThrow();
} finally {
await invalidCore.close();
await fs.rm(invalidDir, { recursive: true, force: true });
}
});
});
describe('Tags support', () => {
beforeEach(async () => {
// Create tasks for a different tag
const taggedTasks = [
{
id: 'tag-1',
title: 'Tagged task',
description: 'Task with tag',
status: 'pending' as TaskStatus,
priority: 'medium' as const,
dependencies: [],
details: 'Tagged task details',
testStrategy: 'Test',
subtasks: []
}
];
const tagFile = path.join(
tmpDir,
'.taskmaster',
'tasks',
'feature-branch.json'
);
await fs.writeFile(
tagFile,
JSON.stringify({
tasks: taggedTasks,
metadata: {
version: '1.0.0',
lastModified: new Date().toISOString(),
taskCount: 1,
completedCount: 0
}
})
);
});
it('should list tasks for specific tag', async () => {
const result = await tmCore.listTasks({ tag: 'feature-branch' });
expect(result.tasks).toHaveLength(1);
expect(result.tasks[0].id).toBe('tag-1');
expect(result.tag).toBe('feature-branch');
});
it('should list default tasks when no tag specified', async () => {
const result = await tmCore.listTasks();
expect(result.tasks).toHaveLength(5);
expect(result.tasks[0].id).toBe('1');
});
});
});
```
--------------------------------------------------------------------------------
/packages/tm-core/tests/integration/auth-token-refresh.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Integration tests for JWT token auto-refresh functionality
*
* These tests verify that expired tokens are automatically refreshed
* when making API calls through AuthManager.
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
import type { Session } from '@supabase/supabase-js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AuthManager } from '../../src/modules/auth/managers/auth-manager.js';
import { CredentialStore } from '../../src/modules/auth/services/credential-store.js';
import type { AuthCredentials } from '../../src/modules/auth/types.js';
describe('AuthManager - Token Auto-Refresh Integration', () => {
let authManager: AuthManager;
let credentialStore: CredentialStore;
let tmpDir: string;
let authFile: string;
// Mock Supabase session that will be returned on refresh
const mockRefreshedSession: Session = {
access_token: 'new-access-token-xyz',
refresh_token: 'new-refresh-token-xyz',
token_type: 'bearer',
expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
expires_in: 3600,
user: {
id: 'test-user-id',
email: '[email protected]',
aud: 'authenticated',
role: 'authenticated',
app_metadata: {},
user_metadata: {},
created_at: new Date().toISOString()
}
};
beforeEach(() => {
// Reset singletons
AuthManager.resetInstance();
CredentialStore.resetInstance();
// Create temporary directory for test isolation
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-auth-integration-'));
authFile = path.join(tmpDir, 'auth.json');
// Initialize AuthManager with test config (this will create CredentialStore internally)
authManager = AuthManager.getInstance({
configDir: tmpDir,
configFile: authFile
});
// Get the CredentialStore instance that AuthManager created
credentialStore = CredentialStore.getInstance();
credentialStore.clearCredentials();
});
afterEach(() => {
// Clean up
try {
credentialStore.clearCredentials();
} catch {
// Ignore cleanup errors
}
AuthManager.resetInstance();
CredentialStore.resetInstance();
vi.restoreAllMocks();
// Remove temporary directory
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
describe('Expired Token Detection', () => {
it('should return expired token for Supabase to refresh', () => {
// Set up expired credentials
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: '[email protected]',
expiresAt: new Date(Date.now() - 60000).toISOString(), // 1 minute ago
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
// Get credentials returns them even if expired
const credentials = authManager.getCredentials();
expect(credentials).not.toBeNull();
expect(credentials?.token).toBe('expired-token');
expect(credentials?.refreshToken).toBe('valid-refresh-token');
});
it('should return valid token', () => {
// Set up valid credentials
const validCredentials: AuthCredentials = {
token: 'valid-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: '[email protected]',
expiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(validCredentials);
authManager = AuthManager.getInstance();
const credentials = authManager.getCredentials();
expect(credentials?.token).toBe('valid-token');
});
});
describe('Token Refresh Flow', () => {
it('should manually refresh expired token and save new credentials', async () => {
const expiredCredentials: AuthCredentials = {
token: 'old-token',
refreshToken: 'old-refresh-token',
userId: 'test-user-id',
email: '[email protected]',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date(Date.now() - 3600000).toISOString(),
selectedContext: {
orgId: 'test-org',
briefId: 'test-brief',
updatedAt: new Date().toISOString()
}
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockResolvedValue(mockRefreshedSession);
// Explicitly call refreshToken() method
const refreshedCredentials = await authManager.refreshToken();
expect(refreshedCredentials).not.toBeNull();
expect(refreshedCredentials.token).toBe('new-access-token-xyz');
expect(refreshedCredentials.refreshToken).toBe('new-refresh-token-xyz');
// Verify context was preserved
expect(refreshedCredentials.selectedContext?.orgId).toBe('test-org');
expect(refreshedCredentials.selectedContext?.briefId).toBe('test-brief');
// Verify new expiration is in the future
const newExpiry = new Date(refreshedCredentials.expiresAt!).getTime();
const now = Date.now();
expect(newExpiry).toBeGreaterThan(now);
});
it('should throw error if manual refresh fails', async () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'invalid-refresh-token',
userId: 'test-user-id',
email: '[email protected]',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
// Mock refresh to fail
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockRejectedValue(new Error('Refresh token expired'));
// Explicit refreshToken() call should throw
await expect(authManager.refreshToken()).rejects.toThrow();
});
it('should return expired credentials even without refresh token', () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
// No refresh token
userId: 'test-user-id',
email: '[email protected]',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
const credentials = authManager.getCredentials();
// Credentials are returned even without refresh token
expect(credentials).not.toBeNull();
expect(credentials?.token).toBe('expired-token');
expect(credentials?.refreshToken).toBeUndefined();
});
it('should return null if credentials missing expiresAt', () => {
const credentialsWithoutExpiry: AuthCredentials = {
token: 'test-token',
refreshToken: 'refresh-token',
userId: 'test-user-id',
email: '[email protected]',
// Missing expiresAt - invalid token
savedAt: new Date().toISOString()
} as any;
credentialStore.saveCredentials(credentialsWithoutExpiry);
authManager = AuthManager.getInstance();
const credentials = authManager.getCredentials();
// Tokens without valid expiration are considered invalid
expect(credentials).toBeNull();
});
});
describe('Clock Skew Tolerance', () => {
it('should return credentials within 30-second expiry window', () => {
// Token expires in 15 seconds (within 30-second buffer)
// Supabase will handle refresh automatically
const almostExpiredCredentials: AuthCredentials = {
token: 'almost-expired-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: '[email protected]',
expiresAt: new Date(Date.now() + 15000).toISOString(), // 15 seconds from now
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(almostExpiredCredentials);
authManager = AuthManager.getInstance();
const credentials = authManager.getCredentials();
// Credentials are returned (Supabase handles auto-refresh in background)
expect(credentials).not.toBeNull();
expect(credentials?.token).toBe('almost-expired-token');
expect(credentials?.refreshToken).toBe('valid-refresh-token');
});
it('should return valid token well before expiry', () => {
// Token expires in 5 minutes
const validCredentials: AuthCredentials = {
token: 'valid-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: '[email protected]',
expiresAt: new Date(Date.now() + 300000).toISOString(), // 5 minutes
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(validCredentials);
authManager = AuthManager.getInstance();
const credentials = authManager.getCredentials();
// Valid credentials are returned as-is
expect(credentials).not.toBeNull();
expect(credentials?.token).toBe('valid-token');
expect(credentials?.refreshToken).toBe('valid-refresh-token');
});
});
describe('Synchronous vs Async Methods', () => {
it('getCredentials should return expired credentials', () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: '[email protected]',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
// Returns credentials even if expired - Supabase will handle refresh
const credentials = authManager.getCredentials();
expect(credentials).not.toBeNull();
expect(credentials?.token).toBe('expired-token');
expect(credentials?.refreshToken).toBe('valid-refresh-token');
});
});
describe('Multiple Concurrent Calls', () => {
it('should handle concurrent getCredentials calls gracefully', () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: '[email protected]',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
// Make multiple concurrent calls (synchronous now)
const creds1 = authManager.getCredentials();
const creds2 = authManager.getCredentials();
const creds3 = authManager.getCredentials();
// All should get the same credentials (even if expired)
expect(creds1?.token).toBe('expired-token');
expect(creds2?.token).toBe('expired-token');
expect(creds3?.token).toBe('expired-token');
// All include refresh token for Supabase to use
expect(creds1?.refreshToken).toBe('valid-refresh-token');
expect(creds2?.refreshToken).toBe('valid-refresh-token');
expect(creds3?.refreshToken).toBe('valid-refresh-token');
});
});
});
```
--------------------------------------------------------------------------------
/apps/extension/src/components/TaskDetails/TaskMetadataSidebar.tsx:
--------------------------------------------------------------------------------
```typescript
import type React from 'react';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Loader2, Play } from 'lucide-react';
import { PriorityBadge } from './PriorityBadge';
import type { TaskMasterTask } from '../../webview/types';
import { useVSCodeContext } from '../../webview/contexts/VSCodeContext';
interface TaskMetadataSidebarProps {
currentTask: TaskMasterTask;
tasks: TaskMasterTask[];
complexity: any;
isSubtask: boolean;
onStatusChange: (status: TaskMasterTask['status']) => void;
onDependencyClick: (depId: string) => void;
isRegenerating?: boolean;
isAppending?: boolean;
}
export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
currentTask,
tasks,
complexity,
isSubtask,
onStatusChange,
onDependencyClick,
isRegenerating = false,
isAppending = false
}) => {
const { sendMessage } = useVSCodeContext();
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
const [mcpComplexityScore, setMcpComplexityScore] = useState<
number | undefined
>(undefined);
const [isStartingTask, setIsStartingTask] = useState(false);
// Get complexity score from task
const currentComplexityScore = complexity?.score;
// Display logic - use MCP score if available, otherwise use current score
const displayComplexityScore =
mcpComplexityScore !== undefined
? mcpComplexityScore
: currentComplexityScore;
// Fetch complexity from MCP when needed
const fetchComplexityFromMCP = async (force = false) => {
if (!currentTask || (!force && currentComplexityScore !== undefined)) {
return;
}
setIsLoadingComplexity(true);
try {
const complexityResult = await sendMessage({
type: 'mcpRequest',
tool: 'complexity_report',
params: {}
});
if (complexityResult?.data?.report?.complexityAnalysis) {
const taskComplexity =
complexityResult.data.report.complexityAnalysis.tasks?.find(
(t: any) => t.id === currentTask.id
);
if (taskComplexity) {
setMcpComplexityScore(taskComplexity.complexityScore);
}
}
} catch (error) {
console.error('Failed to fetch complexity from MCP:', error);
} finally {
setIsLoadingComplexity(false);
}
};
// Handle running complexity analysis for a task
const handleRunComplexityAnalysis = async () => {
if (!currentTask) {
return;
}
setIsLoadingComplexity(true);
try {
// Run complexity analysis on this specific task
await sendMessage({
type: 'mcpRequest',
tool: 'analyze_project_complexity',
params: {
ids: currentTask.id.toString(),
research: false
}
});
// After analysis, fetch the updated complexity report
setTimeout(() => {
fetchComplexityFromMCP(true);
}, 1000);
} catch (error) {
console.error('Failed to run complexity analysis:', error);
} finally {
setIsLoadingComplexity(false);
}
};
// Handle starting a task
const handleStartTask = async () => {
if (!currentTask || isStartingTask) {
return;
}
setIsStartingTask(true);
try {
// Send message to extension to open terminal
const result = await sendMessage({
type: 'openTerminal',
data: {
taskId: currentTask.id,
taskTitle: currentTask.title
}
});
// Handle the response
if (result && !result.success) {
console.error('Terminal execution failed:', result.error);
// The extension will show VS Code error notification and webview toast
} else if (result && result.success) {
console.log('Terminal started successfully:', result.terminalName);
}
} catch (error) {
console.error('Failed to start task:', error);
// This handles network/communication errors
} finally {
// Reset loading state
setIsStartingTask(false);
}
};
// Effect to handle complexity on task change
useEffect(() => {
if (currentTask?.id) {
setMcpComplexityScore(undefined);
if (currentComplexityScore === undefined) {
fetchComplexityFromMCP();
}
}
}, [currentTask?.id, currentComplexityScore]);
return (
<div className="md:col-span-1 border-l border-textSeparator-foreground">
<div className="p-6">
<div className="space-y-6">
<div>
<h3 className="text-sm font-medium text-vscode-foreground/70 mb-3">
Properties
</h3>
</div>
<div className="space-y-4">
{/* Status */}
<div className="flex items-center justify-between">
<span className="text-sm text-vscode-foreground/70">Status</span>
<select
value={currentTask.status}
onChange={(e) =>
onStatusChange(e.target.value as TaskMasterTask['status'])
}
className="border rounded-md px-3 py-1 text-sm font-medium focus:ring-1 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
style={{
backgroundColor:
currentTask.status === 'pending'
? 'rgba(156, 163, 175, 0.2)'
: currentTask.status === 'in-progress'
? 'rgba(245, 158, 11, 0.2)'
: currentTask.status === 'review'
? 'rgba(59, 130, 246, 0.2)'
: currentTask.status === 'done'
? 'rgba(34, 197, 94, 0.2)'
: currentTask.status === 'deferred'
? 'rgba(239, 68, 68, 0.2)'
: 'var(--vscode-input-background)',
color:
currentTask.status === 'pending'
? 'var(--vscode-foreground)'
: currentTask.status === 'in-progress'
? '#d97706'
: currentTask.status === 'review'
? '#2563eb'
: currentTask.status === 'done'
? '#16a34a'
: currentTask.status === 'deferred'
? '#dc2626'
: 'var(--vscode-foreground)',
borderColor:
currentTask.status === 'pending'
? 'rgba(156, 163, 175, 0.4)'
: currentTask.status === 'in-progress'
? 'rgba(245, 158, 11, 0.4)'
: currentTask.status === 'review'
? 'rgba(59, 130, 246, 0.4)'
: currentTask.status === 'done'
? 'rgba(34, 197, 94, 0.4)'
: currentTask.status === 'deferred'
? 'rgba(239, 68, 68, 0.4)'
: 'var(--vscode-input-border)'
}}
>
<option value="pending">To do</option>
<option value="in-progress">In Progress</option>
<option value="review">Review</option>
<option value="done">Done</option>
<option value="deferred">Deferred</option>
</select>
</div>
{/* Priority */}
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Priority</span>
<PriorityBadge priority={currentTask.priority} />
</div>
{/* Complexity Score */}
<div className="space-y-2">
<label className="text-sm font-medium text-[var(--vscode-foreground)]">
Complexity Score
</label>
{isLoadingComplexity ? (
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-[var(--vscode-descriptionForeground)]" />
<span className="text-sm text-[var(--vscode-descriptionForeground)]">
Loading...
</span>
</div>
) : displayComplexityScore !== undefined ? (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-[var(--vscode-foreground)]">
{displayComplexityScore}/10
</span>
<div
className={`flex-1 rounded-full h-2 ${
displayComplexityScore >= 7
? 'bg-red-500/20'
: displayComplexityScore >= 4
? 'bg-yellow-500/20'
: 'bg-green-500/20'
}`}
>
<div
className={`h-2 rounded-full transition-all duration-300 ${
displayComplexityScore >= 7
? 'bg-red-500'
: displayComplexityScore >= 4
? 'bg-yellow-500'
: 'bg-green-500'
}`}
style={{
width: `${(displayComplexityScore || 0) * 10}%`
}}
/>
</div>
</div>
) : currentTask?.status === 'done' ||
currentTask?.status === 'deferred' ||
currentTask?.status === 'review' ? (
<div className="text-sm text-[var(--vscode-descriptionForeground)]">
N/A
</div>
) : (
<>
<div className="text-sm text-[var(--vscode-descriptionForeground)]">
No complexity score available
</div>
<div className="mt-3">
<Button
onClick={() => handleRunComplexityAnalysis()}
variant="outline"
size="sm"
className="text-xs"
disabled={isRegenerating || isAppending}
>
Run Complexity Analysis
</Button>
</div>
</>
)}
</div>
</div>
<div className="border-b border-textSeparator-foreground" />
{/* Dependencies */}
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
<div>
<h4 className="text-sm font-medium text-vscode-foreground/70 mb-3">
Dependencies
</h4>
<div className="space-y-2">
{currentTask.dependencies.map((depId) => {
// Convert both to string for comparison since depId might be string or number
const depTask = tasks.find(
(t) => String(t.id) === String(depId)
);
const fullTitle = `Task ${depId}: ${depTask?.title || 'Unknown Task'}`;
const truncatedTitle =
fullTitle.length > 40
? fullTitle.substring(0, 37) + '...'
: fullTitle;
return (
<div
key={depId}
className="text-sm text-link cursor-pointer hover:text-link-hover"
onClick={() => onDependencyClick(depId)}
title={fullTitle}
>
{truncatedTitle}
</div>
);
})}
</div>
</div>
)}
{/* Divider after Dependencies */}
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
<div className="border-b border-textSeparator-foreground" />
)}
{/* Start Task Button */}
<div className="mt-4">
<Button
onClick={handleStartTask}
variant="default"
size="sm"
className="w-full text-xs"
disabled={
isRegenerating ||
isAppending ||
isStartingTask ||
currentTask?.status === 'done' ||
currentTask?.status === 'in-progress'
}
>
{isStartingTask ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Play className="w-4 h-4 mr-2" />
)}
{isStartingTask ? 'Starting...' : 'Start Task'}
</Button>
</div>
</div>
</div>
</div>
);
};
```
--------------------------------------------------------------------------------
/scripts/modules/utils/fuzzyTaskSearch.js:
--------------------------------------------------------------------------------
```javascript
/**
* fuzzyTaskSearch.js
* Reusable fuzzy search utility for finding relevant tasks based on semantic similarity
*/
import Fuse from 'fuse.js';
/**
* Configuration for different search contexts
*/
const SEARCH_CONFIGS = {
research: {
threshold: 0.5, // More lenient for research (broader context)
limit: 20,
keys: [
{ name: 'title', weight: 2.0 },
{ name: 'description', weight: 1.0 },
{ name: 'details', weight: 0.5 },
{ name: 'dependencyTitles', weight: 0.5 }
]
},
addTask: {
threshold: 0.4, // Stricter for add-task (more precise context)
limit: 15,
keys: [
{ name: 'title', weight: 2.0 },
{ name: 'description', weight: 1.5 },
{ name: 'details', weight: 0.8 },
{ name: 'dependencyTitles', weight: 0.5 }
]
},
default: {
threshold: 0.4,
limit: 15,
keys: [
{ name: 'title', weight: 2.0 },
{ name: 'description', weight: 1.5 },
{ name: 'details', weight: 1.0 },
{ name: 'dependencyTitles', weight: 0.5 }
]
}
};
/**
* Purpose categories for pattern-based task matching
*/
const PURPOSE_CATEGORIES = [
{ pattern: /(command|cli|flag)/i, label: 'CLI commands' },
{ pattern: /(task|subtask|add)/i, label: 'Task management' },
{ pattern: /(dependency|depend)/i, label: 'Dependency handling' },
{ pattern: /(AI|model|prompt|research)/i, label: 'AI integration' },
{ pattern: /(UI|display|show|interface)/i, label: 'User interface' },
{ pattern: /(schedule|time|cron)/i, label: 'Scheduling' },
{ pattern: /(config|setting|option)/i, label: 'Configuration' },
{ pattern: /(test|testing|spec)/i, label: 'Testing' },
{ pattern: /(auth|login|user)/i, label: 'Authentication' },
{ pattern: /(database|db|data)/i, label: 'Data management' },
{ pattern: /(api|endpoint|route)/i, label: 'API development' },
{ pattern: /(deploy|build|release)/i, label: 'Deployment' },
{ pattern: /(security|auth|login|user)/i, label: 'Security' },
{ pattern: /.*/, label: 'Other' }
];
/**
* Relevance score thresholds
*/
const RELEVANCE_THRESHOLDS = {
high: 0.25,
medium: 0.4,
low: 0.6
};
/**
* Fuzzy search utility class for finding relevant tasks
*/
export class FuzzyTaskSearch {
constructor(tasks, searchType = 'default') {
this.tasks = tasks;
this.config = SEARCH_CONFIGS[searchType] || SEARCH_CONFIGS.default;
this.searchableTasks = this._prepareSearchableTasks(tasks);
this.fuse = new Fuse(this.searchableTasks, {
includeScore: true,
threshold: this.config.threshold,
keys: this.config.keys,
shouldSort: true,
useExtendedSearch: true,
limit: this.config.limit
});
}
/**
* Prepare tasks for searching by expanding dependency titles
* @param {Array} tasks - Array of task objects
* @returns {Array} Tasks with expanded dependency information
*/
_prepareSearchableTasks(tasks) {
return tasks.map((task) => {
// Get titles of this task's dependencies if they exist
const dependencyTitles =
task.dependencies?.length > 0
? task.dependencies
.map((depId) => {
const depTask = tasks.find((t) => t.id === depId);
return depTask ? depTask.title : '';
})
.filter((title) => title)
.join(' ')
: '';
return {
...task,
dependencyTitles
};
});
}
/**
* Extract significant words from a prompt
* @param {string} prompt - The search prompt
* @returns {Array<string>} Array of significant words
*/
_extractPromptWords(prompt) {
return prompt
.toLowerCase()
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
.split(/\s+/)
.filter((word) => word.length > 3); // Words at least 4 chars
}
/**
* Find tasks related to a prompt using fuzzy search
* @param {string} prompt - The search prompt
* @param {Object} options - Search options
* @param {number} [options.maxResults=8] - Maximum number of results to return
* @param {boolean} [options.includeRecent=true] - Include recent tasks in results
* @param {boolean} [options.includeCategoryMatches=true] - Include category-based matches
* @returns {Object} Search results with relevance breakdown
*/
findRelevantTasks(prompt, options = {}) {
const {
maxResults = 8,
includeRecent = true,
includeCategoryMatches = true
} = options;
// Extract significant words from prompt
const promptWords = this._extractPromptWords(prompt);
// Perform fuzzy search with full prompt
const fuzzyResults = this.fuse.search(prompt);
// Also search for each significant word to catch different aspects
let wordResults = [];
for (const word of promptWords) {
if (word.length > 5) {
// Only use significant words
const results = this.fuse.search(word);
if (results.length > 0) {
wordResults.push(...results);
}
}
}
// Merge and deduplicate results
const mergedResults = [...fuzzyResults];
// Add word results that aren't already in fuzzyResults
for (const wordResult of wordResults) {
if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
mergedResults.push(wordResult);
}
}
// Group search results by relevance
const highRelevance = mergedResults
.filter((result) => result.score < RELEVANCE_THRESHOLDS.high)
.map((result) => ({ ...result.item, score: result.score }));
const mediumRelevance = mergedResults
.filter(
(result) =>
result.score >= RELEVANCE_THRESHOLDS.high &&
result.score < RELEVANCE_THRESHOLDS.medium
)
.map((result) => ({ ...result.item, score: result.score }));
const lowRelevance = mergedResults
.filter(
(result) =>
result.score >= RELEVANCE_THRESHOLDS.medium &&
result.score < RELEVANCE_THRESHOLDS.low
)
.map((result) => ({ ...result.item, score: result.score }));
// Get recent tasks (newest first) if requested
const recentTasks = includeRecent
? [...this.tasks].sort((a, b) => b.id - a.id).slice(0, 5)
: [];
// Find category-based matches if requested
let categoryTasks = [];
let promptCategory = null;
if (includeCategoryMatches) {
promptCategory = PURPOSE_CATEGORIES.find((cat) =>
cat.pattern.test(prompt)
);
categoryTasks = promptCategory
? this.tasks
.filter(
(t) =>
promptCategory.pattern.test(t.title) ||
promptCategory.pattern.test(t.description) ||
(t.details && promptCategory.pattern.test(t.details))
)
.slice(0, 3)
: [];
}
// Combine all relevant tasks, prioritizing by relevance
const allRelevantTasks = [...highRelevance];
// Add medium relevance if not already included
for (const task of mediumRelevance) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Add low relevance if not already included
for (const task of lowRelevance) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Add category tasks if not already included
for (const task of categoryTasks) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Add recent tasks if not already included
for (const task of recentTasks) {
if (!allRelevantTasks.some((t) => t.id === task.id)) {
allRelevantTasks.push(task);
}
}
// Get top N results for final output
const finalResults = allRelevantTasks.slice(0, maxResults);
return {
results: finalResults,
breakdown: {
highRelevance,
mediumRelevance,
lowRelevance,
categoryTasks,
recentTasks,
promptCategory,
promptWords
},
metadata: {
totalSearched: this.tasks.length,
fuzzyMatches: fuzzyResults.length,
wordMatches: wordResults.length,
finalCount: finalResults.length
}
};
}
/**
* Get task IDs from search results
* @param {Object} searchResults - Results from findRelevantTasks
* @returns {Array<string>} Array of task ID strings
*/
getTaskIds(searchResults) {
return searchResults.results.map((task) => task.id.toString());
}
/**
* Get task IDs including subtasks from search results
* @param {Object} searchResults - Results from findRelevantTasks
* @param {boolean} [includeSubtasks=false] - Whether to include subtask IDs
* @returns {Array<string>} Array of task and subtask ID strings
*/
getTaskIdsWithSubtasks(searchResults, includeSubtasks = false) {
const taskIds = [];
for (const task of searchResults.results) {
taskIds.push(task.id.toString());
if (includeSubtasks && task.subtasks && task.subtasks.length > 0) {
for (const subtask of task.subtasks) {
taskIds.push(`${task.id}.${subtask.id}`);
}
}
}
return taskIds;
}
/**
* Format search results for display
* @param {Object} searchResults - Results from findRelevantTasks
* @param {Object} options - Formatting options
* @returns {string} Formatted search results summary
*/
formatSearchSummary(searchResults, options = {}) {
const { includeScores = false, includeBreakdown = false } = options;
const { results, breakdown, metadata } = searchResults;
let summary = `Found ${results.length} relevant tasks from ${metadata.totalSearched} total tasks`;
if (includeBreakdown && breakdown) {
const parts = [];
if (breakdown.highRelevance.length > 0)
parts.push(`${breakdown.highRelevance.length} high relevance`);
if (breakdown.mediumRelevance.length > 0)
parts.push(`${breakdown.mediumRelevance.length} medium relevance`);
if (breakdown.lowRelevance.length > 0)
parts.push(`${breakdown.lowRelevance.length} low relevance`);
if (breakdown.categoryTasks.length > 0)
parts.push(`${breakdown.categoryTasks.length} category matches`);
if (parts.length > 0) {
summary += ` (${parts.join(', ')})`;
}
if (breakdown.promptCategory) {
summary += `\nCategory detected: ${breakdown.promptCategory.label}`;
}
}
return summary;
}
}
/**
* Factory function to create a fuzzy search instance
* @param {Array} tasks - Array of task objects
* @param {string} [searchType='default'] - Type of search configuration to use
* @returns {FuzzyTaskSearch} Fuzzy search instance
*/
export function createFuzzyTaskSearch(tasks, searchType = 'default') {
return new FuzzyTaskSearch(tasks, searchType);
}
/**
* Quick utility function to find relevant task IDs for a prompt
* @param {Array} tasks - Array of task objects
* @param {string} prompt - Search prompt
* @param {Object} options - Search options
* @returns {Array<string>} Array of relevant task ID strings
*/
export function findRelevantTaskIds(tasks, prompt, options = {}) {
const {
searchType = 'default',
maxResults = 8,
includeSubtasks = false
} = options;
const fuzzySearch = new FuzzyTaskSearch(tasks, searchType);
const results = fuzzySearch.findRelevantTasks(prompt, { maxResults });
return includeSubtasks
? fuzzySearch.getTaskIdsWithSubtasks(results, true)
: fuzzySearch.getTaskIds(results);
}
export default FuzzyTaskSearch;
```
--------------------------------------------------------------------------------
/src/ai-providers/base-provider.js:
--------------------------------------------------------------------------------
```javascript
import {
generateObject,
generateText,
streamText,
streamObject,
zodSchema,
JSONParseError,
NoObjectGeneratedError
} from 'ai';
import { jsonrepair } from 'jsonrepair';
import { log, findProjectRoot } from '../../scripts/modules/utils.js';
import { isProxyEnabled } from '../../scripts/modules/config-manager.js';
import { EnvHttpProxyAgent } from 'undici';
/**
* Base class for all AI providers
*/
export class BaseAIProvider {
constructor() {
if (this.constructor === BaseAIProvider) {
throw new Error('BaseAIProvider cannot be instantiated directly');
}
// Each provider must set their name
this.name = this.constructor.name;
// Cache proxy agent to avoid creating multiple instances
this._proxyAgent = null;
/**
* Whether this provider needs explicit schema in JSON mode
* Can be overridden by subclasses
* @type {boolean}
*/
this.needsExplicitJsonSchema = false;
/**
* Whether this provider supports temperature parameter
* Can be overridden by subclasses
* @type {boolean}
*/
this.supportsTemperature = true;
}
/**
* Validates authentication parameters - can be overridden by providers
* @param {object} params - Parameters to validate
*/
validateAuth(params) {
// Default: require API key (most providers need this)
if (!params.apiKey) {
throw new Error(`${this.name} API key is required`);
}
}
/**
* Creates a custom fetch function with proxy support.
* Only enables proxy when TASKMASTER_ENABLE_PROXY environment variable is set to 'true'
* or enableProxy is set to true in config.json.
* Automatically reads http_proxy/https_proxy environment variables when enabled.
* @returns {Function} Custom fetch function with proxy support, or undefined if proxy is disabled
*/
createProxyFetch() {
// Cache project root to avoid repeated lookups
if (!this._projectRoot) {
this._projectRoot = findProjectRoot();
}
const projectRoot = this._projectRoot;
if (!isProxyEnabled(null, projectRoot)) {
// Return undefined to use default fetch without proxy
return undefined;
}
// Proxy is enabled, create and return proxy fetch
if (!this._proxyAgent) {
this._proxyAgent = new EnvHttpProxyAgent();
}
return (url, options = {}) => {
return fetch(url, {
...options,
dispatcher: this._proxyAgent
});
};
}
/**
* Validates common parameters across all methods
* @param {object} params - Parameters to validate
*/
validateParams(params) {
// Validate authentication (can be overridden by providers)
this.validateAuth(params);
// Validate required model ID
if (!params.modelId) {
throw new Error(`${this.name} Model ID is required`);
}
// Validate optional parameters
this.validateOptionalParams(params);
}
/**
* Validates optional parameters like temperature and maxTokens
* @param {object} params - Parameters to validate
*/
validateOptionalParams(params) {
if (
params.temperature !== undefined &&
(params.temperature < 0 || params.temperature > 1)
) {
throw new Error('Temperature must be between 0 and 1');
}
if (params.maxTokens !== undefined) {
const maxTokens = Number(params.maxTokens);
if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
throw new Error('maxTokens must be a finite number greater than 0');
}
}
}
/**
* Validates message array structure
*/
validateMessages(messages) {
if (!messages || !Array.isArray(messages) || messages.length === 0) {
throw new Error('Invalid or empty messages array provided');
}
for (const msg of messages) {
if (!msg.role || !msg.content) {
throw new Error(
'Invalid message format. Each message must have role and content'
);
}
}
}
/**
* Common error handler
*/
handleError(operation, error) {
const errorMessage = error.message || 'Unknown error occurred';
log('error', `${this.name} ${operation} failed: ${errorMessage}`, {
error
});
throw new Error(
`${this.name} API error during ${operation}: ${errorMessage}`
);
}
/**
* Creates and returns a client instance for the provider
* @abstract
*/
getClient(params) {
throw new Error('getClient must be implemented by provider');
}
/**
* Returns if the API key is required
* @abstract
* @returns {boolean} if the API key is required, defaults to true
*/
isRequiredApiKey() {
return true;
}
/**
* Returns the required API key environment variable name
* @abstract
* @returns {string|null} The environment variable name, or null if no API key is required
*/
getRequiredApiKeyName() {
throw new Error('getRequiredApiKeyName must be implemented by provider');
}
/**
* Prepares token limit parameter based on model requirements
* @param {string} modelId - The model ID
* @param {number} maxTokens - The maximum tokens value
* @returns {object} Object with either maxTokens or max_completion_tokens
*/
prepareTokenParam(modelId, maxTokens) {
if (maxTokens === undefined) {
return {};
}
// Ensure maxTokens is an integer
const tokenValue = Math.floor(Number(maxTokens));
return { maxOutputTokens: tokenValue };
}
/**
* Generates text using the provider's model
*/
async generateText(params) {
try {
this.validateParams(params);
this.validateMessages(params.messages);
log(
'debug',
`Generating ${this.name} text with model: ${params.modelId}`
);
const client = await this.getClient(params);
const result = await generateText({
model: client(params.modelId),
messages: params.messages,
...this.prepareTokenParam(params.modelId, params.maxTokens),
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
'debug',
`${this.name} generateText completed successfully for model: ${params.modelId}`
);
const inputTokens =
result.usage?.inputTokens ?? result.usage?.promptTokens ?? 0;
const outputTokens =
result.usage?.outputTokens ?? result.usage?.completionTokens ?? 0;
const totalTokens =
result.usage?.totalTokens ?? inputTokens + outputTokens;
return {
text: result.text,
usage: {
inputTokens,
outputTokens,
totalTokens
}
};
} catch (error) {
this.handleError('text generation', error);
}
}
/**
* Streams text using the provider's model
*/
async streamText(params) {
try {
this.validateParams(params);
this.validateMessages(params.messages);
log('debug', `Streaming ${this.name} text with model: ${params.modelId}`);
const client = await this.getClient(params);
const stream = await streamText({
model: client(params.modelId),
messages: params.messages,
...this.prepareTokenParam(params.modelId, params.maxTokens),
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
'debug',
`${this.name} streamText initiated successfully for model: ${params.modelId}`
);
return stream;
} catch (error) {
this.handleError('text streaming', error);
}
}
/**
* Streams a structured object using the provider's model
*/
async streamObject(params) {
try {
this.validateParams(params);
this.validateMessages(params.messages);
if (!params.schema) {
throw new Error('Schema is required for object streaming');
}
log(
'debug',
`Streaming ${this.name} object with model: ${params.modelId}`
);
const client = await this.getClient(params);
const result = await streamObject({
model: client(params.modelId),
messages: params.messages,
schema: zodSchema(params.schema),
mode: params.mode || 'auto',
maxOutputTokens: params.maxTokens,
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
'debug',
`${this.name} streamObject initiated successfully for model: ${params.modelId}`
);
// Return the stream result directly
// The stream result contains partialObjectStream and other properties
return result;
} catch (error) {
this.handleError('object streaming', error);
}
}
/**
* Generates a structured object using the provider's model
*/
async generateObject(params) {
try {
this.validateParams(params);
this.validateMessages(params.messages);
if (!params.schema) {
throw new Error('Schema is required for object generation');
}
if (!params.objectName) {
throw new Error('Object name is required for object generation');
}
log(
'debug',
`Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}`
);
const client = await this.getClient(params);
const result = await generateObject({
model: client(params.modelId),
messages: params.messages,
schema: params.schema,
mode: this.needsExplicitJsonSchema ? 'json' : 'auto',
schemaName: params.objectName,
schemaDescription: `Generate a valid JSON object for ${params.objectName}`,
maxTokens: params.maxTokens,
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
'debug',
`${this.name} generateObject completed successfully for model: ${params.modelId}`
);
const inputTokens =
result.usage?.inputTokens ?? result.usage?.promptTokens ?? 0;
const outputTokens =
result.usage?.outputTokens ?? result.usage?.completionTokens ?? 0;
const totalTokens =
result.usage?.totalTokens ?? inputTokens + outputTokens;
return {
object: result.object,
usage: {
inputTokens,
outputTokens,
totalTokens
}
};
} catch (error) {
// Check if this is a JSON parsing error that we can potentially fix
if (
NoObjectGeneratedError.isInstance(error) &&
error.cause instanceof JSONParseError &&
error.cause.text
) {
log(
'warn',
`${this.name} generated malformed JSON, attempting to repair...`
);
try {
// Use jsonrepair to fix the malformed JSON
const repairedJson = jsonrepair(error.cause.text);
const parsed = JSON.parse(repairedJson);
log('info', `Successfully repaired ${this.name} JSON output`);
// Return in the expected format
return {
object: parsed,
usage: {
// Extract usage information from the error if available
inputTokens:
error.usage?.promptTokens || error.usage?.inputTokens || 0,
outputTokens:
error.usage?.completionTokens || error.usage?.outputTokens || 0,
totalTokens: error.usage?.totalTokens || 0
}
};
} catch (repairError) {
log(
'error',
`Failed to repair ${this.name} JSON: ${repairError.message}`
);
// Fall through to handleError with original error
}
}
this.handleError('object generation', error);
}
}
}
```
--------------------------------------------------------------------------------
/tests/integration/cli/commands.test.js:
--------------------------------------------------------------------------------
```javascript
import { jest } from '@jest/globals';
// --- Define mock functions ---
const mockGetMainModelId = jest.fn().mockReturnValue('claude-3-opus');
const mockGetResearchModelId = jest.fn().mockReturnValue('gpt-4-turbo');
const mockGetFallbackModelId = jest.fn().mockReturnValue('claude-3-haiku');
const mockSetMainModel = jest.fn().mockResolvedValue(true);
const mockSetResearchModel = jest.fn().mockResolvedValue(true);
const mockSetFallbackModel = jest.fn().mockResolvedValue(true);
const mockGetAvailableModels = jest.fn().mockReturnValue([
{ id: 'claude-3-opus', name: 'Claude 3 Opus', provider: 'anthropic' },
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai' },
{ id: 'claude-3-haiku', name: 'Claude 3 Haiku', provider: 'anthropic' },
{ id: 'claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'anthropic' }
]);
// Mock UI related functions
const mockDisplayHelp = jest.fn();
const mockDisplayBanner = jest.fn();
const mockLog = jest.fn();
const mockStartLoadingIndicator = jest.fn(() => ({ stop: jest.fn() }));
const mockStopLoadingIndicator = jest.fn();
// --- Setup mocks using unstable_mockModule (recommended for ES modules) ---
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
getMainModelId: mockGetMainModelId,
getResearchModelId: mockGetResearchModelId,
getFallbackModelId: mockGetFallbackModelId,
setMainModel: mockSetMainModel,
setResearchModel: mockSetResearchModel,
setFallbackModel: mockSetFallbackModel,
getAvailableModels: mockGetAvailableModels,
VALID_PROVIDERS: ['anthropic', 'openai']
}));
jest.unstable_mockModule('../../../scripts/modules/ui.js', () => ({
displayHelp: mockDisplayHelp,
displayBanner: mockDisplayBanner,
log: mockLog,
startLoadingIndicator: mockStartLoadingIndicator,
stopLoadingIndicator: mockStopLoadingIndicator
}));
// --- Mock chalk for consistent output formatting ---
const mockChalk = {
red: jest.fn((text) => text),
yellow: jest.fn((text) => text),
blue: jest.fn((text) => text),
green: jest.fn((text) => text),
gray: jest.fn((text) => text),
dim: jest.fn((text) => text),
bold: {
cyan: jest.fn((text) => text),
white: jest.fn((text) => text),
red: jest.fn((text) => text)
},
cyan: {
bold: jest.fn((text) => text)
},
white: {
bold: jest.fn((text) => text)
}
};
// Default function for chalk itself
mockChalk.default = jest.fn((text) => text);
// Add the methods to the function itself for dual usage
Object.keys(mockChalk).forEach((key) => {
if (key !== 'default') mockChalk.default[key] = mockChalk[key];
});
jest.unstable_mockModule('chalk', () => ({
default: mockChalk.default
}));
// --- Import modules (AFTER mock setup) ---
let configManager, ui, chalk;
describe('CLI Models Command (Action Handler Test)', () => {
// Setup dynamic imports before tests run
beforeAll(async () => {
configManager = await import('../../../scripts/modules/config-manager.js');
ui = await import('../../../scripts/modules/ui.js');
chalk = (await import('chalk')).default;
});
// --- Replicate the action handler logic from commands.js ---
async function modelsAction(options) {
options = options || {}; // Ensure options object exists
const availableModels = configManager.getAvailableModels();
const findProvider = (modelId) => {
const modelInfo = availableModels.find((m) => m.id === modelId);
return modelInfo?.provider;
};
let modelSetAction = false;
try {
if (options.setMain) {
const modelId = options.setMain;
if (typeof modelId !== 'string' || modelId.trim() === '') {
console.error(
chalk.red('Error: --set-main flag requires a valid model ID.')
);
process.exit(1);
}
const provider = findProvider(modelId);
if (!provider) {
console.error(
chalk.red(
`Error: Model ID "${modelId}" not found in available models.`
)
);
process.exit(1);
}
if (await configManager.setMainModel(provider, modelId)) {
console.log(
chalk.green(`Main model set to: ${modelId} (Provider: ${provider})`)
);
modelSetAction = true;
} else {
console.error(chalk.red(`Failed to set main model.`));
process.exit(1);
}
}
if (options.setResearch) {
const modelId = options.setResearch;
if (typeof modelId !== 'string' || modelId.trim() === '') {
console.error(
chalk.red('Error: --set-research flag requires a valid model ID.')
);
process.exit(1);
}
const provider = findProvider(modelId);
if (!provider) {
console.error(
chalk.red(
`Error: Model ID "${modelId}" not found in available models.`
)
);
process.exit(1);
}
if (await configManager.setResearchModel(provider, modelId)) {
console.log(
chalk.green(
`Research model set to: ${modelId} (Provider: ${provider})`
)
);
modelSetAction = true;
} else {
console.error(chalk.red(`Failed to set research model.`));
process.exit(1);
}
}
if (options.setFallback) {
const modelId = options.setFallback;
if (typeof modelId !== 'string' || modelId.trim() === '') {
console.error(
chalk.red('Error: --set-fallback flag requires a valid model ID.')
);
process.exit(1);
}
const provider = findProvider(modelId);
if (!provider) {
console.error(
chalk.red(
`Error: Model ID "${modelId}" not found in available models.`
)
);
process.exit(1);
}
if (await configManager.setFallbackModel(provider, modelId)) {
console.log(
chalk.green(
`Fallback model set to: ${modelId} (Provider: ${provider})`
)
);
modelSetAction = true;
} else {
console.error(chalk.red(`Failed to set fallback model.`));
process.exit(1);
}
}
if (!modelSetAction) {
const currentMain = configManager.getMainModelId();
const currentResearch = configManager.getResearchModelId();
const currentFallback = configManager.getFallbackModelId();
if (!availableModels || availableModels.length === 0) {
console.log(chalk.yellow('No models defined in configuration.'));
return;
}
// Create a mock table for testing - avoid using Table constructor
const mockTableData = [];
availableModels.forEach((model) => {
if (model.id.startsWith('[') && model.id.endsWith(']')) return;
mockTableData.push([
model.id,
model.name || 'N/A',
model.provider || 'N/A',
model.id === currentMain ? chalk.green(' ✓') : '',
model.id === currentResearch ? chalk.green(' ✓') : '',
model.id === currentFallback ? chalk.green(' ✓') : ''
]);
});
// In a real implementation, we would use cli-table3, but for testing
// we'll just log 'Mock Table Output'
console.log('Mock Table Output');
}
} catch (error) {
// Use ui.log mock if available, otherwise console.error
(ui.log || console.error)(
`Error processing models command: ${error.message}`,
'error'
);
if (error.stack) {
(ui.log || console.error)(error.stack, 'debug');
}
throw error; // Re-throw for test failure
}
}
// --- End of Action Handler Logic ---
let originalConsoleLog;
let originalConsoleError;
let originalProcessExit;
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Save original console methods
originalConsoleLog = console.log;
originalConsoleError = console.error;
originalProcessExit = process.exit;
// Mock console and process.exit
console.log = jest.fn();
console.error = jest.fn();
process.exit = jest.fn((code) => {
throw new Error(`process.exit(${code}) called`);
});
});
afterEach(() => {
// Restore original console methods
console.log = originalConsoleLog;
console.error = originalConsoleError;
process.exit = originalProcessExit;
});
// --- Test Cases (Calling modelsAction directly) ---
it('should call setMainModel with correct provider and ID', async () => {
const modelId = 'claude-3-opus';
const expectedProvider = 'anthropic';
await modelsAction({ setMain: modelId });
expect(mockSetMainModel).toHaveBeenCalledWith(expectedProvider, modelId);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining(`Main model set to: ${modelId}`)
);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining(`(Provider: ${expectedProvider})`)
);
});
it('should show an error if --set-main model ID is not found', async () => {
await expect(
modelsAction({ setMain: 'non-existent-model' })
).rejects.toThrow(/process.exit/); // Expect exit call
expect(mockSetMainModel).not.toHaveBeenCalled();
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining('Model ID "non-existent-model" not found')
);
});
it('should call setResearchModel with correct provider and ID', async () => {
const modelId = 'gpt-4-turbo';
const expectedProvider = 'openai';
await modelsAction({ setResearch: modelId });
expect(mockSetResearchModel).toHaveBeenCalledWith(
expectedProvider,
modelId
);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining(`Research model set to: ${modelId}`)
);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining(`(Provider: ${expectedProvider})`)
);
});
it('should call setFallbackModel with correct provider and ID', async () => {
const modelId = 'claude-3-haiku';
const expectedProvider = 'anthropic';
await modelsAction({ setFallback: modelId });
expect(mockSetFallbackModel).toHaveBeenCalledWith(
expectedProvider,
modelId
);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining(`Fallback model set to: ${modelId}`)
);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining(`(Provider: ${expectedProvider})`)
);
});
it('should call all set*Model functions when all flags are used', async () => {
const mainModelId = 'claude-3-opus';
const researchModelId = 'gpt-4-turbo';
const fallbackModelId = 'claude-3-haiku';
const mainProvider = 'anthropic';
const researchProvider = 'openai';
const fallbackProvider = 'anthropic';
await modelsAction({
setMain: mainModelId,
setResearch: researchModelId,
setFallback: fallbackModelId
});
expect(mockSetMainModel).toHaveBeenCalledWith(mainProvider, mainModelId);
expect(mockSetResearchModel).toHaveBeenCalledWith(
researchProvider,
researchModelId
);
expect(mockSetFallbackModel).toHaveBeenCalledWith(
fallbackProvider,
fallbackModelId
);
});
it('should call specific get*ModelId and getAvailableModels and log table when run without flags', async () => {
await modelsAction({}); // Call with empty options
expect(mockGetMainModelId).toHaveBeenCalled();
expect(mockGetResearchModelId).toHaveBeenCalled();
expect(mockGetFallbackModelId).toHaveBeenCalled();
expect(mockGetAvailableModels).toHaveBeenCalled();
expect(console.log).toHaveBeenCalled();
// Check the mocked Table.toString() was used via console.log
expect(console.log).toHaveBeenCalledWith('Mock Table Output');
});
});
```
--------------------------------------------------------------------------------
/packages/tm-core/tests/integration/storage/activity-logger.test.ts:
--------------------------------------------------------------------------------
```typescript
import os from 'os';
import path from 'path';
import fs from 'fs-extra';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
filterActivityLog,
logActivity,
readActivityLog
} from '../../../src/storage/activity-logger.js';
describe('Activity Logger', () => {
let testDir: string;
let activityPath: string;
beforeEach(async () => {
// Create a unique temporary test directory
const prefix = path.join(os.tmpdir(), 'activity-test-');
testDir = await fs.mkdtemp(prefix);
activityPath = path.join(testDir, 'activity.jsonl');
});
afterEach(async () => {
// Clean up test directory
await fs.remove(testDir);
});
describe('logActivity', () => {
it('should create activity log file on first write', async () => {
await logActivity(activityPath, {
type: 'phase-start',
phase: 'red',
data: {}
});
const exists = await fs.pathExists(activityPath);
expect(exists).toBe(true);
});
it('should append event to log file', async () => {
await logActivity(activityPath, {
type: 'phase-start',
phase: 'red'
});
const content = await fs.readFile(activityPath, 'utf-8');
const lines = content.trim().split(/\r?\n/);
expect(lines.length).toBe(1);
});
it('should write valid JSONL format', async () => {
await logActivity(activityPath, {
type: 'test-run',
result: 'pass'
});
const content = await fs.readFile(activityPath, 'utf-8');
const line = content.trim();
const parsed = JSON.parse(line);
expect(parsed).toBeDefined();
expect(parsed.type).toBe('test-run');
});
it('should include timestamp in log entry', async () => {
const before = new Date().toISOString();
await logActivity(activityPath, {
type: 'phase-start',
phase: 'red'
});
const after = new Date().toISOString();
const logs = await readActivityLog(activityPath);
expect(logs[0].timestamp).toBeDefined();
expect(logs[0].timestamp >= before).toBe(true);
expect(logs[0].timestamp <= after).toBe(true);
});
it('should append multiple events', async () => {
await logActivity(activityPath, { type: 'event1' });
await logActivity(activityPath, { type: 'event2' });
await logActivity(activityPath, { type: 'event3' });
const logs = await readActivityLog(activityPath);
expect(logs.length).toBe(3);
expect(logs[0].type).toBe('event1');
expect(logs[1].type).toBe('event2');
expect(logs[2].type).toBe('event3');
});
it('should preserve event data', async () => {
const eventData = {
type: 'git-commit',
hash: 'abc123',
message: 'test commit',
files: ['file1.ts', 'file2.ts']
};
await logActivity(activityPath, eventData);
const logs = await readActivityLog(activityPath);
expect(logs[0].type).toBe('git-commit');
expect(logs[0].hash).toBe('abc123');
expect(logs[0].message).toBe('test commit');
expect(logs[0].files).toEqual(['file1.ts', 'file2.ts']);
});
it('should handle nested objects in event data', async () => {
await logActivity(activityPath, {
type: 'test-results',
results: {
passed: 10,
failed: 2,
details: { coverage: 85 }
}
});
const logs = await readActivityLog(activityPath);
expect(logs[0].results.details.coverage).toBe(85);
});
it('should handle special characters in event data', async () => {
await logActivity(activityPath, {
type: 'error',
message: 'Error: "Something went wrong"\nLine 2'
});
const logs = await readActivityLog(activityPath);
expect(logs[0].message).toBe('Error: "Something went wrong"\nLine 2');
});
it('should create parent directory if it does not exist', async () => {
const nestedPath = path.join(testDir, 'nested', 'dir', 'activity.jsonl');
await logActivity(nestedPath, { type: 'test' });
const exists = await fs.pathExists(nestedPath);
expect(exists).toBe(true);
});
});
describe('readActivityLog', () => {
it('should read all events from log', async () => {
await logActivity(activityPath, { type: 'event1' });
await logActivity(activityPath, { type: 'event2' });
const logs = await readActivityLog(activityPath);
expect(logs.length).toBe(2);
expect(logs[0].type).toBe('event1');
expect(logs[1].type).toBe('event2');
});
it('should return empty array for non-existent file', async () => {
const logs = await readActivityLog(activityPath);
expect(logs).toEqual([]);
});
it('should parse JSONL correctly', async () => {
await logActivity(activityPath, { type: 'event1', data: 'test1' });
await logActivity(activityPath, { type: 'event2', data: 'test2' });
const logs = await readActivityLog(activityPath);
expect(logs[0].data).toBe('test1');
expect(logs[1].data).toBe('test2');
});
it('should handle empty lines', async () => {
await fs.writeFile(
activityPath,
'{"type":"event1"}\n\n{"type":"event2"}\n'
);
const logs = await readActivityLog(activityPath);
expect(logs.length).toBe(2);
expect(logs[0].type).toBe('event1');
expect(logs[1].type).toBe('event2');
});
it('should throw error for invalid JSON line', async () => {
await fs.writeFile(activityPath, '{"type":"event1"}\ninvalid json\n');
await expect(readActivityLog(activityPath)).rejects.toThrow(
/Invalid JSON/i
);
});
it('should preserve chronological order', async () => {
for (let i = 0; i < 10; i++) {
await logActivity(activityPath, { type: 'event', index: i });
}
const logs = await readActivityLog(activityPath);
for (let i = 0; i < 10; i++) {
expect(logs[i].index).toBe(i);
}
});
});
describe('filterActivityLog', () => {
beforeEach(async () => {
// Create sample log entries
await logActivity(activityPath, { type: 'phase-start', phase: 'red' });
await logActivity(activityPath, { type: 'test-run', result: 'fail' });
await logActivity(activityPath, { type: 'phase-start', phase: 'green' });
await logActivity(activityPath, { type: 'test-run', result: 'pass' });
await logActivity(activityPath, { type: 'git-commit', hash: 'abc123' });
});
it('should filter by event type', async () => {
const filtered = await filterActivityLog(activityPath, {
type: 'phase-start'
});
expect(filtered.length).toBe(2);
expect(filtered[0].type).toBe('phase-start');
expect(filtered[1].type).toBe('phase-start');
});
it('should filter by multiple criteria', async () => {
const filtered = await filterActivityLog(activityPath, {
type: 'test-run',
result: 'pass'
});
expect(filtered.length).toBe(1);
expect(filtered[0].result).toBe('pass');
});
it('should return all events when no filter provided', async () => {
const filtered = await filterActivityLog(activityPath, {});
expect(filtered.length).toBe(5);
});
it('should filter by timestamp range', async () => {
const logs = await readActivityLog(activityPath);
const midpoint = logs[2].timestamp;
const filtered = await filterActivityLog(activityPath, {
timestampFrom: midpoint
});
// Should get events from midpoint onwards (inclusive)
// Expect at least 3 events, may be more due to timestamp collisions
expect(filtered.length).toBeGreaterThanOrEqual(3);
expect(filtered.length).toBeLessThanOrEqual(5);
});
it('should filter by custom predicate', async () => {
const filtered = await filterActivityLog(activityPath, {
predicate: (event: any) => event.phase === 'red'
});
expect(filtered.length).toBe(1);
expect(filtered[0].phase).toBe('red');
});
it('should return empty array for non-matching filter', async () => {
const filtered = await filterActivityLog(activityPath, {
type: 'non-existent'
});
expect(filtered).toEqual([]);
});
it('should handle nested property filters', async () => {
await logActivity(activityPath, {
type: 'test-results',
results: { coverage: 85 }
});
const filtered = await filterActivityLog(activityPath, {
predicate: (event: any) => event.results?.coverage > 80
});
expect(filtered.length).toBe(1);
expect(filtered[0].results.coverage).toBe(85);
});
});
describe('Event types', () => {
it('should support phase-transition events', async () => {
await logActivity(activityPath, {
type: 'phase-transition',
from: 'red',
to: 'green'
});
const logs = await readActivityLog(activityPath);
expect(logs[0].type).toBe('phase-transition');
expect(logs[0].from).toBe('red');
expect(logs[0].to).toBe('green');
});
it('should support test-run events', async () => {
await logActivity(activityPath, {
type: 'test-run',
result: 'pass',
testsRun: 50,
testsPassed: 50,
testsFailed: 0,
coverage: 85.5
});
const logs = await readActivityLog(activityPath);
expect(logs[0].testsRun).toBe(50);
expect(logs[0].coverage).toBe(85.5);
});
it('should support git-operation events', async () => {
await logActivity(activityPath, {
type: 'git-commit',
hash: 'abc123def456',
message: 'feat: add new feature',
files: ['file1.ts', 'file2.ts']
});
const logs = await readActivityLog(activityPath);
expect(logs[0].hash).toBe('abc123def456');
expect(logs[0].files.length).toBe(2);
});
it('should support error events', async () => {
await logActivity(activityPath, {
type: 'error',
phase: 'red',
error: 'Test failed',
stack: 'Error stack trace...'
});
const logs = await readActivityLog(activityPath);
expect(logs[0].type).toBe('error');
expect(logs[0].error).toBe('Test failed');
});
});
describe('Concurrency handling', () => {
it('should handle rapid concurrent writes', async () => {
const writes: Promise<void>[] = [];
for (let i = 0; i < 50; i++) {
writes.push(logActivity(activityPath, { type: 'event', index: i }));
}
await Promise.all(writes);
const logs = await readActivityLog(activityPath);
expect(logs.length).toBe(50);
});
it('should maintain data integrity with concurrent writes', async () => {
const writes: Promise<void>[] = [];
for (let i = 0; i < 20; i++) {
writes.push(
logActivity(activityPath, {
type: 'concurrent-test',
id: i,
data: `data-${i}`
})
);
}
await Promise.all(writes);
const logs = await readActivityLog(activityPath);
// All events should be present
expect(logs.length).toBe(20);
// Validate ids set
const ids = new Set(logs.map((l) => l.id));
expect([...ids].sort((a, b) => a - b)).toEqual([...Array(20).keys()]);
// Validate shape
for (const log of logs) {
expect(log.type).toBe('concurrent-test');
expect(typeof log.id).toBe('number');
expect(log.data).toMatch(/^data-\d+$/);
}
});
});
describe('File integrity', () => {
it('should maintain valid JSONL after many operations', async () => {
for (let i = 0; i < 100; i++) {
await logActivity(activityPath, { type: 'test', iteration: i });
}
const content = await fs.readFile(activityPath, 'utf-8');
const lines = content.trim().split(/\r?\n/);
expect(lines.length).toBe(100);
// All lines should be valid JSON
for (const line of lines) {
expect(() => JSON.parse(line)).not.toThrow();
}
});
});
});
```
--------------------------------------------------------------------------------
/tests/unit/mcp/tools/analyze-complexity.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* Tests for the analyze_project_complexity MCP tool
*
* Note: This test does NOT test the actual implementation. It tests that:
* 1. The tool is registered correctly with the correct parameters
* 2. Arguments are passed correctly to analyzeTaskComplexityDirect
* 3. The threshold parameter is properly validated
* 4. Error handling works as expected
*
* We do NOT import the real implementation - everything is mocked
*/
import { jest } from '@jest/globals';
// Mock EVERYTHING
const mockAnalyzeTaskComplexityDirect = jest.fn();
jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
analyzeTaskComplexityDirect: mockAnalyzeTaskComplexityDirect
}));
const mockHandleApiResult = jest.fn((result) => result);
const mockGetProjectRootFromSession = jest.fn(() => '/mock/project/root');
const mockCreateErrorResponse = jest.fn((msg) => ({
success: false,
error: { code: 'ERROR', message: msg }
}));
jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
getProjectRootFromSession: mockGetProjectRootFromSession,
handleApiResult: mockHandleApiResult,
createErrorResponse: mockCreateErrorResponse,
createContentResponse: jest.fn((content) => ({
success: true,
data: content
})),
executeTaskMasterCommand: jest.fn()
}));
// This is a more complex mock of Zod to test actual validation
const createZodMock = () => {
// Storage for validation rules
const validationRules = {
threshold: {
type: 'coerce.number',
min: 1,
max: 10,
optional: true
}
};
// Create validator functions
const validateThreshold = (value) => {
if (value === undefined && validationRules.threshold.optional) {
return true;
}
// Attempt to coerce to number (if string)
const numValue = typeof value === 'string' ? Number(value) : value;
// Check if it's a valid number
if (isNaN(numValue)) {
throw new Error(`Invalid type for parameter 'threshold'`);
}
// Check min/max constraints
if (numValue < validationRules.threshold.min) {
throw new Error(
`Threshold must be at least ${validationRules.threshold.min}`
);
}
if (numValue > validationRules.threshold.max) {
throw new Error(
`Threshold must be at most ${validationRules.threshold.max}`
);
}
return true;
};
// Create actual validators for parameters
const validators = {
threshold: validateThreshold
};
// Main validation function for the entire object
const validateObject = (obj) => {
// Validate each field
if (obj.threshold !== undefined) {
validators.threshold(obj.threshold);
}
// If we get here, all validations passed
return obj;
};
// Base object with chainable methods
const zodBase = {
optional: () => {
return zodBase;
},
describe: (desc) => {
return zodBase;
}
};
// Number-specific methods
const zodNumber = {
...zodBase,
min: (value) => {
return zodNumber;
},
max: (value) => {
return zodNumber;
}
};
// Main mock implementation
const mockZod = {
object: () => ({
...zodBase,
// This parse method will be called by the tool execution
parse: validateObject
}),
string: () => zodBase,
boolean: () => zodBase,
number: () => zodNumber,
coerce: {
number: () => zodNumber
},
union: (schemas) => zodBase,
_def: {
shape: () => ({
output: {},
model: {},
threshold: {},
file: {},
research: {},
projectRoot: {}
})
}
};
return mockZod;
};
// Create our Zod mock
const mockZod = createZodMock();
jest.mock('zod', () => ({
z: mockZod
}));
// DO NOT import the real module - create a fake implementation
// This is the fake implementation of registerAnalyzeTool
const registerAnalyzeTool = (server) => {
// Create simplified version of the tool config
const toolConfig = {
name: 'analyze_project_complexity',
description:
'Analyze task complexity and generate expansion recommendations',
parameters: mockZod.object(),
// Create a simplified mock of the execute function
execute: (args, context) => {
const { log, session } = context;
try {
log.info &&
log.info(
`Analyzing task complexity with args: ${JSON.stringify(args)}`
);
// Get project root
const rootFolder = mockGetProjectRootFromSession(session, log);
// Call analyzeTaskComplexityDirect
const result = mockAnalyzeTaskComplexityDirect(
{
...args,
projectRoot: rootFolder
},
log,
{ session }
);
// Handle result
return mockHandleApiResult(result, log);
} catch (error) {
log.error && log.error(`Error in analyze tool: ${error.message}`);
return mockCreateErrorResponse(error.message);
}
}
};
// Register the tool with the server
server.addTool(toolConfig);
};
describe('MCP Tool: analyze_project_complexity', () => {
// Create mock server
let mockServer;
let executeFunction;
// Create mock logger
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
};
// Test data
const validArgs = {
output: 'output/path/report.json',
model: 'claude-3-opus-20240229',
threshold: 5,
research: true
};
// Standard responses
const successResponse = {
success: true,
data: {
message: 'Task complexity analysis complete',
reportPath: '/mock/project/root/output/path/report.json',
reportSummary: {
taskCount: 10,
highComplexityTasks: 3,
mediumComplexityTasks: 5,
lowComplexityTasks: 2
}
}
};
const errorResponse = {
success: false,
error: {
code: 'ANALYZE_ERROR',
message: 'Failed to analyze task complexity'
}
};
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Create mock server
mockServer = {
addTool: jest.fn((config) => {
executeFunction = config.execute;
})
};
// Setup default successful response
mockAnalyzeTaskComplexityDirect.mockReturnValue(successResponse);
// Register the tool
registerAnalyzeTool(mockServer);
});
test('should register the tool correctly', () => {
// Verify tool was registered
expect(mockServer.addTool).toHaveBeenCalledWith(
expect.objectContaining({
name: 'analyze_project_complexity',
description:
'Analyze task complexity and generate expansion recommendations',
parameters: expect.any(Object),
execute: expect.any(Function)
})
);
// Verify the tool config was passed
const toolConfig = mockServer.addTool.mock.calls[0][0];
expect(toolConfig).toHaveProperty('parameters');
expect(toolConfig).toHaveProperty('execute');
});
test('should execute the tool with valid threshold as number', () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Test with valid numeric threshold
const args = { ...validArgs, threshold: 7 };
executeFunction(args, mockContext);
// Verify analyzeTaskComplexityDirect was called with correct arguments
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
expect.objectContaining({
threshold: 7,
projectRoot: '/mock/project/root'
}),
mockLogger,
{ session: mockContext.session }
);
// Verify handleApiResult was called
expect(mockHandleApiResult).toHaveBeenCalledWith(
successResponse,
mockLogger
);
});
test('should execute the tool with valid threshold as string', () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Test with valid string threshold
const args = { ...validArgs, threshold: '7' };
executeFunction(args, mockContext);
// The mock doesn't actually coerce the string, just verify that the string is passed correctly
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
expect.objectContaining({
threshold: '7', // Expect string value, not coerced to number in our mock
projectRoot: '/mock/project/root'
}),
mockLogger,
{ session: mockContext.session }
);
});
test('should execute the tool with decimal threshold', () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Test with decimal threshold
const args = { ...validArgs, threshold: 6.5 };
executeFunction(args, mockContext);
// Verify it was passed correctly
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
expect.objectContaining({
threshold: 6.5,
projectRoot: '/mock/project/root'
}),
mockLogger,
{ session: mockContext.session }
);
});
test('should execute the tool without threshold parameter', () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Test without threshold (should use default)
const { threshold, ...argsWithoutThreshold } = validArgs;
executeFunction(argsWithoutThreshold, mockContext);
// Verify threshold is undefined
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
expect.objectContaining({
projectRoot: '/mock/project/root'
}),
mockLogger,
{ session: mockContext.session }
);
// Check threshold is not included
const callArgs = mockAnalyzeTaskComplexityDirect.mock.calls[0][0];
expect(callArgs).not.toHaveProperty('threshold');
});
test('should handle errors from analyzeTaskComplexityDirect', () => {
// Setup error response
mockAnalyzeTaskComplexityDirect.mockReturnValueOnce(errorResponse);
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
executeFunction(validArgs, mockContext);
// Verify analyzeTaskComplexityDirect was called
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalled();
// Verify handleApiResult was called with error response
expect(mockHandleApiResult).toHaveBeenCalledWith(errorResponse, mockLogger);
});
test('should handle unexpected errors', () => {
// Setup error
const testError = new Error('Unexpected error');
mockAnalyzeTaskComplexityDirect.mockImplementationOnce(() => {
throw testError;
});
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
executeFunction(validArgs, mockContext);
// Verify error was logged
expect(mockLogger.error).toHaveBeenCalledWith(
'Error in analyze tool: Unexpected error'
);
// Verify error response was created
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
});
test('should verify research parameter is correctly passed', () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Test with research=true
executeFunction(
{
...validArgs,
research: true
},
mockContext
);
// Verify analyzeTaskComplexityDirect was called with research=true
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
expect.objectContaining({
research: true
}),
expect.any(Object),
expect.any(Object)
);
// Reset mocks
jest.clearAllMocks();
// Test with research=false
executeFunction(
{
...validArgs,
research: false
},
mockContext
);
// Verify analyzeTaskComplexityDirect was called with research=false
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
expect.objectContaining({
research: false
}),
expect.any(Object),
expect.any(Object)
);
});
});
```
--------------------------------------------------------------------------------
/tests/unit/mcp/tools/tool-registration.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* tool-registration.test.js
* Comprehensive unit tests for the Task Master MCP tool registration system
* Tests environment variable control system covering all configuration modes and edge cases
*/
import {
describe,
it,
expect,
beforeEach,
afterEach,
jest
} from '@jest/globals';
import {
EXPECTED_TOOL_COUNTS,
EXPECTED_CORE_TOOLS,
validateToolCounts,
validateToolStructure
} from '../../../helpers/tool-counts.js';
import { registerTaskMasterTools } from '../../../../mcp-server/src/tools/index.js';
import {
toolRegistry,
coreTools,
standardTools
} from '../../../../mcp-server/src/tools/tool-registry.js';
// Derive constants from imported registry to avoid brittle magic numbers
const ALL_COUNT = Object.keys(toolRegistry).length;
const CORE_COUNT = coreTools.length;
const STANDARD_COUNT = standardTools.length;
describe('Task Master Tool Registration System', () => {
let mockServer;
let originalEnv;
beforeEach(() => {
originalEnv = process.env.TASK_MASTER_TOOLS;
mockServer = {
tools: [],
addTool: jest.fn((tool) => {
mockServer.tools.push(tool);
return tool;
})
};
delete process.env.TASK_MASTER_TOOLS;
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env.TASK_MASTER_TOOLS = originalEnv;
} else {
delete process.env.TASK_MASTER_TOOLS;
}
jest.clearAllMocks();
});
describe('Test Environment Setup', () => {
it('should have properly configured mock server', () => {
expect(mockServer).toBeDefined();
expect(typeof mockServer.addTool).toBe('function');
expect(Array.isArray(mockServer.tools)).toBe(true);
expect(mockServer.tools.length).toBe(0);
});
it('should have correct tool registry structure', () => {
const validation = validateToolCounts();
expect(validation.isValid).toBe(true);
if (!validation.isValid) {
console.error('Tool count validation failed:', validation);
}
expect(validation.actual.total).toBe(EXPECTED_TOOL_COUNTS.total);
expect(validation.actual.core).toBe(EXPECTED_TOOL_COUNTS.core);
expect(validation.actual.standard).toBe(EXPECTED_TOOL_COUNTS.standard);
});
it('should have correct core tools', () => {
const structure = validateToolStructure();
expect(structure.isValid).toBe(true);
if (!structure.isValid) {
console.error('Tool structure validation failed:', structure);
}
expect(coreTools).toEqual(expect.arrayContaining(EXPECTED_CORE_TOOLS));
expect(coreTools.length).toBe(EXPECTED_TOOL_COUNTS.core);
});
it('should have correct standard tools that include all core tools', () => {
const structure = validateToolStructure();
expect(structure.details.coreInStandard).toBe(true);
expect(standardTools.length).toBe(EXPECTED_TOOL_COUNTS.standard);
coreTools.forEach((tool) => {
expect(standardTools).toContain(tool);
});
});
it('should have all expected tools in registry', () => {
const expectedTools = [
'initialize_project',
'models',
'research',
'add_tag',
'delete_tag',
'get_tasks',
'next_task',
'get_task'
];
expectedTools.forEach((tool) => {
expect(toolRegistry).toHaveProperty(tool);
});
});
});
describe('Configuration Modes', () => {
it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS is not set (default behavior)`, () => {
delete process.env.TASK_MASTER_TOOLS;
registerTaskMasterTools(mockServer);
expect(mockServer.addTool).toHaveBeenCalledTimes(
EXPECTED_TOOL_COUNTS.total
);
});
it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS=all`, () => {
process.env.TASK_MASTER_TOOLS = 'all';
registerTaskMasterTools(mockServer);
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
});
it(`should register exactly ${CORE_COUNT} core tools when TASK_MASTER_TOOLS=core`, () => {
process.env.TASK_MASTER_TOOLS = 'core';
registerTaskMasterTools(mockServer, 'core');
expect(mockServer.addTool).toHaveBeenCalledTimes(
EXPECTED_TOOL_COUNTS.core
);
});
it(`should register exactly ${STANDARD_COUNT} standard tools when TASK_MASTER_TOOLS=standard`, () => {
process.env.TASK_MASTER_TOOLS = 'standard';
registerTaskMasterTools(mockServer, 'standard');
expect(mockServer.addTool).toHaveBeenCalledTimes(
EXPECTED_TOOL_COUNTS.standard
);
});
it(`should treat lean as alias for core mode (${CORE_COUNT} tools)`, () => {
process.env.TASK_MASTER_TOOLS = 'lean';
registerTaskMasterTools(mockServer, 'lean');
expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT);
});
it('should handle case insensitive configuration values', () => {
process.env.TASK_MASTER_TOOLS = 'CORE';
registerTaskMasterTools(mockServer, 'CORE');
expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT);
});
});
describe('Custom Tool Selection and Edge Cases', () => {
it('should register specific tools from comma-separated list', () => {
process.env.TASK_MASTER_TOOLS = 'get_tasks,next_task,get_task';
registerTaskMasterTools(mockServer, 'get_tasks,next_task,get_task');
expect(mockServer.addTool).toHaveBeenCalledTimes(3);
});
it('should handle mixed valid and invalid tool names gracefully', () => {
process.env.TASK_MASTER_TOOLS =
'invalid_tool,get_tasks,fake_tool,next_task';
registerTaskMasterTools(
mockServer,
'invalid_tool,get_tasks,fake_tool,next_task'
);
expect(mockServer.addTool).toHaveBeenCalledTimes(2);
});
it('should default to all tools with completely invalid input', () => {
process.env.TASK_MASTER_TOOLS = 'completely_invalid';
registerTaskMasterTools(mockServer);
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
});
it('should handle empty string environment variable', () => {
process.env.TASK_MASTER_TOOLS = '';
registerTaskMasterTools(mockServer);
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
});
it('should handle whitespace in comma-separated lists', () => {
process.env.TASK_MASTER_TOOLS = ' get_tasks , next_task , get_task ';
registerTaskMasterTools(mockServer, ' get_tasks , next_task , get_task ');
expect(mockServer.addTool).toHaveBeenCalledTimes(3);
});
it('should ignore duplicate tools in list', () => {
process.env.TASK_MASTER_TOOLS = 'get_tasks,get_tasks,next_task,get_tasks';
registerTaskMasterTools(
mockServer,
'get_tasks,get_tasks,next_task,get_tasks'
);
expect(mockServer.addTool).toHaveBeenCalledTimes(2);
});
it('should handle only commas and empty entries', () => {
process.env.TASK_MASTER_TOOLS = ',,,';
registerTaskMasterTools(mockServer);
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
});
it('should handle single tool selection', () => {
process.env.TASK_MASTER_TOOLS = 'get_tasks';
registerTaskMasterTools(mockServer, 'get_tasks');
expect(mockServer.addTool).toHaveBeenCalledTimes(1);
});
});
describe('Coverage Analysis and Integration Tests', () => {
it('should provide 100% code coverage for environment control logic', () => {
const testCases = [
{
env: undefined,
expectedCount: ALL_COUNT,
description: 'undefined env (all)'
},
{
env: '',
expectedCount: ALL_COUNT,
description: 'empty string (all)'
},
{ env: 'all', expectedCount: ALL_COUNT, description: 'all mode' },
{ env: 'core', expectedCount: CORE_COUNT, description: 'core mode' },
{
env: 'lean',
expectedCount: CORE_COUNT,
description: 'lean mode (alias)'
},
{
env: 'standard',
expectedCount: STANDARD_COUNT,
description: 'standard mode'
},
{
env: 'get_tasks,next_task',
expectedCount: 2,
description: 'custom list'
},
{
env: 'invalid_tool',
expectedCount: ALL_COUNT,
description: 'invalid fallback'
}
];
testCases.forEach((testCase) => {
delete process.env.TASK_MASTER_TOOLS;
if (testCase.env !== undefined) {
process.env.TASK_MASTER_TOOLS = testCase.env;
}
mockServer.tools = [];
mockServer.addTool.mockClear();
registerTaskMasterTools(mockServer, testCase.env || 'all');
expect(mockServer.addTool).toHaveBeenCalledTimes(
testCase.expectedCount
);
});
});
it('should have optimal performance characteristics', () => {
const startTime = Date.now();
process.env.TASK_MASTER_TOOLS = 'all';
registerTaskMasterTools(mockServer);
const endTime = Date.now();
const executionTime = endTime - startTime;
expect(executionTime).toBeLessThan(100);
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
});
it('should validate token reduction claims', () => {
expect(coreTools.length).toBeLessThan(standardTools.length);
expect(standardTools.length).toBeLessThan(
Object.keys(toolRegistry).length
);
expect(coreTools.length).toBe(CORE_COUNT);
expect(standardTools.length).toBe(STANDARD_COUNT);
expect(Object.keys(toolRegistry).length).toBe(ALL_COUNT);
const allToolsCount = Object.keys(toolRegistry).length;
const coreReduction =
((allToolsCount - coreTools.length) / allToolsCount) * 100;
const standardReduction =
((allToolsCount - standardTools.length) / allToolsCount) * 100;
expect(coreReduction).toBeGreaterThan(80);
expect(standardReduction).toBeGreaterThan(50);
});
it('should maintain referential integrity of tool registry', () => {
coreTools.forEach((tool) => {
expect(standardTools).toContain(tool);
});
standardTools.forEach((tool) => {
expect(toolRegistry).toHaveProperty(tool);
});
Object.keys(toolRegistry).forEach((tool) => {
expect(typeof toolRegistry[tool]).toBe('function');
});
});
it('should handle concurrent registration attempts', () => {
process.env.TASK_MASTER_TOOLS = 'core';
registerTaskMasterTools(mockServer, 'core');
registerTaskMasterTools(mockServer, 'core');
registerTaskMasterTools(mockServer, 'core');
expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT * 3);
});
it('should validate all documented tool categories exist', () => {
const allTools = Object.keys(toolRegistry);
const projectSetupTools = allTools.filter((tool) =>
['initialize_project', 'models', 'rules', 'parse_prd'].includes(tool)
);
expect(projectSetupTools.length).toBeGreaterThan(0);
const taskManagementTools = allTools.filter((tool) =>
['get_tasks', 'get_task', 'next_task', 'set_task_status'].includes(tool)
);
expect(taskManagementTools.length).toBeGreaterThan(0);
const analysisTools = allTools.filter((tool) =>
['analyze_project_complexity', 'complexity_report'].includes(tool)
);
expect(analysisTools.length).toBeGreaterThan(0);
const tagManagementTools = allTools.filter((tool) =>
['add_tag', 'delete_tag', 'list_tags', 'use_tag'].includes(tool)
);
expect(tagManagementTools.length).toBeGreaterThan(0);
});
it('should handle error conditions gracefully', () => {
const problematicInputs = [
'null',
'undefined',
' ',
'\n\t',
'special!@#$%^&*()characters',
'very,very,very,very,very,very,very,long,comma,separated,list,with,invalid,tools,that,should,fallback,to,all'
];
problematicInputs.forEach((input) => {
mockServer.tools = [];
mockServer.addTool.mockClear();
process.env.TASK_MASTER_TOOLS = input;
expect(() => registerTaskMasterTools(mockServer)).not.toThrow();
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
});
});
});
});
```
--------------------------------------------------------------------------------
/apps/extension/src/utils/task-master-api/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* TaskMaster API
* Main API class that coordinates all modules
*/
import * as vscode from 'vscode';
import { ExtensionLogger } from '../logger';
import type { MCPClientManager } from '../mcpClient';
import { CacheManager } from './cache/cache-manager';
import { MCPClient } from './mcp-client';
import { TaskTransformer } from './transformers/task-transformer';
import type {
AddSubtaskOptions,
CacheConfig,
GetTasksOptions,
SubtaskData,
TaskMasterApiConfig,
TaskMasterApiResponse,
TaskMasterTask,
TaskUpdate,
UpdateSubtaskOptions,
UpdateTaskOptions,
UpdateTaskStatusOptions
} from './types';
// Re-export types for backward compatibility
export * from './types';
export class TaskMasterApi {
private mcpWrapper: MCPClient;
private cache: CacheManager;
private transformer: TaskTransformer;
private config: TaskMasterApiConfig;
private logger: ExtensionLogger;
private readonly defaultCacheConfig: CacheConfig = {
maxSize: 100,
enableBackgroundRefresh: true,
refreshInterval: 5 * 60 * 1000, // 5 minutes
enableAnalytics: true,
enablePrefetch: true,
compressionEnabled: false,
persistToDisk: false
};
private readonly defaultConfig: TaskMasterApiConfig = {
timeout: 30000,
retryAttempts: 3,
cacheDuration: 5 * 60 * 1000, // 5 minutes
cache: this.defaultCacheConfig
};
constructor(
mcpClient: MCPClientManager,
config?: Partial<TaskMasterApiConfig>
) {
this.logger = ExtensionLogger.getInstance();
// Merge config - ensure cache is always fully defined
const mergedCache: CacheConfig = {
maxSize: config?.cache?.maxSize ?? this.defaultCacheConfig.maxSize,
enableBackgroundRefresh:
config?.cache?.enableBackgroundRefresh ??
this.defaultCacheConfig.enableBackgroundRefresh,
refreshInterval:
config?.cache?.refreshInterval ??
this.defaultCacheConfig.refreshInterval,
enableAnalytics:
config?.cache?.enableAnalytics ??
this.defaultCacheConfig.enableAnalytics,
enablePrefetch:
config?.cache?.enablePrefetch ?? this.defaultCacheConfig.enablePrefetch,
compressionEnabled:
config?.cache?.compressionEnabled ??
this.defaultCacheConfig.compressionEnabled,
persistToDisk:
config?.cache?.persistToDisk ?? this.defaultCacheConfig.persistToDisk
};
this.config = {
...this.defaultConfig,
...config,
cache: mergedCache
};
// Initialize modules
this.mcpWrapper = new MCPClient(mcpClient, this.logger, {
timeout: this.config.timeout,
retryAttempts: this.config.retryAttempts
});
this.cache = new CacheManager(
{ ...mergedCache, cacheDuration: this.config.cacheDuration },
this.logger
);
this.transformer = new TaskTransformer(this.logger);
// Start background refresh if enabled
if (this.config.cache?.enableBackgroundRefresh) {
this.startBackgroundRefresh();
}
this.logger.log('TaskMasterApi: Initialized with modular architecture');
}
/**
* Get tasks from TaskMaster
*/
async getTasks(
options?: GetTasksOptions
): Promise<TaskMasterApiResponse<TaskMasterTask[]>> {
const startTime = Date.now();
const cacheKey = `get_tasks_${JSON.stringify(options || {})}`;
try {
// Check cache first
const cached = this.cache.get(cacheKey);
if (cached) {
return {
success: true,
data: cached,
requestDuration: Date.now() - startTime
};
}
// Prepare MCP tool arguments
const mcpArgs: Record<string, unknown> = {
projectRoot: options?.projectRoot || this.getWorkspaceRoot(),
withSubtasks: options?.withSubtasks ?? true
};
if (options?.status) {
mcpArgs.status = options.status;
}
if (options?.tag) {
mcpArgs.tag = options.tag;
}
this.logger.log('Calling get_tasks with args:', mcpArgs);
// Call MCP tool
const mcpResponse = await this.mcpWrapper.callTool('get_tasks', mcpArgs);
// Transform response
const transformedTasks =
this.transformer.transformMCPTasksResponse(mcpResponse);
// Cache the result
this.cache.set(cacheKey, transformedTasks);
return {
success: true,
data: transformedTasks,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error getting tasks:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Update task status
*/
async updateTaskStatus(
taskId: string,
status: string,
options?: UpdateTaskStatusOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const mcpArgs: Record<string, unknown> = {
id: String(taskId),
status: status,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
this.logger.log('Calling set_task_status with args:', mcpArgs);
await this.mcpWrapper.callTool('set_task_status', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error updating task status:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Update task content
*/
async updateTask(
taskId: string,
updates: TaskUpdate,
options?: UpdateTaskOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
// Build update prompt
const updateFields: string[] = [];
if (updates.title !== undefined) {
updateFields.push(`Title: ${updates.title}`);
}
if (updates.description !== undefined) {
updateFields.push(`Description: ${updates.description}`);
}
if (updates.details !== undefined) {
updateFields.push(`Details: ${updates.details}`);
}
if (updates.priority !== undefined) {
updateFields.push(`Priority: ${updates.priority}`);
}
if (updates.testStrategy !== undefined) {
updateFields.push(`Test Strategy: ${updates.testStrategy}`);
}
if (updates.dependencies !== undefined) {
updateFields.push(`Dependencies: ${updates.dependencies.join(', ')}`);
}
const prompt = `Update task with the following changes:\n${updateFields.join('\n')}`;
const mcpArgs: Record<string, unknown> = {
id: String(taskId),
prompt: prompt,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
if (options?.append !== undefined) {
mcpArgs.append = options.append;
}
if (options?.research !== undefined) {
mcpArgs.research = options.research;
}
this.logger.log('Calling update_task with args:', mcpArgs);
await this.mcpWrapper.callTool('update_task', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error updating task:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Update subtask content
*/
async updateSubtask(
taskId: string,
prompt: string,
options?: UpdateSubtaskOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const mcpArgs: Record<string, unknown> = {
id: String(taskId),
prompt: prompt,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
if (options?.research !== undefined) {
mcpArgs.research = options.research;
}
this.logger.log('Calling update_subtask with args:', mcpArgs);
await this.mcpWrapper.callTool('update_subtask', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error updating subtask:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Add a new subtask
*/
async addSubtask(
parentTaskId: string,
subtaskData: SubtaskData,
options?: AddSubtaskOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const mcpArgs: Record<string, unknown> = {
id: String(parentTaskId),
title: subtaskData.title,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
if (subtaskData.description) {
mcpArgs.description = subtaskData.description;
}
if (subtaskData.dependencies && subtaskData.dependencies.length > 0) {
mcpArgs.dependencies = subtaskData.dependencies.join(',');
}
if (subtaskData.status) {
mcpArgs.status = subtaskData.status;
}
this.logger.log('Calling add_subtask with args:', mcpArgs);
await this.mcpWrapper.callTool('add_subtask', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error adding subtask:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Get connection status
*/
getConnectionStatus(): { isConnected: boolean; error?: string } {
const status = this.mcpWrapper.getStatus();
return {
isConnected: status.isRunning,
error: status.error
};
}
/**
* Test connection
*/
async testConnection(): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const isConnected = await this.mcpWrapper.testConnection();
return {
success: true,
data: isConnected,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Connection test failed:', error);
return {
success: false,
error:
error instanceof Error ? error.message : 'Connection test failed',
requestDuration: Date.now() - startTime
};
}
}
/**
* Clear all cached data
*/
clearCache(): void {
this.cache.clear();
}
/**
* Get cache analytics
*/
getCacheAnalytics() {
return this.cache.getAnalytics();
}
/**
* Cleanup resources
*/
destroy(): void {
this.cache.destroy();
this.logger.log('TaskMasterApi: Destroyed and cleaned up resources');
}
/**
* Start background refresh
*/
private startBackgroundRefresh(): void {
const interval = this.config.cache?.refreshInterval || 5 * 60 * 1000;
setInterval(() => {
this.performBackgroundRefresh();
}, interval);
}
/**
* Perform background refresh of frequently accessed cache entries
*/
private async performBackgroundRefresh(): Promise<void> {
if (!this.config.cache?.enableBackgroundRefresh) {
return;
}
this.logger.log('Starting background cache refresh');
const candidates = this.cache.getRefreshCandidates();
let refreshedCount = 0;
for (const [key, entry] of candidates) {
try {
const optionsMatch = key.match(/get_tasks_(.+)/);
if (optionsMatch) {
const options = JSON.parse(optionsMatch[1]);
await this.getTasks(options);
refreshedCount++;
this.cache.incrementRefreshes();
}
} catch (error) {
this.logger.warn(`Background refresh failed for key ${key}:`, error);
}
}
this.logger.log(
`Background refresh completed, refreshed ${refreshedCount} entries`
);
}
/**
* Get workspace root path
*/
private getWorkspaceRoot(): string {
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
}
}
```
--------------------------------------------------------------------------------
/tests/unit/manage-gitignore.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* Unit tests for manage-gitignore.js module
* Tests the logic with Jest spies instead of mocked modules
*/
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Import the module under test and its exports
import manageGitignoreFile, {
normalizeLine,
isTaskLine,
buildTaskFilesSection,
TASK_FILES_COMMENT,
TASK_JSON_PATTERN,
TASK_DIR_PATTERN
} from '../../src/utils/manage-gitignore.js';
describe('manage-gitignore.js Unit Tests', () => {
let tempDir;
beforeEach(() => {
jest.clearAllMocks();
// Create a temporary directory for testing
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'manage-gitignore-test-'));
});
afterEach(() => {
// Clean up the temporary directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (err) {
// Ignore cleanup errors
}
});
describe('Constants', () => {
test('should have correct constant values', () => {
expect(TASK_FILES_COMMENT).toBe('# Task files');
expect(TASK_JSON_PATTERN).toBe('tasks.json');
expect(TASK_DIR_PATTERN).toBe('tasks/');
});
});
describe('normalizeLine function', () => {
test('should remove leading/trailing whitespace', () => {
expect(normalizeLine(' test ')).toBe('test');
});
test('should remove comment hash and trim', () => {
expect(normalizeLine('# tasks.json')).toBe('tasks.json');
expect(normalizeLine('#tasks/')).toBe('tasks/');
});
test('should handle empty strings', () => {
expect(normalizeLine('')).toBe('');
expect(normalizeLine(' ')).toBe('');
});
test('should handle lines without comments', () => {
expect(normalizeLine('tasks.json')).toBe('tasks.json');
});
});
describe('isTaskLine function', () => {
test('should identify task.json patterns', () => {
expect(isTaskLine('tasks.json')).toBe(true);
expect(isTaskLine('# tasks.json')).toBe(true);
expect(isTaskLine(' # tasks.json ')).toBe(true);
});
test('should identify tasks/ patterns', () => {
expect(isTaskLine('tasks/')).toBe(true);
expect(isTaskLine('# tasks/')).toBe(true);
expect(isTaskLine(' # tasks/ ')).toBe(true);
});
test('should reject non-task patterns', () => {
expect(isTaskLine('node_modules/')).toBe(false);
expect(isTaskLine('# Some comment')).toBe(false);
expect(isTaskLine('')).toBe(false);
expect(isTaskLine('tasks.txt')).toBe(false);
});
});
describe('buildTaskFilesSection function', () => {
test('should build commented section when storeTasksInGit is true (tasks stored in git)', () => {
const result = buildTaskFilesSection(true);
expect(result).toEqual(['# Task files', '# tasks.json', '# tasks/ ']);
});
test('should build uncommented section when storeTasksInGit is false (tasks ignored)', () => {
const result = buildTaskFilesSection(false);
expect(result).toEqual(['# Task files', 'tasks.json', 'tasks/ ']);
});
});
describe('manageGitignoreFile function - Input Validation', () => {
test('should throw error for invalid targetPath', () => {
expect(() => {
manageGitignoreFile('', 'content', false);
}).toThrow('targetPath must be a non-empty string');
expect(() => {
manageGitignoreFile(null, 'content', false);
}).toThrow('targetPath must be a non-empty string');
expect(() => {
manageGitignoreFile('invalid.txt', 'content', false);
}).toThrow('targetPath must end with .gitignore');
});
test('should throw error for invalid content', () => {
expect(() => {
manageGitignoreFile('.gitignore', '', false);
}).toThrow('content must be a non-empty string');
expect(() => {
manageGitignoreFile('.gitignore', null, false);
}).toThrow('content must be a non-empty string');
});
test('should throw error for invalid storeTasksInGit', () => {
expect(() => {
manageGitignoreFile('.gitignore', 'content', 'not-boolean');
}).toThrow('storeTasksInGit must be a boolean');
});
});
describe('manageGitignoreFile function - File Operations with Spies', () => {
let writeFileSyncSpy;
let readFileSyncSpy;
let existsSyncSpy;
let mockLog;
beforeEach(() => {
// Set up spies
writeFileSyncSpy = jest
.spyOn(fs, 'writeFileSync')
.mockImplementation(() => {});
readFileSyncSpy = jest
.spyOn(fs, 'readFileSync')
.mockImplementation(() => '');
existsSyncSpy = jest
.spyOn(fs, 'existsSync')
.mockImplementation(() => false);
mockLog = jest.fn();
});
afterEach(() => {
// Restore original implementations
writeFileSyncSpy.mockRestore();
readFileSyncSpy.mockRestore();
existsSyncSpy.mockRestore();
});
describe('New File Creation', () => {
const templateContent = `# Logs
logs
*.log
# Task files
tasks.json
tasks/ `;
test('should create new file with commented task lines when storeTasksInGit is true', () => {
existsSyncSpy.mockReturnValue(false); // File doesn't exist
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
`# Logs
logs
*.log
# Task files
# tasks.json
# tasks/
`
);
expect(mockLog).toHaveBeenCalledWith(
'success',
'Created .gitignore with full template'
);
});
test('should create new file with uncommented task lines when storeTasksInGit is false', () => {
existsSyncSpy.mockReturnValue(false); // File doesn't exist
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
`# Logs
logs
*.log
# Task files
tasks.json
tasks/
`
);
expect(mockLog).toHaveBeenCalledWith(
'success',
'Created .gitignore with full template'
);
});
test('should handle write errors gracefully', () => {
existsSyncSpy.mockReturnValue(false);
const writeError = new Error('Permission denied');
writeFileSyncSpy.mockImplementation(() => {
throw writeError;
});
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
}).toThrow('Permission denied');
expect(mockLog).toHaveBeenCalledWith(
'error',
'Failed to create .gitignore: Permission denied'
);
});
});
describe('File Merging', () => {
const templateContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Task files
tasks.json
tasks/ `;
test('should merge with existing file and add new content', () => {
const existingContent = `# Old content
old-file.txt
# Task files
# tasks.json
# tasks/`;
existsSyncSpy.mockReturnValue(true); // File exists
readFileSyncSpy.mockReturnValue(existingContent);
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Old content')
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Logs')
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Dependencies')
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Task files')
);
});
test('should remove existing task section and replace with new preferences', () => {
const existingContent = `# Existing
existing.txt
# Task files
tasks.json
tasks/
# More content
more.txt`;
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue(existingContent);
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
// Should contain existing non-task content
expect(writtenContent).toContain('# Existing');
expect(writtenContent).toContain('existing.txt');
expect(writtenContent).toContain('# More content');
expect(writtenContent).toContain('more.txt');
// Should contain new template content
expect(writtenContent).toContain('# Logs');
expect(writtenContent).toContain('# Dependencies');
// Should have uncommented task lines (storeTasksInGit = false means ignore tasks)
expect(writtenContent).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
test('should handle different task preferences correctly', () => {
const existingContent = `# Existing
existing.txt
# Task files
# tasks.json
# tasks/`;
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue(existingContent);
// Test with storeTasksInGit = true (commented)
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
expect(writtenContent).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
});
test('should not duplicate existing template content', () => {
const existingContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Task files
# tasks.json
# tasks/`;
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue(existingContent);
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
// Should not duplicate the logs section
const logsCount = (writtenContent.match(/# Logs/g) || []).length;
expect(logsCount).toBe(1);
// Should not duplicate dependencies
const depsCount = (writtenContent.match(/# Dependencies/g) || [])
.length;
expect(depsCount).toBe(1);
});
test('should handle read errors gracefully', () => {
existsSyncSpy.mockReturnValue(true);
const readError = new Error('File not readable');
readFileSyncSpy.mockImplementation(() => {
throw readError;
});
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
}).toThrow('File not readable');
expect(mockLog).toHaveBeenCalledWith(
'error',
'Failed to merge content with .gitignore: File not readable'
);
});
test('should handle write errors during merge gracefully', () => {
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue('existing content');
const writeError = new Error('Disk full');
writeFileSyncSpy.mockImplementation(() => {
throw writeError;
});
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
}).toThrow('Disk full');
expect(mockLog).toHaveBeenCalledWith(
'error',
'Failed to merge content with .gitignore: Disk full'
);
});
});
describe('Edge Cases', () => {
test('should work without log function', () => {
existsSyncSpy.mockReturnValue(false);
const templateContent = `# Test
test.txt
# Task files
tasks.json
tasks/`;
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false);
}).not.toThrow();
expect(writeFileSyncSpy).toHaveBeenCalled();
});
test('should handle empty existing file', () => {
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue('');
const templateContent = `# Task files
tasks.json
tasks/`;
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalled();
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
expect(writtenContent).toContain('# Task files');
});
test('should handle template with only task files', () => {
existsSyncSpy.mockReturnValue(false);
const templateContent = `# Task files
tasks.json
tasks/ `;
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
expect(writtenContent).toBe(`# Task files
# tasks.json
# tasks/
`);
});
});
});
});
```