feat: add comma-separated status filtering to list-tasks
- supports multiple statuses like 'blocked,deferred' with comprehensive test coverage and backward compatibility - also adjusts biome.json to stop bitching about templating.
This commit is contained in:
7
.changeset/tiny-ads-decide.md
Normal file
7
.changeset/tiny-ads-decide.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Adds support for filtering tasks by multiple statuses at once using comma-separated statuses.
|
||||
|
||||
Example: `cancelled,deferred`
|
||||
@@ -28,7 +28,9 @@ export function registerListTasksTool(server) {
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter tasks by status (e.g., 'pending', 'done')"),
|
||||
.describe(
|
||||
"Filter tasks by status (e.g., 'pending', 'done') or multiple statuses separated by commas (e.g., 'blocked,deferred')"
|
||||
),
|
||||
withSubtasks: z
|
||||
.boolean()
|
||||
.optional()
|
||||
|
||||
465
package-lock.json
generated
465
package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"ai": "^4.3.10",
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.4.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^11.1.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -32,6 +33,7 @@
|
||||
"fastmcp": "^2.2.2",
|
||||
"figlet": "^1.8.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"gpt-tokens": "^1.3.14",
|
||||
"gradient-string": "^3.0.0",
|
||||
"helmet": "^8.1.0",
|
||||
"inquirer": "^12.5.0",
|
||||
@@ -4983,6 +4985,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
@@ -5558,6 +5566,139 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz",
|
||||
"integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"highlight.js": "^10.7.1",
|
||||
"mz": "^2.4.0",
|
||||
"parse5": "^5.1.1",
|
||||
"parse5-htmlparser2-tree-adapter": "^6.0.0",
|
||||
"yargs": "^16.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"highlight": "bin/highlight"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0",
|
||||
"npm": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-highlight/node_modules/yargs-parser": {
|
||||
"version": "20.2.9",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-spinners": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||
@@ -6004,6 +6145,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
|
||||
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
|
||||
@@ -7363,6 +7510,17 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gpt-tokens": {
|
||||
"version": "1.3.14",
|
||||
"resolved": "https://registry.npmjs.org/gpt-tokens/-/gpt-tokens-1.3.14.tgz",
|
||||
"integrity": "sha512-cFNErQQYGWRwYmew0wVqhCBZxTvGNr96/9pMwNXqSNu9afxqB5PNHOKHlWtUC/P4UW6Ne2UQHHaO2PaWWLpqWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decimal.js": "^10.4.3",
|
||||
"js-tiktoken": "^1.0.15",
|
||||
"openai-chat-tokens": "^0.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -7421,7 +7579,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -7485,6 +7642,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -9041,6 +9207,15 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tiktoken": {
|
||||
"version": "1.0.20",
|
||||
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.20.tgz",
|
||||
"integrity": "sha512-Xlaqhhs8VfCd6Sh7a1cFkZHQbYTLCwVJJWiHVxBYzLPxW0XsoxBy1hitmjkdIjD3Aon5BXLHFwU5O8WUx6HH+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -9571,6 +9746,17 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0",
|
||||
"object-assign": "^4.0.1",
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -9786,6 +9972,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/openai-chat-tokens": {
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/openai-chat-tokens/-/openai-chat-tokens-0.2.8.tgz",
|
||||
"integrity": "sha512-nW7QdFDIZlAYe6jsCT/VPJ/Lam3/w2DX9oxf/5wHpebBT49KI3TN43PPhYlq1klq2ajzXWKNOLY6U4FNZM7AoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tiktoken": "^1.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -9964,6 +10159,27 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
|
||||
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse5": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -11094,7 +11310,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
@@ -11129,6 +11344,231 @@
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/task-master-ai/-/task-master-ai-0.15.0.tgz",
|
||||
"integrity": "sha512-eOekJUdFFuJBt0Q4BMD0qO18UuPLh9qloNDs5YG4JK8YsxP6yRUnvxXL7wShOJZ2rNKE9Q0jnKPbvcGafupd8w==",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.10",
|
||||
"@ai-sdk/azure": "^1.3.17",
|
||||
"@ai-sdk/google": "^1.2.13",
|
||||
"@ai-sdk/mistral": "^1.2.7",
|
||||
"@ai-sdk/openai": "^1.3.20",
|
||||
"@ai-sdk/perplexity": "^1.1.7",
|
||||
"@ai-sdk/xai": "^1.2.15",
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@openrouter/ai-sdk-provider": "^0.4.5",
|
||||
"ai": "^4.3.10",
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.4.1",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^11.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.21.2",
|
||||
"fastmcp": "^1.20.5",
|
||||
"figlet": "^1.8.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"gradient-string": "^3.0.0",
|
||||
"helmet": "^8.1.0",
|
||||
"inquirer": "^12.5.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lru-cache": "^10.2.0",
|
||||
"ollama-ai-provider": "^1.2.0",
|
||||
"openai": "^4.89.0",
|
||||
"ora": "^8.2.0",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"bin": {
|
||||
"task-master": "bin/task-master.js",
|
||||
"task-master-ai": "mcp-server/server.js",
|
||||
"task-master-mcp": "mcp-server/server.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/eventsource": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.0.0.tgz",
|
||||
"integrity": "sha512-fvIkb9qZzdMxgZrEQDyll+9oJsyaVvY92I2Re+qK0qEJ+w5s0X3dtz+M0VAPOjP1gtU3iqWyjQ0G3nvd5CLZ2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/execa": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz",
|
||||
"integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/merge-streams": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"figures": "^6.1.0",
|
||||
"get-stream": "^9.0.0",
|
||||
"human-signals": "^8.0.1",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
"is-stream": "^4.0.1",
|
||||
"npm-run-path": "^6.0.0",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^4.0.0",
|
||||
"yoctocolors": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/fastmcp": {
|
||||
"version": "1.27.7",
|
||||
"resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-1.27.7.tgz",
|
||||
"integrity": "sha512-ozMJl5mTIWd2WMR6/hxBNEeSnFLUS9+PzzfLxQmejHOVg3A6IfRdCPVLGeNnXBhGjsK80JB2GPd4HDqd/szqLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"execa": "^9.5.2",
|
||||
"file-type": "^20.4.1",
|
||||
"fuse.js": "^7.1.0",
|
||||
"mcp-proxy": "^2.13.1",
|
||||
"strict-event-emitter-types": "^2.0.0",
|
||||
"undici": "^7.8.0",
|
||||
"uri-templates": "^0.2.0",
|
||||
"xsschema": "0.2.0-beta.3",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.24.3",
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
},
|
||||
"bin": {
|
||||
"fastmcp": "dist/bin/fastmcp.js"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/get-stream": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sec-ant/readable-stream": "^0.4.1",
|
||||
"is-stream": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/human-signals": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
|
||||
"integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/is-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/mcp-proxy": {
|
||||
"version": "2.14.3",
|
||||
"resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-2.14.3.tgz",
|
||||
"integrity": "sha512-2jmpBclH72z5ViXhdVWopKDLAyhnFGyIhR7MrjE3BLzgYv3pZwVMHKLOogu1aIWZN/J4OrlZp0jraHKjEPD1RQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.11.4",
|
||||
"eventsource": "^4.0.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"mcp-proxy": "dist/bin/mcp-proxy.js"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/npm-run-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
|
||||
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0",
|
||||
"unicorn-magic": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/strip-final-newline": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
|
||||
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai/node_modules/xsschema": {
|
||||
"version": "0.2.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/xsschema/-/xsschema-0.2.0-beta.3.tgz",
|
||||
"integrity": "sha512-ViOZ1a1kAPHvFjDJp4ITeutdlbnEs56lx/NotlvcitwN04eHnU3DhR+P5GvXT7A+69qKrXAx0YrccvmfwGnUGw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@valibot/to-json-schema": "^1.0.0",
|
||||
"arktype": "^2.1.16",
|
||||
"effect": "^3.14.5",
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@valibot/to-json-schema": {
|
||||
"optional": true
|
||||
},
|
||||
"arktype": {
|
||||
"optional": true
|
||||
},
|
||||
"effect": {
|
||||
"optional": true
|
||||
},
|
||||
"zod-to-json-schema": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/term-size": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
|
||||
@@ -11157,6 +11597,27 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/thenify-all": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/throttleit": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
/**
|
||||
* List all tasks
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string} statusFilter - Filter by status
|
||||
* @param {string} statusFilter - Filter by status (single status or comma-separated list, e.g., 'pending' or 'blocked,deferred')
|
||||
* @param {string} reportPath - Path to the complexity report
|
||||
* @param {boolean} withSubtasks - Whether to show subtasks
|
||||
* @param {string} outputFormat - Output format (text or json)
|
||||
@@ -48,15 +48,23 @@ function listTasks(
|
||||
data.tasks.forEach((task) => addComplexityToTask(task, complexityReport));
|
||||
}
|
||||
|
||||
// Filter tasks by status if specified
|
||||
const filteredTasks =
|
||||
statusFilter && statusFilter.toLowerCase() !== 'all' // <-- Added check for 'all'
|
||||
? data.tasks.filter(
|
||||
// Filter tasks by status if specified - now supports comma-separated statuses
|
||||
let filteredTasks;
|
||||
if (statusFilter && statusFilter.toLowerCase() !== 'all') {
|
||||
// Handle comma-separated statuses
|
||||
const allowedStatuses = statusFilter
|
||||
.split(',')
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter((s) => s.length > 0); // Remove empty strings
|
||||
|
||||
filteredTasks = data.tasks.filter(
|
||||
(task) =>
|
||||
task.status &&
|
||||
task.status.toLowerCase() === statusFilter.toLowerCase()
|
||||
)
|
||||
: data.tasks; // Default to all tasks if no filter or filter is 'all'
|
||||
task.status && allowedStatuses.includes(task.status.toLowerCase())
|
||||
);
|
||||
} else {
|
||||
// Default to all tasks if no filter or filter is 'all'
|
||||
filteredTasks = data.tasks;
|
||||
}
|
||||
|
||||
// Calculate completion statistics
|
||||
const totalTasks = data.tasks.length;
|
||||
@@ -83,6 +91,9 @@ function listTasks(
|
||||
const cancelledCount = data.tasks.filter(
|
||||
(task) => task.status === 'cancelled'
|
||||
).length;
|
||||
const reviewCount = data.tasks.filter(
|
||||
(task) => task.status === 'review'
|
||||
).length;
|
||||
|
||||
// Count subtasks and their statuses
|
||||
let totalSubtasks = 0;
|
||||
@@ -92,6 +103,7 @@ function listTasks(
|
||||
let blockedSubtasks = 0;
|
||||
let deferredSubtasks = 0;
|
||||
let cancelledSubtasks = 0;
|
||||
let reviewSubtasks = 0;
|
||||
|
||||
data.tasks.forEach((task) => {
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
@@ -114,6 +126,9 @@ function listTasks(
|
||||
cancelledSubtasks += task.subtasks.filter(
|
||||
(st) => st.status === 'cancelled'
|
||||
).length;
|
||||
reviewSubtasks += task.subtasks.filter(
|
||||
(st) => st.status === 'review'
|
||||
).length;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,6 +237,7 @@ function listTasks(
|
||||
blocked: blockedCount,
|
||||
deferred: deferredCount,
|
||||
cancelled: cancelledCount,
|
||||
review: reviewCount,
|
||||
completionPercentage,
|
||||
subtasks: {
|
||||
total: totalSubtasks,
|
||||
@@ -257,6 +273,7 @@ function listTasks(
|
||||
blockedSubtasks,
|
||||
deferredSubtasks,
|
||||
cancelledSubtasks,
|
||||
reviewSubtasks,
|
||||
tasksWithNoDeps,
|
||||
tasksReadyToWork,
|
||||
tasksWithUnsatisfiedDeps,
|
||||
@@ -278,7 +295,8 @@ function listTasks(
|
||||
pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0,
|
||||
blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0,
|
||||
deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0,
|
||||
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0
|
||||
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0,
|
||||
review: totalTasks > 0 ? (reviewCount / totalTasks) * 100 : 0
|
||||
};
|
||||
|
||||
const subtaskStatusBreakdown = {
|
||||
@@ -289,7 +307,8 @@ function listTasks(
|
||||
deferred:
|
||||
totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0,
|
||||
cancelled:
|
||||
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0
|
||||
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0,
|
||||
review: totalSubtasks > 0 ? (reviewSubtasks / totalSubtasks) * 100 : 0
|
||||
};
|
||||
|
||||
// Create progress bars with status breakdowns
|
||||
|
||||
459
tests/unit/mcp/tools/get-tasks.test.js
Normal file
459
tests/unit/mcp/tools/get-tasks.test.js
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* Tests for the get-tasks MCP tool
|
||||
*
|
||||
* This test verifies the MCP tool properly handles comma-separated status filtering
|
||||
* and passes arguments correctly to the underlying direct function.
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import {
|
||||
sampleTasks,
|
||||
emptySampleTasks
|
||||
} from '../../../fixtures/sample-tasks.js';
|
||||
|
||||
// Mock EVERYTHING
|
||||
const mockListTasksDirect = jest.fn();
|
||||
jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
|
||||
listTasksDirect: mockListTasksDirect
|
||||
}));
|
||||
|
||||
const mockHandleApiResult = jest.fn((result) => result);
|
||||
const mockWithNormalizedProjectRoot = jest.fn((executeFn) => executeFn);
|
||||
const mockCreateErrorResponse = jest.fn((msg) => ({
|
||||
success: false,
|
||||
error: { code: 'ERROR', message: msg }
|
||||
}));
|
||||
|
||||
const mockResolveTasksPath = jest.fn(() => '/mock/project/tasks.json');
|
||||
const mockResolveComplexityReportPath = jest.fn(
|
||||
() => '/mock/project/complexity-report.json'
|
||||
);
|
||||
|
||||
jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
|
||||
withNormalizedProjectRoot: mockWithNormalizedProjectRoot,
|
||||
handleApiResult: mockHandleApiResult,
|
||||
createErrorResponse: mockCreateErrorResponse,
|
||||
createContentResponse: jest.fn((content) => ({
|
||||
success: true,
|
||||
data: content
|
||||
}))
|
||||
}));
|
||||
|
||||
jest.mock('../../../../mcp-server/src/core/utils/path-utils.js', () => ({
|
||||
resolveTasksPath: mockResolveTasksPath,
|
||||
resolveComplexityReportPath: mockResolveComplexityReportPath
|
||||
}));
|
||||
|
||||
// Mock the z object from zod
|
||||
const mockZod = {
|
||||
object: jest.fn(() => mockZod),
|
||||
string: jest.fn(() => mockZod),
|
||||
boolean: jest.fn(() => mockZod),
|
||||
optional: jest.fn(() => mockZod),
|
||||
describe: jest.fn(() => mockZod),
|
||||
_def: {
|
||||
shape: () => ({
|
||||
status: {},
|
||||
withSubtasks: {},
|
||||
file: {},
|
||||
complexityReport: {},
|
||||
projectRoot: {}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
jest.mock('zod', () => ({
|
||||
z: mockZod
|
||||
}));
|
||||
|
||||
// DO NOT import the real module - create a fake implementation
|
||||
const registerListTasksTool = (server) => {
|
||||
const toolConfig = {
|
||||
name: 'get_tasks',
|
||||
description:
|
||||
'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
|
||||
parameters: mockZod,
|
||||
|
||||
execute: (args, context) => {
|
||||
const { log, session } = context;
|
||||
|
||||
try {
|
||||
log.info &&
|
||||
log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);
|
||||
|
||||
// Resolve paths using mock functions
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = mockResolveTasksPath(args, log);
|
||||
} catch (error) {
|
||||
log.error && log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return mockCreateErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
let complexityReportPath;
|
||||
try {
|
||||
complexityReportPath = mockResolveComplexityReportPath(args, session);
|
||||
} catch (error) {
|
||||
log.error &&
|
||||
log.error(`Error finding complexity report: ${error.message}`);
|
||||
complexityReportPath = null;
|
||||
}
|
||||
|
||||
const result = mockListTasksDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
status: args.status,
|
||||
withSubtasks: args.withSubtasks,
|
||||
reportPath: complexityReportPath
|
||||
},
|
||||
log
|
||||
);
|
||||
|
||||
log.info &&
|
||||
log.info(
|
||||
`Retrieved ${result.success ? result.data?.tasks?.length || 0 : 0} tasks`
|
||||
);
|
||||
return mockHandleApiResult(result, log, 'Error getting tasks');
|
||||
} catch (error) {
|
||||
log.error && log.error(`Error getting tasks: ${error.message}`);
|
||||
return mockCreateErrorResponse(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
server.addTool(toolConfig);
|
||||
};
|
||||
|
||||
describe('MCP Tool: get-tasks', () => {
|
||||
let mockServer;
|
||||
let executeFunction;
|
||||
|
||||
const mockLogger = {
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn()
|
||||
};
|
||||
|
||||
// Sample response data with different statuses for testing
|
||||
const tasksResponse = {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: [
|
||||
{ id: 1, title: 'Task 1', status: 'done' },
|
||||
{ id: 2, title: 'Task 2', status: 'pending' },
|
||||
{ id: 3, title: 'Task 3', status: 'in-progress' },
|
||||
{ id: 4, title: 'Task 4', status: 'blocked' },
|
||||
{ id: 5, title: 'Task 5', status: 'deferred' },
|
||||
{ id: 6, title: 'Task 6', status: 'review' }
|
||||
],
|
||||
filter: 'all',
|
||||
stats: {
|
||||
total: 6,
|
||||
completed: 1,
|
||||
inProgress: 1,
|
||||
pending: 1,
|
||||
blocked: 1,
|
||||
deferred: 1,
|
||||
review: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockServer = {
|
||||
addTool: jest.fn((config) => {
|
||||
executeFunction = config.execute;
|
||||
})
|
||||
};
|
||||
|
||||
// Setup default successful response
|
||||
mockListTasksDirect.mockReturnValue(tasksResponse);
|
||||
|
||||
// Register the tool
|
||||
registerListTasksTool(mockServer);
|
||||
});
|
||||
|
||||
test('should register the tool correctly', () => {
|
||||
expect(mockServer.addTool).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'get_tasks',
|
||||
description: expect.stringContaining('Get all tasks from Task Master'),
|
||||
parameters: expect.any(Object),
|
||||
execute: expect.any(Function)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle single status filter', () => {
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
const args = {
|
||||
status: 'pending',
|
||||
withSubtasks: false,
|
||||
projectRoot: '/mock/project'
|
||||
};
|
||||
|
||||
executeFunction(args, mockContext);
|
||||
|
||||
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'pending'
|
||||
}),
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle comma-separated status filter', () => {
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
const args = {
|
||||
status: 'done,pending,in-progress',
|
||||
withSubtasks: false,
|
||||
projectRoot: '/mock/project'
|
||||
};
|
||||
|
||||
executeFunction(args, mockContext);
|
||||
|
||||
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'done,pending,in-progress'
|
||||
}),
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle comma-separated status with spaces', () => {
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
const args = {
|
||||
status: 'blocked, deferred , review',
|
||||
withSubtasks: true,
|
||||
projectRoot: '/mock/project'
|
||||
};
|
||||
|
||||
executeFunction(args, mockContext);
|
||||
|
||||
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'blocked, deferred , review',
|
||||
withSubtasks: true
|
||||
}),
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle withSubtasks parameter correctly', () => {
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
// Test with withSubtasks=true
|
||||
executeFunction(
|
||||
{
|
||||
status: 'pending',
|
||||
withSubtasks: true,
|
||||
projectRoot: '/mock/project'
|
||||
},
|
||||
mockContext
|
||||
);
|
||||
|
||||
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
withSubtasks: true
|
||||
}),
|
||||
mockLogger
|
||||
);
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Test with withSubtasks=false
|
||||
executeFunction(
|
||||
{
|
||||
status: 'pending',
|
||||
withSubtasks: false,
|
||||
projectRoot: '/mock/project'
|
||||
},
|
||||
mockContext
|
||||
);
|
||||
|
||||
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
withSubtasks: false
|
||||
}),
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle path resolution errors gracefully', () => {
|
||||
mockResolveTasksPath.mockImplementationOnce(() => {
|
||||
throw new Error('Tasks file not found');
|
||||
});
|
||||
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
const args = {
|
||||
status: 'pending',
|
||||
projectRoot: '/mock/project'
|
||||
};
|
||||
|
||||
const result = executeFunction(args, mockContext);
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'Error finding tasks.json: Tasks file not found'
|
||||
);
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
|
||||
'Failed to find tasks.json: Tasks file not found'
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle complexity report path resolution errors gracefully', () => {
|
||||
mockResolveComplexityReportPath.mockImplementationOnce(() => {
|
||||
throw new Error('Complexity report not found');
|
||||
});
|
||||
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
const args = {
|
||||
status: 'pending',
|
||||
projectRoot: '/mock/project'
|
||||
};
|
||||
|
||||
executeFunction(args, mockContext);
|
||||
|
||||
// Should not fail the operation but set complexityReportPath to null
|
||||
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reportPath: null
|
||||
}),
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle listTasksDirect errors', () => {
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'LIST_TASKS_ERROR',
|
||||
message: 'Failed to list tasks'
|
||||
}
|
||||
};
|
||||
|
||||
mockListTasksDirect.mockReturnValueOnce(errorResponse);
|
||||
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
const args = {
|
||||
status: 'pending',
|
||||
projectRoot: '/mock/project'
|
||||
};
|
||||
|
||||
executeFunction(args, mockContext);
|
||||
|
||||
expect(mockHandleApiResult).toHaveBeenCalledWith(
|
||||
errorResponse,
|
||||
mockLogger,
|
||||
'Error getting tasks'
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle unexpected errors', () => {
|
||||
const testError = new Error('Unexpected error');
|
||||
mockListTasksDirect.mockImplementationOnce(() => {
|
||||
throw testError;
|
||||
});
|
||||
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
const args = {
|
||||
status: 'pending',
|
||||
projectRoot: '/mock/project'
|
||||
};
|
||||
|
||||
executeFunction(args, mockContext);
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'Error getting tasks: Unexpected error'
|
||||
);
|
||||
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
|
||||
});
|
||||
|
||||
test('should pass all parameters correctly', () => {
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
const args = {
|
||||
status: 'done,pending',
|
||||
withSubtasks: true,
|
||||
file: 'custom-tasks.json',
|
||||
complexityReport: 'custom-report.json',
|
||||
projectRoot: '/mock/project'
|
||||
};
|
||||
|
||||
executeFunction(args, mockContext);
|
||||
|
||||
// Verify path resolution functions were called with correct arguments
|
||||
expect(mockResolveTasksPath).toHaveBeenCalledWith(args, mockLogger);
|
||||
expect(mockResolveComplexityReportPath).toHaveBeenCalledWith(
|
||||
args,
|
||||
mockContext.session
|
||||
);
|
||||
|
||||
// Verify listTasksDirect was called with correct parameters
|
||||
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||||
{
|
||||
tasksJsonPath: '/mock/project/tasks.json',
|
||||
status: 'done,pending',
|
||||
withSubtasks: true,
|
||||
reportPath: '/mock/project/complexity-report.json'
|
||||
},
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
test('should log task count after successful retrieval', () => {
|
||||
const mockContext = {
|
||||
log: mockLogger,
|
||||
session: { workingDirectory: '/mock/dir' }
|
||||
};
|
||||
|
||||
const args = {
|
||||
status: 'pending',
|
||||
projectRoot: '/mock/project'
|
||||
};
|
||||
|
||||
executeFunction(args, mockContext);
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
`Retrieved ${tasksResponse.data.tasks.length} tasks`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -109,6 +109,14 @@ const sampleTasks = {
|
||||
status: 'cancelled',
|
||||
dependencies: [2, 3],
|
||||
priority: 'low'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Code Review',
|
||||
description: 'Review code for quality and standards',
|
||||
status: 'review',
|
||||
dependencies: [3],
|
||||
priority: 'medium'
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -154,7 +162,8 @@ describe('listTasks', () => {
|
||||
expect.objectContaining({ id: 1 }),
|
||||
expect.objectContaining({ id: 2 }),
|
||||
expect.objectContaining({ id: 3 }),
|
||||
expect.objectContaining({ id: 4 })
|
||||
expect.objectContaining({ id: 4 }),
|
||||
expect.objectContaining({ id: 5 })
|
||||
])
|
||||
})
|
||||
);
|
||||
@@ -174,6 +183,7 @@ describe('listTasks', () => {
|
||||
// Verify only pending tasks are returned
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
expect(result.tasks[0].status).toBe('pending');
|
||||
expect(result.tasks[0].id).toBe(2);
|
||||
});
|
||||
|
||||
test('should filter tasks by done status', async () => {
|
||||
@@ -190,6 +200,21 @@ describe('listTasks', () => {
|
||||
expect(result.tasks[0].status).toBe('done');
|
||||
});
|
||||
|
||||
test('should filter tasks by review status', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'review';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Verify only review tasks are returned
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
expect(result.tasks[0].status).toBe('review');
|
||||
expect(result.tasks[0].id).toBe(5);
|
||||
});
|
||||
|
||||
test('should include subtasks when withSubtasks option is true', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
@@ -320,13 +345,203 @@ describe('listTasks', () => {
|
||||
tasks: expect.any(Array),
|
||||
filter: 'all',
|
||||
stats: expect.objectContaining({
|
||||
total: 4,
|
||||
total: 5,
|
||||
completed: expect.any(Number),
|
||||
inProgress: expect.any(Number),
|
||||
pending: expect.any(Number)
|
||||
})
|
||||
})
|
||||
);
|
||||
expect(result.tasks).toHaveLength(4);
|
||||
expect(result.tasks).toHaveLength(5);
|
||||
});
|
||||
|
||||
// Tests for comma-separated status filtering
|
||||
describe('Comma-separated status filtering', () => {
|
||||
test('should filter tasks by multiple statuses separated by commas', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'done,pending';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
expect(readJSON).toHaveBeenCalledWith(tasksPath);
|
||||
|
||||
// Should return tasks with 'done' or 'pending' status
|
||||
expect(result.tasks).toHaveLength(2);
|
||||
expect(result.tasks.map((task) => task.status)).toEqual(
|
||||
expect.arrayContaining(['done', 'pending'])
|
||||
);
|
||||
|
||||
// Verify specific tasks
|
||||
const taskIds = result.tasks.map((task) => task.id);
|
||||
expect(taskIds).toContain(1); // done task
|
||||
expect(taskIds).toContain(2); // pending task
|
||||
});
|
||||
|
||||
test('should filter tasks by three or more statuses', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'done,pending,in-progress';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should return tasks with 'done', 'pending', or 'in-progress' status
|
||||
expect(result.tasks).toHaveLength(3);
|
||||
const statusValues = result.tasks.map((task) => task.status);
|
||||
expect(statusValues).toEqual(
|
||||
expect.arrayContaining(['done', 'pending', 'in-progress'])
|
||||
);
|
||||
|
||||
// Verify all matching tasks are included
|
||||
const taskIds = result.tasks.map((task) => task.id);
|
||||
expect(taskIds).toContain(1); // done
|
||||
expect(taskIds).toContain(2); // pending
|
||||
expect(taskIds).toContain(3); // in-progress
|
||||
expect(taskIds).not.toContain(4); // cancelled - should not be included
|
||||
});
|
||||
|
||||
test('should handle spaces around commas in status filter', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'done, pending , in-progress';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should trim spaces and work correctly
|
||||
expect(result.tasks).toHaveLength(3);
|
||||
const statusValues = result.tasks.map((task) => task.status);
|
||||
expect(statusValues).toEqual(
|
||||
expect.arrayContaining(['done', 'pending', 'in-progress'])
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle empty status values in comma-separated list', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'done,,pending,';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should ignore empty values and work with valid ones
|
||||
expect(result.tasks).toHaveLength(2);
|
||||
const statusValues = result.tasks.map((task) => task.status);
|
||||
expect(statusValues).toEqual(expect.arrayContaining(['done', 'pending']));
|
||||
});
|
||||
|
||||
test('should handle case-insensitive matching for comma-separated statuses', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'DONE,Pending,IN-PROGRESS';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should match case-insensitively
|
||||
expect(result.tasks).toHaveLength(3);
|
||||
const statusValues = result.tasks.map((task) => task.status);
|
||||
expect(statusValues).toEqual(
|
||||
expect.arrayContaining(['done', 'pending', 'in-progress'])
|
||||
);
|
||||
});
|
||||
|
||||
test('should return empty array when no tasks match comma-separated statuses', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'blocked,deferred';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should return empty array as no tasks have these statuses
|
||||
expect(result.tasks).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should work with single status when using comma syntax', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'pending,';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should work the same as single status filter
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
expect(result.tasks[0].status).toBe('pending');
|
||||
});
|
||||
|
||||
test('should set correct filter value in response for comma-separated statuses', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'done,pending';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should return the original filter string
|
||||
expect(result.filter).toBe('done,pending');
|
||||
});
|
||||
|
||||
test('should handle all statuses filter with comma syntax', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'all';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should return all tasks when filter is 'all'
|
||||
expect(result.tasks).toHaveLength(5);
|
||||
expect(result.filter).toBe('all');
|
||||
});
|
||||
|
||||
test('should handle mixed existing and non-existing statuses', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'done,nonexistent,pending';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should return only tasks with existing statuses
|
||||
expect(result.tasks).toHaveLength(2);
|
||||
const statusValues = result.tasks.map((task) => task.status);
|
||||
expect(statusValues).toEqual(expect.arrayContaining(['done', 'pending']));
|
||||
});
|
||||
|
||||
test('should filter by review status in comma-separated list', async () => {
|
||||
// Arrange
|
||||
const tasksPath = 'tasks/tasks.json';
|
||||
const statusFilter = 'review,cancelled';
|
||||
|
||||
// Act
|
||||
const result = listTasks(tasksPath, statusFilter, null, false, 'json');
|
||||
|
||||
// Assert
|
||||
// Should return tasks with 'review' or 'cancelled' status
|
||||
expect(result.tasks).toHaveLength(2);
|
||||
const statusValues = result.tasks.map((task) => task.status);
|
||||
expect(statusValues).toEqual(
|
||||
expect.arrayContaining(['review', 'cancelled'])
|
||||
);
|
||||
|
||||
// Verify specific tasks
|
||||
const taskIds = result.tasks.map((task) => task.id);
|
||||
expect(taskIds).toContain(4); // cancelled task
|
||||
expect(taskIds).toContain(5); // review task
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user