wip: e2e tests improvements

This commit is contained in:
Ralph Khreish
2025-07-11 19:11:49 +03:00
parent 39369ecd3c
commit e580e5d7c0
14 changed files with 930 additions and 2423 deletions

4
.gitignore vendored
View File

@@ -30,8 +30,8 @@ coverage-e2e/
# Test results and reports
test-results/
e2e-test-report.html
e2e-junit.xml
jest-results.json
jest-stare/
# Test temporary files and directories
tests/temp/

View File

@@ -33,18 +33,21 @@ export default {
reporters: [
'default',
[
'jest-html-reporter',
'jest-stare',
{
pageTitle: 'Task Master E2E Test Report',
outputPath: '<rootDir>/test-results/e2e-test-report.html',
includeFailureMsg: true,
includeConsoleLog: true,
dateFormat: 'yyyy-mm-dd HH:MM:ss',
theme: 'darkTheme',
sort: 'status',
executionTimeWarningThreshold: 5,
customCss: '.test-result { padding: 10px; }',
collapseSuitesByDefault: false
resultDir: 'test-results',
reportTitle: 'Task Master E2E Test Report',
additionalResultsProcessors: ['jest-junit'],
coverageLink: '../coverage-e2e/lcov-report/index.html',
jestStareConfigJson: 'jest-stare.config.json',
jestGlobalConfigJson: 'jest.e2e.config.json',
report: true,
reportSummary: true,
reportHeadline: 'Task Master E2E Test Results',
reportDescriptionHeadline: 'Comprehensive E2E test suite for all CLI commands',
disableCharts: false,
resultJson: 'jest-results.json',
resultHtml: 'index.html'
}
],
[

594
package-lock.json generated
View File

@@ -69,7 +69,7 @@
"ink": "^5.0.1",
"jest": "^29.7.0",
"jest-environment-node": "^29.7.0",
"jest-html-reporter": "^4.3.0",
"jest-html-reporters": "^3.1.7",
"jest-junit": "^16.0.0",
"mcp-jest": "^1.0.10",
"mock-fs": "^5.5.0",
@@ -2701,8 +2701,8 @@
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"devOptional": true,
"license": "ISC",
"optional": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
@@ -2719,8 +2719,8 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"devOptional": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12"
},
@@ -2732,15 +2732,15 @@
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"devOptional": true,
"license": "MIT",
"optional": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
@@ -2757,8 +2757,8 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"devOptional": true,
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -2773,8 +2773,8 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"devOptional": true,
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
@@ -3033,30 +3033,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/pattern": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
"integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"jest-regex-util": "30.0.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/pattern/node_modules/jest-regex-util": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/reporters": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
@@ -5354,13 +5330,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"dev": true,
"license": "ISC"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -6704,16 +6673,6 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/dateformat": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.2.tgz",
"integrity": "sha512-EelsCzH0gMC2YmXuMeaZ3c6md1sUJQxyb1XXc4xaisi/K6qKukqZhKPrEQyRkdNIncgYyLoDTReq0nNyuKerTg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -6993,8 +6952,8 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
@@ -7304,16 +7263,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/exit-x": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz",
"integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/expect": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
@@ -7828,8 +7777,8 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"devOptional": true,
"license": "ISC",
"optional": true,
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
@@ -9078,8 +9027,8 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"devOptional": true,
"license": "BlueOak-1.0.0",
"optional": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
@@ -9751,485 +9700,110 @@
"fsevents": "^2.3.2"
}
},
"node_modules/jest-html-reporter": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/jest-html-reporter/-/jest-html-reporter-4.3.0.tgz",
"integrity": "sha512-lq4Zx35yc6Ehw513CXJ1ok3wUmkSiOImWcyLAmylfzrz7DAqtrhDF9V73F4qfstmGxlr8X0QrEjWsl/oqhf4sQ==",
"node_modules/jest-html-reporters": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/jest-html-reporters/-/jest-html-reporters-3.1.7.tgz",
"integrity": "sha512-GTmjqK6muQ0S0Mnksf9QkL9X9z2FGIpNSxC52E0PHDzjPQ1XDu2+XTI3B3FS43ZiUzD1f354/5FfwbNIBzT7ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/reporters": "^30.0.2",
"@jest/test-result": "^30.0.2",
"@jest/types": "^30.0.1",
"dateformat": "3.0.2",
"mkdirp": "^1.0.3",
"strip-ansi": "6.0.1",
"xmlbuilder": "15.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"jest": "19.x - 30.x",
"typescript": "^3.7.x || ^4.3.x || ^5.x"
"fs-extra": "^10.0.0",
"open": "^8.0.3"
}
},
"node_modules/jest-html-reporter/node_modules/@jest/console": {
"version": "30.0.4",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.4.tgz",
"integrity": "sha512-tMLCDvBJBwPqMm4OAiuKm2uF5y5Qe26KgcMn+nrDSWpEW+eeFmqA0iO4zJfL16GP7gE3bUUQ3hIuUJ22AqVRnw==",
"node_modules/jest-html-reporters/node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.0.1",
"@types/node": "*",
"chalk": "^4.1.2",
"jest-message-util": "30.0.2",
"jest-util": "30.0.2",
"slash": "^3.0.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/@jest/reporters": {
"version": "30.0.4",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.4.tgz",
"integrity": "sha512-6ycNmP0JSJEEys1FbIzHtjl9BP0tOZ/KN6iMeAKrdvGmUsa1qfRdlQRUDKJ4P84hJ3xHw1yTqJt4fvPNHhyE+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^0.2.3",
"@jest/console": "30.0.4",
"@jest/test-result": "30.0.4",
"@jest/transform": "30.0.4",
"@jest/types": "30.0.1",
"@jridgewell/trace-mapping": "^0.3.25",
"@types/node": "*",
"chalk": "^4.1.2",
"collect-v8-coverage": "^1.0.2",
"exit-x": "^0.2.2",
"glob": "^10.3.10",
"graceful-fs": "^4.2.11",
"istanbul-lib-coverage": "^3.0.0",
"istanbul-lib-instrument": "^6.0.0",
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^5.0.0",
"istanbul-reports": "^3.1.3",
"jest-message-util": "30.0.2",
"jest-util": "30.0.2",
"jest-worker": "30.0.2",
"slash": "^3.0.0",
"string-length": "^4.0.2",
"v8-to-istanbul": "^9.0.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
},
"peerDependencies": {
"node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
},
"peerDependenciesMeta": {
"node-notifier": {
"optional": true
}
}
},
"node_modules/jest-html-reporter/node_modules/@jest/schemas": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz",
"integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/@jest/test-result": {
"version": "30.0.4",
"resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.4.tgz",
"integrity": "sha512-Mfpv8kjyKTHqsuu9YugB6z1gcdB3TSSOaKlehtVaiNlClMkEHY+5ZqCY2CrEE3ntpBMlstX/ShDAf84HKWsyIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/console": "30.0.4",
"@jest/types": "30.0.1",
"@types/istanbul-lib-coverage": "^2.0.6",
"collect-v8-coverage": "^1.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/@jest/transform": {
"version": "30.0.4",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.4.tgz",
"integrity": "sha512-atvy4hRph/UxdCIBp+UB2jhEA/jJiUeGZ7QPgBi9jUUKNgi3WEoMXGNG7zbbELG2+88PMabUNCDchmqgJy3ELg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/types": "30.0.1",
"@jridgewell/trace-mapping": "^0.3.25",
"babel-plugin-istanbul": "^7.0.0",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.0.2",
"jest-regex-util": "30.0.1",
"jest-util": "30.0.2",
"micromatch": "^4.0.8",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/@jest/types": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz",
"integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
"@jest/schemas": "30.0.1",
"@types/istanbul-lib-coverage": "^2.0.6",
"@types/istanbul-reports": "^3.0.4",
"@types/node": "*",
"@types/yargs": "^17.0.33",
"chalk": "^4.1.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/@sinclair/typebox": {
"version": "0.34.37",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
"integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-html-reporter/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-html-reporter/node_modules/babel-plugin-istanbul": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz",
"integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==",
"node_modules/jest-html-reporters/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "BSD-3-Clause",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
"@istanbuljs/load-nyc-config": "^1.0.0",
"@istanbuljs/schema": "^0.1.3",
"istanbul-lib-instrument": "^6.0.2",
"test-exclude": "^6.0.0"
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/jest-html-reporter/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"node_modules/jest-html-reporters/node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=10"
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/jest-html-reporter/node_modules/ci-info": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
"integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
"node_modules/jest-html-reporters/node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"license": "MIT",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jest-html-reporter/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"node_modules/jest-html-reporters/node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/jest-html-reporter/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jest-html-reporter/node_modules/istanbul-lib-source-maps": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.23",
"debug": "^4.1.1",
"istanbul-lib-coverage": "^3.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jest-html-reporter/node_modules/jest-haste-map": {
"version": "30.0.2",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz",
"integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.0.1",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
"jest-util": "30.0.2",
"jest-worker": "30.0.2",
"micromatch": "^4.0.8",
"walker": "^1.0.8"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
"universalify": "^2.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.3"
"graceful-fs": "^4.1.6"
}
},
"node_modules/jest-html-reporter/node_modules/jest-message-util": {
"version": "30.0.2",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz",
"integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==",
"node_modules/jest-html-reporters/node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.0.1",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"micromatch": "^4.0.8",
"pretty-format": "30.0.2",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/jest-regex-util": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/jest-util": {
"version": "30.0.2",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz",
"integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.0.1",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/jest-worker": {
"version": "30.0.2",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz",
"integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@ungap/structured-clone": "^1.3.0",
"jest-util": "30.0.2",
"merge-stream": "^2.0.0",
"supports-color": "^8.1.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/jest-worker/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jest-html-reporter/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jest-html-reporter/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-html-reporter/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/jest-html-reporter/node_modules/pretty-format": {
"version": "30.0.2",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
"integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.1",
"ansi-styles": "^5.2.0",
"react-is": "^18.3.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-html-reporter/node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"node_modules/jest-html-reporters/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-html-reporter/node_modules/write-file-atomic": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
"signal-exit": "^4.0.1"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
"node": ">= 10.0.0"
}
},
"node_modules/jest-junit": {
@@ -11551,8 +11125,8 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"devOptional": true,
"license": "ISC",
"optional": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -12066,8 +11640,8 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"devOptional": true,
"license": "BlueOak-1.0.0"
"license": "BlueOak-1.0.0",
"optional": true
},
"node_modules/package-manager-detector": {
"version": "0.2.11",
@@ -12210,8 +11784,8 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"devOptional": true,
"license": "BlueOak-1.0.0",
"optional": true,
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -13331,8 +12905,8 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"devOptional": true,
"license": "MIT",
"optional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -13346,15 +12920,15 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"devOptional": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
}
@@ -13403,8 +12977,8 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"devOptional": true,
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -14062,8 +13636,8 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"devOptional": true,
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -14080,8 +13654,8 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"devOptional": true,
"license": "MIT",
"optional": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -14096,15 +13670,15 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"devOptional": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
}
@@ -14113,8 +13687,8 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"devOptional": true,
"license": "MIT",
"optional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -14224,16 +13798,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/xmlbuilder": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.0.0.tgz",
"integrity": "sha512-KLu/G0DoWhkncQ9eHSI6s0/w+T4TM7rQaLhtCaL6tORv8jFlJPlnGumsgTcGfYeS1qZ/IHqrvDG7zJZ4d7e+nw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.0"
}
},
"node_modules/xsschema": {
"version": "0.3.0-beta.8",
"resolved": "https://registry.npmjs.org/xsschema/-/xsschema-0.3.0-beta.8.tgz",

View File

@@ -25,10 +25,10 @@
"test:e2e:core": "node tests/e2e/run-e2e-tests.js --groups core",
"test:e2e:providers": "node tests/e2e/run-e2e-tests.js --groups providers",
"test:e2e:advanced": "node tests/e2e/run-e2e-tests.js --groups advanced",
"test:e2e:jest": "jest --config jest.e2e.config.js",
"test:e2e:jest:watch": "jest --config jest.e2e.config.js --watch",
"test:e2e:jest:command": "jest --config jest.e2e.config.js --testNamePattern",
"test:e2e:jest:report": "open test-results/e2e-test-report.html",
"test:e2e:jest": "node --experimental-vm-modules node_modules/.bin/jest --config jest.e2e.config.js",
"test:e2e:jest:watch": "node --experimental-vm-modules node_modules/.bin/jest --config jest.e2e.config.js --watch",
"test:e2e:jest:command": "node --experimental-vm-modules node_modules/.bin/jest --config jest.e2e.config.js --testNamePattern",
"test:e2e:jest:report": "open test-results/index.html",
"prepare": "chmod +x bin/task-master.js mcp-server/server.js",
"changeset": "changeset",
"release": "changeset publish",
@@ -133,8 +133,8 @@
"ink": "^5.0.1",
"jest": "^29.7.0",
"jest-environment-node": "^29.7.0",
"jest-html-reporter": "^4.3.0",
"jest-junit": "^16.0.0",
"jest-html-reporters": "^3.1.7",
"mcp-jest": "^1.0.10",
"mock-fs": "^5.5.0",
"prettier": "^3.5.3",

View File

@@ -1,30 +1,40 @@
# E2E Test Reports
Task Master's E2E tests now generate comprehensive test reports similar to Playwright's reporting capabilities.
Task Master's E2E tests now generate comprehensive test reports using Jest Stare, providing an interactive and visually appealing test report similar to Playwright's reporting capabilities.
## Test Report Formats
When you run `npm run test:e2e:jest`, the following reports are generated:
### 1. HTML Report
- **Location**: `test-results/e2e-test-report.html`
### 1. Jest Stare HTML Report
- **Location**: `test-results/index.html`
- **Features**:
- Beautiful dark theme UI
- Test execution timeline
- Detailed failure messages
- Interactive dashboard with charts and graphs
- Test execution timeline and performance metrics
- Detailed failure messages with stack traces
- Console output for each test
- Search and filter capabilities
- Pass/Fail/Skip statistics with visual charts
- Test duration analysis
- Collapsible test suites
- Execution time warnings
- Sort by status (failed tests first)
- Coverage link integration
- Summary statistics
### 2. JUnit XML Report
### 2. JSON Results
- **Location**: `test-results/jest-results.json`
- **Use Cases**:
- Programmatic access to test results
- Custom reporting tools
- Test result analysis
### 3. JUnit XML Report
- **Location**: `test-results/e2e-junit.xml`
- **Use Cases**:
- CI/CD integration
- Test result parsing
- Historical tracking
### 3. Console Output
### 4. Console Output
- Standard Jest terminal output with verbose mode enabled
## Running Tests with Reports

View File

@@ -3,8 +3,9 @@
* Runs before each test file
*/
const { TestHelpers } = require('../utils/test-helpers.cjs');
const { TestLogger } = require('../utils/logger.cjs');
import { jest, expect, afterAll } from '@jest/globals';
import { TestHelpers } from '../utils/test-helpers.js';
import { TestLogger } from '../utils/logger.js';
// Increase timeout for all E2E tests (can be overridden per test)
jest.setTimeout(180000);
@@ -79,4 +80,4 @@ global.createTestContext = (testName) => {
afterAll(async () => {
// Give time for any async operations to complete
await new Promise((resolve) => setTimeout(resolve, 100));
});
});

View File

@@ -1,443 +0,0 @@
export default async function testAdvanced(logger, helpers, context) {
const { testDir } = context;
logger.info('Starting advanced features tests...');
const results = [];
try {
// Test Tag Context Management
logger.info('Testing tag context management...');
// Create new tag contexts
const tag1Result = await helpers.taskMaster(
'add-tag',
['feature-auth', '--description', 'Authentication feature'],
{ cwd: testDir }
);
results.push({
test: 'Create tag context - feature-auth',
passed: tag1Result.exitCode === 0,
output: tag1Result.stdout
});
const tag2Result = await helpers.taskMaster(
'add-tag',
['feature-api', '--description', 'API development'],
{ cwd: testDir }
);
results.push({
test: 'Create tag context - feature-api',
passed: tag2Result.exitCode === 0,
output: tag2Result.stdout
});
// Add task to feature-auth tag
const task1Result = await helpers.taskMaster(
'add-task',
['--tag=feature-auth', '--prompt', 'Implement user authentication'],
{ cwd: testDir }
);
results.push({
test: 'Add task to feature-auth tag',
passed: task1Result.exitCode === 0,
output: task1Result.stdout
});
// Add task to feature-api tag
const task2Result = await helpers.taskMaster(
'add-task',
['--tag=feature-api', '--prompt', 'Create REST API endpoints'],
{ cwd: testDir }
);
results.push({
test: 'Add task to feature-api tag',
passed: task2Result.exitCode === 0,
output: task2Result.stdout
});
// List all tag contexts
const listTagsResult = await helpers.taskMaster('tags', [], {
cwd: testDir
});
results.push({
test: 'List all tag contexts',
passed:
listTagsResult.exitCode === 0 &&
listTagsResult.stdout.includes('feature-auth') &&
listTagsResult.stdout.includes('feature-api'),
output: listTagsResult.stdout
});
// List tasks in feature-auth tag
const taggedTasksResult = await helpers.taskMaster(
'list',
['--tag=feature-auth'],
{ cwd: testDir }
);
results.push({
test: 'List tasks in feature-auth tag',
passed:
taggedTasksResult.exitCode === 0 &&
taggedTasksResult.stdout.includes('Implement user authentication'),
output: taggedTasksResult.stdout
});
// Test Model Configuration
logger.info('Testing model configuration...');
// Set main model
const setMainModelResult = await helpers.taskMaster(
'models',
['--set-main', 'gpt-4'],
{ cwd: testDir }
);
results.push({
test: 'Set main model',
passed: setMainModelResult.exitCode === 0,
output: setMainModelResult.stdout
});
// Set research model
const setResearchModelResult = await helpers.taskMaster(
'models',
['--set-research', 'claude-3-sonnet'],
{ cwd: testDir }
);
results.push({
test: 'Set research model',
passed: setResearchModelResult.exitCode === 0,
output: setResearchModelResult.stdout
});
// Set fallback model
const setFallbackModelResult = await helpers.taskMaster(
'models',
['--set-fallback', 'gpt-3.5-turbo'],
{ cwd: testDir }
);
results.push({
test: 'Set fallback model',
passed: setFallbackModelResult.exitCode === 0,
output: setFallbackModelResult.stdout
});
// Verify model configuration
const showModelsResult = await helpers.taskMaster('models', [], {
cwd: testDir
});
results.push({
test: 'Show model configuration',
passed:
showModelsResult.exitCode === 0 &&
showModelsResult.stdout.includes('gpt-4') &&
showModelsResult.stdout.includes('claude-3-sonnet') &&
showModelsResult.stdout.includes('gpt-3.5-turbo'),
output: showModelsResult.stdout
});
// Test Task Expansion
logger.info('Testing task expansion...');
// Add task for expansion
const expandTaskResult = await helpers.taskMaster(
'add-task',
['--prompt', 'Build REST API'],
{ cwd: testDir }
);
const expandTaskMatch = expandTaskResult.stdout.match(/#(\d+)/);
const expandTaskId = expandTaskMatch ? expandTaskMatch[1] : null;
results.push({
test: 'Add task for expansion',
passed: expandTaskResult.exitCode === 0 && expandTaskId !== null,
output: expandTaskResult.stdout
});
if (expandTaskId) {
// Single task expansion
const expandResult = await helpers.taskMaster('expand', [expandTaskId], {
cwd: testDir
});
results.push({
test: 'Expand single task',
passed:
expandResult.exitCode === 0 &&
expandResult.stdout.includes('subtasks'),
output: expandResult.stdout
});
// Verify expand worked
const afterExpandResult = await helpers.taskMaster(
'show',
[expandTaskId],
{ cwd: testDir }
);
results.push({
test: 'Verify task expansion',
passed:
afterExpandResult.exitCode === 0 &&
afterExpandResult.stdout.includes('subtasks'),
output: afterExpandResult.stdout
});
// Force expand (re-expand)
const forceExpandResult = await helpers.taskMaster(
'expand',
[expandTaskId, '--force'],
{ cwd: testDir }
);
results.push({
test: 'Force expand task',
passed: forceExpandResult.exitCode === 0,
output: forceExpandResult.stdout
});
}
// Test Subtask Management
logger.info('Testing subtask management...');
// Add task for subtask testing
const subtaskParentResult = await helpers.taskMaster(
'add-task',
['--prompt', 'Create user interface'],
{ cwd: testDir }
);
const parentMatch = subtaskParentResult.stdout.match(/#(\d+)/);
const parentTaskId = parentMatch ? parentMatch[1] : null;
if (parentTaskId) {
// Add subtasks manually
const addSubtask1Result = await helpers.taskMaster(
'add-subtask',
['--parent', parentTaskId, '--title', 'Design mockups'],
{ cwd: testDir }
);
results.push({
test: 'Add subtask - Design mockups',
passed: addSubtask1Result.exitCode === 0,
output: addSubtask1Result.stdout
});
const addSubtask2Result = await helpers.taskMaster(
'add-subtask',
['--parent', parentTaskId, '--title', 'Implement components'],
{ cwd: testDir }
);
results.push({
test: 'Add subtask - Implement components',
passed: addSubtask2Result.exitCode === 0,
output: addSubtask2Result.stdout
});
// List subtasks (use show command to see subtasks)
const listSubtasksResult = await helpers.taskMaster(
'show',
[parentTaskId],
{ cwd: testDir }
);
results.push({
test: 'List subtasks',
passed:
listSubtasksResult.exitCode === 0 &&
listSubtasksResult.stdout.includes('Design mockups') &&
listSubtasksResult.stdout.includes('Implement components'),
output: listSubtasksResult.stdout
});
// Update subtask
const subtaskId = `${parentTaskId}.1`;
const updateSubtaskResult = await helpers.taskMaster(
'update-subtask',
['--id', subtaskId, '--prompt', 'Create detailed mockups'],
{ cwd: testDir }
);
results.push({
test: 'Update subtask',
passed: updateSubtaskResult.exitCode === 0,
output: updateSubtaskResult.stdout
});
// Remove subtask
const removeSubtaskId = `${parentTaskId}.2`;
const removeSubtaskResult = await helpers.taskMaster(
'remove-subtask',
['--id', removeSubtaskId],
{ cwd: testDir }
);
results.push({
test: 'Remove subtask',
passed: removeSubtaskResult.exitCode === 0,
output: removeSubtaskResult.stdout
});
// Verify subtask changes
const verifySubtasksResult = await helpers.taskMaster(
'show',
[parentTaskId],
{ cwd: testDir }
);
results.push({
test: 'Verify subtask changes',
passed:
verifySubtasksResult.exitCode === 0 &&
verifySubtasksResult.stdout.includes('Create detailed mockups') &&
!verifySubtasksResult.stdout.includes('Implement components'),
output: verifySubtasksResult.stdout
});
// Clear all subtasks
const clearSubtasksResult = await helpers.taskMaster(
'clear-subtasks',
['--id', parentTaskId],
{ cwd: testDir }
);
results.push({
test: 'Clear all subtasks',
passed: clearSubtasksResult.exitCode === 0,
output: clearSubtasksResult.stdout
});
// Verify subtasks cleared
const verifyClearResult = await helpers.taskMaster(
'show',
[parentTaskId],
{ cwd: testDir }
);
results.push({
test: 'Verify subtasks cleared',
passed:
verifyClearResult.exitCode === 0 &&
!verifyClearResult.stdout.includes('Design mockups') &&
!verifyClearResult.stdout.includes('Create detailed mockups'),
output: verifyClearResult.stdout
});
}
// Test Expand All
logger.info('Testing expand all...');
// Add multiple tasks
await helpers.taskMaster(
'add-task',
['--prompt', 'Task A for expand all'],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-task',
['--prompt', 'Task B for expand all'],
{ cwd: testDir }
);
const expandAllResult = await helpers.taskMaster('expand', ['--all'], {
cwd: testDir
});
results.push({
test: 'Expand all tasks',
passed: expandAllResult.exitCode === 0,
output: expandAllResult.stdout
});
// Test Generate Task Files
logger.info('Testing generate task files...');
// Generate files for a specific task
if (expandTaskId) {
const generateResult = await helpers.taskMaster(
'generate',
[expandTaskId],
{ cwd: testDir }
);
results.push({
test: 'Generate task files',
passed: generateResult.exitCode === 0,
output: generateResult.stdout
});
// Check if files were created
const taskFilePath = `${testDir}/tasks/task_${expandTaskId}.md`;
const fileExists = helpers.fileExists(taskFilePath);
results.push({
test: 'Verify generated task file exists',
passed: fileExists,
output: fileExists
? `Task file created at ${taskFilePath}`
: 'Task file not found'
});
}
// Test Tag Context Integrity After Operations
logger.info('Testing tag context integrity after operations...');
// Verify tag contexts still exist
const finalTagListResult = await helpers.taskMaster('tags', [], {
cwd: testDir
});
results.push({
test: 'Final tag context list verification',
passed:
finalTagListResult.exitCode === 0 &&
finalTagListResult.stdout.includes('feature-auth') &&
finalTagListResult.stdout.includes('feature-api'),
output: finalTagListResult.stdout
});
// Verify tasks are still in their respective tag contexts
const finalTaggedTasksResult = await helpers.taskMaster(
'list',
['--tag=feature-api'],
{ cwd: testDir }
);
results.push({
test: 'Final tasks in tag context verification',
passed:
finalTaggedTasksResult.exitCode === 0 &&
finalTaggedTasksResult.stdout.includes('Create REST API endpoints'),
output: finalTaggedTasksResult.stdout
});
// Test Additional Advanced Features
logger.info('Testing additional advanced features...');
// Test priority task
const priorityTagResult = await helpers.taskMaster(
'add-task',
['--prompt', 'High priority task', '--priority', 'high'],
{ cwd: testDir }
);
results.push({
test: 'Add task with high priority',
passed: priorityTagResult.exitCode === 0,
output: priorityTagResult.stdout
});
// Test filtering by status
const statusFilterResult = await helpers.taskMaster(
'list',
['--status', 'pending'],
{ cwd: testDir }
);
results.push({
test: 'Filter by status',
passed: statusFilterResult.exitCode === 0,
output: statusFilterResult.stdout
});
} catch (error) {
logger.error('Error in advanced features tests:', error);
results.push({
test: 'Advanced features test suite',
passed: false,
error: error.message
});
}
const passed = results.filter((r) => r.passed).length;
const total = results.length;
return {
name: 'Advanced Features',
passed,
total,
results,
summary: `Advanced features tests: ${passed}/${total} passed`
};
}

View File

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

View File

@@ -1,379 +0,0 @@
/**
* Core task operations test module
* Tests all fundamental task management functionality
*/
export default async function testCoreOperations(logger, helpers, context) {
const { testDir } = context;
const results = {
status: 'passed',
errors: []
};
try {
logger.info('Starting core task operations tests...');
// Test 1: List tasks (may have tasks from PRD parsing)
logger.info('\nTest 1: List tasks');
const listResult1 = await helpers.taskMaster('list', [], { cwd: testDir });
if (listResult1.exitCode !== 0) {
throw new Error(`List command failed: ${listResult1.stderr}`);
}
// Check for expected output patterns - either empty or with tasks
const hasValidOutput =
listResult1.stdout.includes('No tasks found') ||
listResult1.stdout.includes('Task List') ||
listResult1.stdout.includes('Project Dashboard') ||
listResult1.stdout.includes('Listing tasks from');
if (!hasValidOutput) {
throw new Error('Unexpected list output format');
}
logger.success('✓ List tasks successful');
// Test 2: Add manual task
logger.info('\nTest 2: Add manual task');
const addResult1 = await helpers.taskMaster(
'add-task',
[
'--title',
'Write unit tests',
'--description',
'Create comprehensive unit tests for the application'
],
{ cwd: testDir }
);
if (addResult1.exitCode !== 0) {
throw new Error(`Failed to add manual task: ${addResult1.stderr}`);
}
const manualTaskId = helpers.extractTaskId(addResult1.stdout);
if (!manualTaskId) {
throw new Error('Failed to extract task ID from add output');
}
logger.success(`✓ Added manual task with ID: ${manualTaskId}`);
// Test 3: Add AI task
logger.info('\nTest 3: Add AI task');
const addResult2 = await helpers.taskMaster(
'add-task',
['--prompt', 'Implement authentication system'],
{ cwd: testDir }
);
if (addResult2.exitCode !== 0) {
throw new Error(`Failed to add AI task: ${addResult2.stderr}`);
}
const aiTaskId = helpers.extractTaskId(addResult2.stdout);
if (!aiTaskId) {
throw new Error('Failed to extract AI task ID from add output');
}
logger.success(`✓ Added AI task with ID: ${aiTaskId}`);
// Test 4: Add another task for dependency testing
logger.info('\nTest 4: Add task for dependency testing');
const addResult3 = await helpers.taskMaster(
'add-task',
[
'--title',
'Create database schema',
'--description',
'Design and implement the database schema'
],
{ cwd: testDir }
);
if (addResult3.exitCode !== 0) {
throw new Error(`Failed to add database task: ${addResult3.stderr}`);
}
const dbTaskId = helpers.extractTaskId(addResult3.stdout);
if (!dbTaskId) {
throw new Error('Failed to extract database task ID');
}
logger.success(`✓ Added database task with ID: ${dbTaskId}`);
// Test 5: List tasks (should show our newly added tasks)
logger.info('\nTest 5: List all tasks');
const listResult2 = await helpers.taskMaster('list', [], { cwd: testDir });
if (listResult2.exitCode !== 0) {
throw new Error(`List command failed: ${listResult2.stderr}`);
}
// Check that we can find our task IDs in the output
const hasTask11 = listResult2.stdout.includes('11');
const hasTask12 = listResult2.stdout.includes('12');
const hasTask13 = listResult2.stdout.includes('13');
if (!hasTask11 || !hasTask12 || !hasTask13) {
throw new Error('Not all task IDs found in list output');
}
// Also check for partial matches (list may truncate titles)
const hasOurTasks =
listResult2.stdout.includes('Write') ||
listResult2.stdout.includes('Create');
if (hasOurTasks) {
logger.success('✓ List tasks shows our added tasks');
} else {
logger.warning('Task titles may be truncated in list view');
}
// Test 6: Get next task
logger.info('\nTest 6: Get next task');
const nextResult = await helpers.taskMaster('next', [], { cwd: testDir });
if (nextResult.exitCode !== 0) {
throw new Error(`Next task command failed: ${nextResult.stderr}`);
}
logger.success('✓ Get next task successful');
// Test 7: Show task details
logger.info('\nTest 7: Show task details');
const showResult = await helpers.taskMaster('show', [aiTaskId], {
cwd: testDir
});
if (showResult.exitCode !== 0) {
throw new Error(`Show task details failed: ${showResult.stderr}`);
}
// Check that the task ID is shown and basic structure is present
if (
!showResult.stdout.includes(`Task: #${aiTaskId}`) &&
!showResult.stdout.includes(`ID: │ ${aiTaskId}`)
) {
throw new Error('Task ID not found in show output');
}
if (
!showResult.stdout.includes('Status:') ||
!showResult.stdout.includes('Priority:')
) {
throw new Error('Task details missing expected fields');
}
logger.success('✓ Show task details successful');
// Test 8: Add dependencies
logger.info('\nTest 8: Add dependencies');
const addDepResult = await helpers.taskMaster(
'add-dependency',
['--id', aiTaskId, '--depends-on', dbTaskId],
{ cwd: testDir }
);
if (addDepResult.exitCode !== 0) {
throw new Error(`Failed to add dependency: ${addDepResult.stderr}`);
}
logger.success('✓ Added dependency successfully');
// Test 9: Verify dependency was added
logger.info('\nTest 9: Verify dependency');
const showResult2 = await helpers.taskMaster('show', [aiTaskId], {
cwd: testDir
});
if (showResult2.exitCode !== 0) {
throw new Error(`Show task failed: ${showResult2.stderr}`);
}
if (
!showResult2.stdout.includes('Dependencies:') ||
!showResult2.stdout.includes(dbTaskId)
) {
throw new Error('Dependency not shown in task details');
}
logger.success('✓ Dependency verified in task details');
// Test 10: Test circular dependency (should fail)
logger.info('\nTest 10: Test circular dependency prevention');
const circularResult = await helpers.taskMaster(
'add-dependency',
['--id', dbTaskId, '--depends-on', aiTaskId],
{
cwd: testDir,
allowFailure: true
}
);
if (circularResult.exitCode === 0) {
throw new Error('Circular dependency was not prevented');
}
if (!circularResult.stderr.toLowerCase().includes('circular')) {
throw new Error('Expected circular dependency error message');
}
logger.success('✓ Circular dependency prevented successfully');
// Test 11: Test non-existent dependency
logger.info('\nTest 11: Test non-existent dependency');
const nonExistResult = await helpers.taskMaster(
'add-dependency',
['--id', '99999', '--depends-on', '88888'],
{
cwd: testDir,
allowFailure: true
}
);
if (nonExistResult.exitCode === 0) {
throw new Error('Non-existent dependency was incorrectly allowed');
}
logger.success('✓ Non-existent dependency handled correctly');
// Test 12: Remove dependency
logger.info('\nTest 12: Remove dependency');
const removeDepResult = await helpers.taskMaster(
'remove-dependency',
['--id', aiTaskId, '--depends-on', dbTaskId],
{ cwd: testDir }
);
if (removeDepResult.exitCode !== 0) {
throw new Error(`Failed to remove dependency: ${removeDepResult.stderr}`);
}
logger.success('✓ Removed dependency successfully');
// Test 13: Validate dependencies
logger.info('\nTest 13: Validate dependencies');
const validateResult = await helpers.taskMaster(
'validate-dependencies',
[],
{ cwd: testDir }
);
if (validateResult.exitCode !== 0) {
throw new Error(`Dependency validation failed: ${validateResult.stderr}`);
}
logger.success('✓ Dependency validation successful');
// Test 14: Update task description
logger.info('\nTest 14: Update task description');
const updateResult = await helpers.taskMaster(
'update-task',
[manualTaskId, '--description', 'Write comprehensive unit tests'],
{ cwd: testDir }
);
if (updateResult.exitCode !== 0) {
throw new Error(`Failed to update task: ${updateResult.stderr}`);
}
logger.success('✓ Updated task description successfully');
// Test 15: Add subtask
logger.info('\nTest 15: Add subtask');
const subtaskResult = await helpers.taskMaster(
'add-subtask',
[manualTaskId, 'Write test for login'],
{ cwd: testDir }
);
if (subtaskResult.exitCode !== 0) {
throw new Error(`Failed to add subtask: ${subtaskResult.stderr}`);
}
const subtaskId = helpers.extractTaskId(subtaskResult.stdout) || '1.1';
logger.success(`✓ Added subtask with ID: ${subtaskId}`);
// Test 16: Verify subtask relationship
logger.info('\nTest 16: Verify subtask relationship');
const showResult3 = await helpers.taskMaster('show', [manualTaskId], {
cwd: testDir
});
if (showResult3.exitCode !== 0) {
throw new Error(`Show task failed: ${showResult3.stderr}`);
}
if (!showResult3.stdout.includes('Subtasks:')) {
throw new Error('Subtasks section not shown in parent task');
}
logger.success('✓ Subtask relationship verified');
// Test 17: Set task status to in_progress
logger.info('\nTest 17: Set task status to in_progress');
const statusResult1 = await helpers.taskMaster(
'set-status',
[manualTaskId, 'in_progress'],
{ cwd: testDir }
);
if (statusResult1.exitCode !== 0) {
throw new Error(`Failed to update task status: ${statusResult1.stderr}`);
}
logger.success('✓ Set task status to in_progress');
// Test 18: Set task status to completed
logger.info('\nTest 18: Set task status to completed');
const statusResult2 = await helpers.taskMaster(
'set-status',
[dbTaskId, 'completed'],
{ cwd: testDir }
);
if (statusResult2.exitCode !== 0) {
throw new Error(`Failed to complete task: ${statusResult2.stderr}`);
}
logger.success('✓ Set task status to completed');
// Test 19: List tasks with status filter
logger.info('\nTest 19: List tasks by status');
const listStatusResult = await helpers.taskMaster(
'list',
['--status', 'completed'],
{ cwd: testDir }
);
if (listStatusResult.exitCode !== 0) {
throw new Error(`List by status failed: ${listStatusResult.stderr}`);
}
if (!listStatusResult.stdout.includes('Create database schema')) {
throw new Error('Completed task not shown in filtered list');
}
logger.success('✓ List tasks by status successful');
// Test 20: Remove single task
logger.info('\nTest 20: Remove single task');
const removeResult1 = await helpers.taskMaster('remove-task', [dbTaskId], {
cwd: testDir
});
if (removeResult1.exitCode !== 0) {
throw new Error(`Failed to remove task: ${removeResult1.stderr}`);
}
logger.success('✓ Removed single task successfully');
// Test 21: Remove multiple tasks
logger.info('\nTest 21: Remove multiple tasks');
const removeResult2 = await helpers.taskMaster(
'remove-task',
[manualTaskId, aiTaskId],
{ cwd: testDir }
);
if (removeResult2.exitCode !== 0) {
throw new Error(
`Failed to remove multiple tasks: ${removeResult2.stderr}`
);
}
logger.success('✓ Removed multiple tasks successfully');
// Test 22: Verify tasks were removed
logger.info('\nTest 22: Verify tasks were removed');
const listResult3 = await helpers.taskMaster('list', [], { cwd: testDir });
if (listResult3.exitCode !== 0) {
throw new Error(`List command failed: ${listResult3.stderr}`);
}
// Check that our specific task IDs are no longer in the list
const stillHasTask11 = new RegExp(`\\b${manualTaskId}\\b`).test(
listResult3.stdout
);
const stillHasTask12 = new RegExp(`\\b${aiTaskId}\\b`).test(
listResult3.stdout
);
const stillHasTask13 = new RegExp(`\\b${dbTaskId}\\b`).test(
listResult3.stdout
);
if (stillHasTask11 || stillHasTask12 || stillHasTask13) {
throw new Error('Removed task IDs still appear in list');
}
logger.success('✓ Verified tasks were removed');
// Test 23: Fix dependencies (cleanup)
logger.info('\nTest 23: Fix dependencies');
const fixDepsResult = await helpers.taskMaster('fix-dependencies', [], {
cwd: testDir
});
if (fixDepsResult.exitCode !== 0) {
// Non-critical, just log
logger.warning(`Fix dependencies had issues: ${fixDepsResult.stderr}`);
} else {
logger.success('✓ Fix dependencies command executed');
}
logger.info('\n✅ All core task operations tests passed!');
} catch (error) {
results.status = 'failed';
results.errors.push({
test: 'core operations',
error: error.message,
stack: error.stack
});
logger.error(`Core operations test failed: ${error.message}`);
}
return results;
}

View File

@@ -0,0 +1,284 @@
const { exec } = require('child_process');
const { promisify } = require('util');
const fs = require('fs').promises;
const path = require('path');
const execAsync = promisify(exec);
// Helper function to run MCP inspector CLI commands
async function runMCPCommand(method, args = {}) {
const serverPath = path.join(__dirname, '../../../../mcp-server/server.js');
let command = `npx @modelcontextprotocol/inspector --cli node ${serverPath} --method ${method}`;
// Add tool-specific arguments
if (args.toolName) {
command += ` --tool-name ${args.toolName}`;
}
// Add tool arguments
if (args.toolArgs) {
for (const [key, value] of Object.entries(args.toolArgs)) {
command += ` --tool-arg ${key}=${value}`;
}
}
try {
const { stdout, stderr } = await execAsync(command, {
timeout: 60000, // 60 second timeout for AI operations
env: { ...process.env, NODE_ENV: 'test' }
});
if (stderr && !stderr.includes('DeprecationWarning')) {
console.error('MCP Command stderr:', stderr);
}
return { stdout, stderr };
} catch (error) {
console.error('MCP Command failed:', error);
throw error;
}
}
describe('MCP Inspector CLI - expand_task Tool Tests', () => {
const testProjectPath = path.join(__dirname, '../../../../test-fixtures/mcp-expand-test-project');
const tasksDir = path.join(testProjectPath, '.taskmaster/tasks');
const tasksFile = path.join(tasksDir, 'tasks.json');
beforeAll(async () => {
// Create test project directory structure
await fs.mkdir(tasksDir, { recursive: true });
// Create sample tasks data
const sampleTasks = {
tasks: [
{
id: 1,
description: 'Implement user authentication system',
status: 'pending',
tags: ['master'],
subtasks: []
},
{
id: 2,
description: 'Create API endpoints',
status: 'pending',
tags: ['master'],
subtasks: [
{
id: '2.1',
description: 'Setup Express server',
status: 'pending'
}
]
},
{
id: 3,
description: 'Design database schema',
status: 'completed',
tags: ['master']
}
],
tags: {
master: {
name: 'master',
description: 'Main development branch'
}
},
activeTag: 'master',
metadata: {
nextId: 4,
version: '1.0.0'
}
};
await fs.writeFile(tasksFile, JSON.stringify(sampleTasks, null, 2));
});
afterAll(async () => {
// Clean up test project
await fs.rm(testProjectPath, { recursive: true, force: true });
});
it('should list available tools including expand_task', async () => {
const { stdout } = await runMCPCommand('tools/list');
const response = JSON.parse(stdout);
expect(response).toHaveProperty('tools');
expect(Array.isArray(response.tools)).toBe(true);
const expandTaskTool = response.tools.find(tool => tool.name === 'expand_task');
expect(expandTaskTool).toBeDefined();
expect(expandTaskTool.description).toContain('Expand a task into subtasks');
});
it('should expand a task without existing subtasks', async () => {
// Skip if no API key is set
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
console.log('Skipping test: No AI API key found in environment');
return;
}
const { stdout } = await runMCPCommand('tools/call', {
toolName: 'expand_task',
toolArgs: {
id: '1',
projectRoot: testProjectPath,
num: '3',
prompt: 'Focus on security and authentication best practices'
}
});
const response = JSON.parse(stdout);
expect(response).toHaveProperty('content');
expect(Array.isArray(response.content)).toBe(true);
// Parse the text content to get result
const textContent = response.content.find(c => c.type === 'text');
expect(textContent).toBeDefined();
const result = JSON.parse(textContent.text);
expect(result.task).toBeDefined();
expect(result.task.id).toBe(1);
expect(result.subtasksAdded).toBeGreaterThan(0);
// Verify the task was actually updated
const updatedTasks = JSON.parse(await fs.readFile(tasksFile, 'utf8'));
const expandedTask = updatedTasks.tasks.find(t => t.id === 1);
expect(expandedTask.subtasks.length).toBeGreaterThan(0);
});
it('should handle expansion with force flag for task with existing subtasks', async () => {
// Skip if no API key is set
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
console.log('Skipping test: No AI API key found in environment');
return;
}
const { stdout } = await runMCPCommand('tools/call', {
toolName: 'expand_task',
toolArgs: {
id: '2',
projectRoot: testProjectPath,
force: 'true',
num: '2'
}
});
const response = JSON.parse(stdout);
const textContent = response.content.find(c => c.type === 'text');
const result = JSON.parse(textContent.text);
expect(result.task).toBeDefined();
expect(result.task.id).toBe(2);
expect(result.subtasksAdded).toBe(2);
});
it('should reject expansion of completed task', async () => {
const { stdout } = await runMCPCommand('tools/call', {
toolName: 'expand_task',
toolArgs: {
id: '3',
projectRoot: testProjectPath
}
});
const response = JSON.parse(stdout);
expect(response).toHaveProperty('content');
const textContent = response.content.find(c => c.type === 'text');
expect(textContent.text).toContain('Error');
expect(textContent.text).toContain('completed');
});
it('should handle invalid task ID', async () => {
const { stdout } = await runMCPCommand('tools/call', {
toolName: 'expand_task',
toolArgs: {
id: '999',
projectRoot: testProjectPath
}
});
const response = JSON.parse(stdout);
const textContent = response.content.find(c => c.type === 'text');
expect(textContent.text).toContain('Error');
expect(textContent.text).toContain('not found');
});
it('should handle missing required parameters', async () => {
try {
await runMCPCommand('tools/call', {
toolName: 'expand_task',
toolArgs: {
// Missing id and projectRoot
num: '3'
}
});
fail('Should have thrown an error');
} catch (error) {
expect(error.message).toContain('validation');
}
});
it('should work with custom tasks file path', async () => {
// Skip if no API key is set
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
console.log('Skipping test: No AI API key found in environment');
return;
}
// Create custom tasks file
const customDir = path.join(testProjectPath, 'custom');
await fs.mkdir(customDir, { recursive: true });
const customTasksPath = path.join(customDir, 'my-tasks.json');
await fs.copyFile(tasksFile, customTasksPath);
const { stdout } = await runMCPCommand('tools/call', {
toolName: 'expand_task',
toolArgs: {
id: '1',
projectRoot: testProjectPath,
file: 'custom/my-tasks.json',
num: '2'
}
});
const response = JSON.parse(stdout);
const textContent = response.content.find(c => c.type === 'text');
const result = JSON.parse(textContent.text);
expect(result.task).toBeDefined();
expect(result.subtasksAdded).toBe(2);
// Verify the custom file was updated
const updatedData = JSON.parse(await fs.readFile(customTasksPath, 'utf8'));
const task = updatedData.tasks.find(t => t.id === 1);
expect(task.subtasks.length).toBe(2);
});
it('should handle expansion with research flag', async () => {
// Skip if no API key is set
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.PERPLEXITY_API_KEY) {
console.log('Skipping test: No AI API key found in environment');
return;
}
const { stdout } = await runMCPCommand('tools/call', {
toolName: 'expand_task',
toolArgs: {
id: '1',
projectRoot: testProjectPath,
research: 'true',
num: '2'
}
});
const response = JSON.parse(stdout);
const textContent = response.content.find(c => c.type === 'text');
// Even if research fails, expansion should still work
const result = JSON.parse(textContent.text);
expect(result.task).toBeDefined();
expect(result.subtasksAdded).toBeGreaterThanOrEqual(0);
});
});

View File

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

View File

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

View File

@@ -1,186 +1,325 @@
/**
* Multi-provider functionality test module
* Tests add-task operation across all configured providers
*/
const { exec } = require('child_process');
const { promisify } = require('util');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
export default async function testProviders(logger, helpers, context) {
const { testDir, config } = context;
const results = {
status: 'passed',
errors: [],
providerComparison: {},
summary: {
totalProviders: 0,
successfulProviders: 0,
failedProviders: 0,
averageExecutionTime: 0,
successRate: '0%'
}
};
const execAsync = promisify(exec);
try {
logger.info('Starting multi-provider tests...');
const providers = config.providers;
const standardPrompt = config.prompts.addTask;
results.summary.totalProviders = providers.length;
let totalExecutionTime = 0;
// Process providers in batches to avoid rate limits
const batchSize = 3;
for (let i = 0; i < providers.length; i += batchSize) {
const batch = providers.slice(i, i + batchSize);
const batchPromises = batch.map(async (provider) => {
const providerResult = {
status: 'failed',
taskId: null,
executionTime: 0,
subtaskCount: 0,
features: {
hasTitle: false,
hasDescription: false,
hasSubtasks: false,
hasDependencies: false
},
error: null,
taskDetails: null
};
const startTime = Date.now();
try {
logger.info(
`\nTesting provider: ${provider.name} with model: ${provider.model}`
);
// Step 1: Set the main model for this provider
logger.info(`Setting model to ${provider.model}...`);
const setModelResult = await helpers.taskMaster(
'models',
['--set-main', provider.model],
{ cwd: testDir }
);
if (setModelResult.exitCode !== 0) {
throw new Error(
`Failed to set model for ${provider.name}: ${setModelResult.stderr}`
);
}
// Step 2: Execute add-task with standard prompt
logger.info(`Adding task with ${provider.name}...`);
const addTaskArgs = ['--prompt', standardPrompt];
if (provider.flags && provider.flags.length > 0) {
addTaskArgs.push(...provider.flags);
}
const addTaskResult = await helpers.taskMaster(
'add-task',
addTaskArgs,
{
cwd: testDir,
timeout: 120000 // 2 minutes timeout for AI tasks
}
);
if (addTaskResult.exitCode !== 0) {
throw new Error(`Add-task failed: ${addTaskResult.stderr}`);
}
// Step 3: Extract task ID from output
const taskId = helpers.extractTaskId(addTaskResult.stdout);
if (!taskId) {
throw new Error(`Failed to extract task ID from output`);
}
providerResult.taskId = taskId;
logger.success(`✓ Created task ${taskId} with ${provider.name}`);
// Step 4: Get task details
const showResult = await helpers.taskMaster('show', [taskId], {
cwd: testDir
});
if (showResult.exitCode === 0) {
providerResult.taskDetails = showResult.stdout;
// Analyze task features
providerResult.features.hasTitle =
showResult.stdout.includes('Title:') ||
showResult.stdout.includes('Task:');
providerResult.features.hasDescription =
showResult.stdout.includes('Description:');
providerResult.features.hasSubtasks =
showResult.stdout.includes('Subtasks:');
providerResult.features.hasDependencies =
showResult.stdout.includes('Dependencies:');
// Count subtasks
const subtaskMatches = showResult.stdout.match(/\d+\.\d+/g);
providerResult.subtaskCount = subtaskMatches
? subtaskMatches.length
: 0;
}
providerResult.status = 'success';
results.summary.successfulProviders++;
} catch (error) {
providerResult.status = 'failed';
providerResult.error = error.message;
results.summary.failedProviders++;
logger.error(`${provider.name} test failed: ${error.message}`);
}
providerResult.executionTime = Date.now() - startTime;
totalExecutionTime += providerResult.executionTime;
results.providerComparison[provider.name] = providerResult;
});
// Wait for batch to complete
await Promise.all(batchPromises);
// Small delay between batches to avoid rate limits
if (i + batchSize < providers.length) {
logger.info('Waiting 2 seconds before next batch...');
await helpers.wait(2000);
}
}
// Calculate summary statistics
results.summary.averageExecutionTime = Math.round(
totalExecutionTime / providers.length
);
results.summary.successRate = `${Math.round((results.summary.successfulProviders / results.summary.totalProviders) * 100)}%`;
// Log summary
logger.info('\n=== Provider Test Summary ===');
logger.info(`Total providers tested: ${results.summary.totalProviders}`);
logger.info(`Successful: ${results.summary.successfulProviders}`);
logger.info(`Failed: ${results.summary.failedProviders}`);
logger.info(`Success rate: ${results.summary.successRate}`);
logger.info(
`Average execution time: ${results.summary.averageExecutionTime}ms`
);
// Determine overall status
if (results.summary.failedProviders === 0) {
logger.success('✅ All provider tests passed!');
} else if (results.summary.successfulProviders > 0) {
results.status = 'partial';
logger.warning(`⚠️ ${results.summary.failedProviders} provider(s) failed`);
} else {
results.status = 'failed';
logger.error('❌ All provider tests failed');
}
} catch (error) {
results.status = 'failed';
results.errors.push({
test: 'provider tests',
error: error.message,
stack: error.stack
});
logger.error(`Provider tests failed: ${error.message}`);
}
return results;
// Helper function to run task-master commands
async function runTaskMaster(args, options = {}) {
const taskMasterPath = path.join(__dirname, '../../../scripts/task-master.js');
const command = `node ${taskMasterPath} ${args.join(' ')}`;
try {
const { stdout, stderr } = await execAsync(command, {
cwd: options.cwd || process.cwd(),
timeout: options.timeout || 30000,
env: { ...process.env, NODE_ENV: 'test' }
});
return {
exitCode: 0,
stdout: stdout.trim(),
stderr: stderr.trim()
};
} catch (error) {
return {
exitCode: error.code || 1,
stdout: (error.stdout || '').trim(),
stderr: (error.stderr || error.message || '').trim()
};
}
}
// Helper to extract task ID from output
function extractTaskId(output) {
const idMatch = output.match(/Task #?(\d+(?:\.\d+)?)/i);
return idMatch ? idMatch[1] : null;
}
// Helper function to wait
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Test configuration
const testConfig = {
providers: [
{ name: 'OpenAI GPT-4', model: 'openai:gpt-4', flags: [] },
{ name: 'OpenAI GPT-3.5', model: 'openai:gpt-3.5-turbo', flags: [] },
{ name: 'Anthropic Claude 3 Opus', model: 'anthropic:claude-3-opus-20240229', flags: [] },
{ name: 'Anthropic Claude 3 Sonnet', model: 'anthropic:claude-3-sonnet-20240229', flags: [] },
{ name: 'Anthropic Claude 3 Haiku', model: 'anthropic:claude-3-haiku-20240307', flags: [] },
{ name: 'Google Gemini Pro', model: 'google:gemini-pro', flags: [] },
{ name: 'Groq Llama 3 70B', model: 'groq:llama3-70b-8192', flags: [] },
{ name: 'Groq Mixtral', model: 'groq:mixtral-8x7b-32768', flags: [] }
],
prompts: {
addTask: 'Create a comprehensive plan to build a task management CLI application with file-based storage and AI integration'
}
};
describe('Multi-Provider Functionality Tests', () => {
let testDir;
beforeAll(async () => {
// Create temporary test directory
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-master-provider-test-'));
// Initialize task-master in test directory
const initResult = await runTaskMaster(['init'], { cwd: testDir });
if (initResult.exitCode !== 0) {
throw new Error(`Failed to initialize task-master: ${initResult.stderr}`);
}
});
afterAll(async () => {
// Clean up test directory
if (testDir) {
await fs.rm(testDir, { recursive: true, force: true });
}
});
// Check if any AI API keys are available
const hasAIKeys = !!(
process.env.OPENAI_API_KEY ||
process.env.ANTHROPIC_API_KEY ||
process.env.GOOGLE_API_KEY ||
process.env.GROQ_API_KEY
);
const testCondition = hasAIKeys ? it : it.skip;
testCondition('should test add-task across multiple AI providers', async () => {
const results = {
providerComparison: {},
summary: {
totalProviders: 0,
successfulProviders: 0,
failedProviders: 0,
averageExecutionTime: 0,
successRate: '0%'
}
};
// Filter providers based on available API keys
const availableProviders = testConfig.providers.filter(provider => {
if (provider.model.startsWith('openai:') && !process.env.OPENAI_API_KEY) return false;
if (provider.model.startsWith('anthropic:') && !process.env.ANTHROPIC_API_KEY) return false;
if (provider.model.startsWith('google:') && !process.env.GOOGLE_API_KEY) return false;
if (provider.model.startsWith('groq:') && !process.env.GROQ_API_KEY) return false;
return true;
});
results.summary.totalProviders = availableProviders.length;
let totalExecutionTime = 0;
// Process providers in batches to avoid rate limits
const batchSize = 3;
for (let i = 0; i < availableProviders.length; i += batchSize) {
const batch = availableProviders.slice(i, i + batchSize);
const batchPromises = batch.map(async (provider) => {
const providerResult = {
status: 'failed',
taskId: null,
executionTime: 0,
subtaskCount: 0,
features: {
hasTitle: false,
hasDescription: false,
hasSubtasks: false,
hasDependencies: false
},
error: null,
taskDetails: null
};
const startTime = Date.now();
try {
console.log(`\nTesting provider: ${provider.name} with model: ${provider.model}`);
// Step 1: Set the main model for this provider
console.log(`Setting model to ${provider.model}...`);
const setModelResult = await runTaskMaster(
['models', '--set-main', provider.model],
{ cwd: testDir }
);
expect(setModelResult.exitCode).toBe(0);
// Step 2: Execute add-task with standard prompt
console.log(`Adding task with ${provider.name}...`);
const addTaskArgs = ['add-task', '--prompt', testConfig.prompts.addTask];
if (provider.flags && provider.flags.length > 0) {
addTaskArgs.push(...provider.flags);
}
const addTaskResult = await runTaskMaster(addTaskArgs, {
cwd: testDir,
timeout: 120000 // 2 minutes timeout for AI tasks
});
expect(addTaskResult.exitCode).toBe(0);
// Step 3: Extract task ID from output
const taskId = extractTaskId(addTaskResult.stdout);
expect(taskId).toBeTruthy();
providerResult.taskId = taskId;
console.log(`✓ Created task ${taskId} with ${provider.name}`);
// Step 4: Get task details
const showResult = await runTaskMaster(['show', taskId], { cwd: testDir });
expect(showResult.exitCode).toBe(0);
providerResult.taskDetails = showResult.stdout;
// Analyze task features
providerResult.features.hasTitle =
showResult.stdout.includes('Title:') ||
showResult.stdout.includes('Task:');
providerResult.features.hasDescription =
showResult.stdout.includes('Description:');
providerResult.features.hasSubtasks =
showResult.stdout.includes('Subtasks:');
providerResult.features.hasDependencies =
showResult.stdout.includes('Dependencies:');
// Count subtasks
const subtaskMatches = showResult.stdout.match(/\d+\.\d+/g);
providerResult.subtaskCount = subtaskMatches ? subtaskMatches.length : 0;
providerResult.status = 'success';
results.summary.successfulProviders++;
} catch (error) {
providerResult.status = 'failed';
providerResult.error = error.message;
results.summary.failedProviders++;
console.error(`${provider.name} test failed: ${error.message}`);
}
providerResult.executionTime = Date.now() - startTime;
totalExecutionTime += providerResult.executionTime;
results.providerComparison[provider.name] = providerResult;
});
// Wait for batch to complete
await Promise.all(batchPromises);
// Small delay between batches to avoid rate limits
if (i + batchSize < availableProviders.length) {
console.log('Waiting 2 seconds before next batch...');
await wait(2000);
}
}
// Calculate summary statistics
results.summary.averageExecutionTime = Math.round(
totalExecutionTime / availableProviders.length
);
results.summary.successRate = `${Math.round(
(results.summary.successfulProviders / results.summary.totalProviders) * 100
)}%`;
// Log summary
console.log('\n=== Provider Test Summary ===');
console.log(`Total providers tested: ${results.summary.totalProviders}`);
console.log(`Successful: ${results.summary.successfulProviders}`);
console.log(`Failed: ${results.summary.failedProviders}`);
console.log(`Success rate: ${results.summary.successRate}`);
console.log(`Average execution time: ${results.summary.averageExecutionTime}ms`);
// Log provider comparison details
console.log('\n=== Provider Feature Comparison ===');
Object.entries(results.providerComparison).forEach(([providerName, result]) => {
console.log(`\n${providerName}:`);
console.log(` Status: ${result.status}`);
console.log(` Task ID: ${result.taskId || 'N/A'}`);
console.log(` Execution Time: ${result.executionTime}ms`);
console.log(` Subtask Count: ${result.subtaskCount}`);
console.log(` Features:`);
console.log(` - Has Title: ${result.features.hasTitle}`);
console.log(` - Has Description: ${result.features.hasDescription}`);
console.log(` - Has Subtasks: ${result.features.hasSubtasks}`);
console.log(` - Has Dependencies: ${result.features.hasDependencies}`);
if (result.error) {
console.log(` Error: ${result.error}`);
}
});
// Assertions
expect(results.summary.successfulProviders).toBeGreaterThan(0);
expect(results.summary.successRate).not.toBe('0%');
}, 300000); // 5 minute timeout for entire test
testCondition('should maintain task quality across different providers', async () => {
const standardPrompt = 'Create a simple todo list feature with add, remove, and list functionality';
const providerResults = [];
// Test a subset of providers to check quality consistency
const testProviders = [
{ name: 'OpenAI GPT-4', model: 'openai:gpt-4' },
{ name: 'Anthropic Claude 3 Sonnet', model: 'anthropic:claude-3-sonnet-20240229' }
].filter(provider => {
if (provider.model.startsWith('openai:') && !process.env.OPENAI_API_KEY) return false;
if (provider.model.startsWith('anthropic:') && !process.env.ANTHROPIC_API_KEY) return false;
return true;
});
for (const provider of testProviders) {
console.log(`\nTesting quality with ${provider.name}...`);
// Set model
const setModelResult = await runTaskMaster(
['models', '--set-main', provider.model],
{ cwd: testDir }
);
expect(setModelResult.exitCode).toBe(0);
// Add task
const addTaskResult = await runTaskMaster(
['add-task', '--prompt', standardPrompt],
{ cwd: testDir, timeout: 60000 }
);
expect(addTaskResult.exitCode).toBe(0);
const taskId = extractTaskId(addTaskResult.stdout);
expect(taskId).toBeTruthy();
// Get task details
const showResult = await runTaskMaster(['show', taskId], { cwd: testDir });
expect(showResult.exitCode).toBe(0);
// Analyze quality metrics
const subtaskCount = (showResult.stdout.match(/\d+\.\d+/g) || []).length;
const hasDescription = showResult.stdout.includes('Description:');
const wordCount = showResult.stdout.split(/\s+/).length;
providerResults.push({
provider: provider.name,
taskId,
subtaskCount,
hasDescription,
wordCount
});
}
// Compare quality metrics
console.log('\n=== Quality Comparison ===');
providerResults.forEach(result => {
console.log(`\n${result.provider}:`);
console.log(` Subtasks: ${result.subtaskCount}`);
console.log(` Has Description: ${result.hasDescription}`);
console.log(` Word Count: ${result.wordCount}`);
});
// Basic quality assertions
providerResults.forEach(result => {
expect(result.subtaskCount).toBeGreaterThan(0);
expect(result.hasDescription).toBe(true);
expect(result.wordCount).toBeGreaterThan(50); // Reasonable task detail
});
}, 180000); // 3 minute timeout
});

View File

@@ -1,295 +0,0 @@
import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { testConfig } from '../config/test-config.js';
/**
* Setup test module that handles initialization, PRD parsing, and complexity analysis
* @param {Object} logger - TestLogger instance
* @param {Object} helpers - TestHelpers instance
* @returns {Promise<Object>} Test results object with status and directory path
*/
export async function runSetupTest(logger, helpers) {
const testResults = {
status: 'pending',
testDir: null,
steps: {
createDirectory: false,
linkGlobally: false,
copyEnv: false,
initialize: false,
parsePrd: false,
analyzeComplexity: false,
generateReport: false
},
errors: [],
prdPath: null,
complexityReport: null
};
try {
// Step 1: Create test directory with timestamp
logger.step('Creating test directory');
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, '')
.replace('T', '_')
.slice(0, -1);
const testDir = join(testConfig.paths.baseTestDir, `run_${timestamp}`);
if (!existsSync(testDir)) {
mkdirSync(testDir, { recursive: true });
}
testResults.testDir = testDir;
testResults.steps.createDirectory = true;
logger.success(`Test directory created: ${testDir}`);
// Step 2: Link task-master globally
logger.step('Linking task-master globally');
const linkResult = await helpers.executeCommand('npm', ['link'], {
cwd: testConfig.paths.projectRoot,
timeout: 60000
});
if (linkResult.exitCode === 0) {
testResults.steps.linkGlobally = true;
logger.success('Task-master linked globally');
} else {
throw new Error(`Failed to link task-master: ${linkResult.stderr}`);
}
// Step 3: Copy .env file
logger.step('Copying .env file to test directory');
const envSourcePath = testConfig.paths.mainEnvFile;
const envDestPath = join(testDir, '.env');
if (helpers.fileExists(envSourcePath)) {
if (helpers.copyFile(envSourcePath, envDestPath)) {
testResults.steps.copyEnv = true;
logger.success('.env file copied successfully');
} else {
throw new Error('Failed to copy .env file');
}
} else {
logger.warning('.env file not found at source, proceeding without it');
}
// Step 4: Initialize project with task-master init
logger.step('Initializing project with task-master');
const initResult = await helpers.taskMaster(
'init',
[
'-y',
'--name="E2E Test ' + testDir.split('/').pop() + '"',
'--description="Automated E2E test run"'
],
{
cwd: testDir,
timeout: 120000
}
);
if (initResult.exitCode === 0) {
testResults.steps.initialize = true;
logger.success('Project initialized successfully');
// Save init debug log if available
const initDebugPath = join(testDir, 'init-debug.log');
if (existsSync(initDebugPath)) {
logger.info('Init debug log saved');
}
} else {
throw new Error(`Initialization failed: ${initResult.stderr}`);
}
// Step 5: Parse PRD from sample file
logger.step('Parsing PRD from sample file');
// First, copy the sample PRD to the test directory
const prdSourcePath = testConfig.paths.samplePrdSource;
const prdDestPath = join(testDir, 'prd.txt');
testResults.prdPath = prdDestPath;
if (!helpers.fileExists(prdSourcePath)) {
// If sample PRD doesn't exist in fixtures, use the example PRD
const examplePrdPath = join(
testConfig.paths.projectRoot,
'assets/example_prd.txt'
);
if (helpers.fileExists(examplePrdPath)) {
helpers.copyFile(examplePrdPath, prdDestPath);
logger.info('Using example PRD file');
} else {
// Create a minimal PRD for testing
const minimalPrd = `<PRD>
# Overview
A simple task management system for developers.
# Core Features
- Task creation and management
- Task dependencies
- Status tracking
- Task prioritization
# Technical Architecture
- Node.js backend
- REST API
- JSON data storage
- CLI interface
# Development Roadmap
Phase 1: Core functionality
- Initialize project structure
- Implement task CRUD operations
- Add dependency management
Phase 2: Enhanced features
- Add task prioritization
- Implement search functionality
- Add export capabilities
# Logical Dependency Chain
1. Project setup and initialization
2. Core data models
3. Basic CRUD operations
4. Dependency system
5. CLI interface
6. Advanced features
</PRD>`;
writeFileSync(prdDestPath, minimalPrd);
logger.info('Created minimal PRD for testing');
}
} else {
helpers.copyFile(prdSourcePath, prdDestPath);
}
// Parse the PRD
const parsePrdResult = await helpers.taskMaster('parse-prd', ['prd.txt'], {
cwd: testDir,
timeout: 180000
});
if (parsePrdResult.exitCode === 0) {
testResults.steps.parsePrd = true;
logger.success('PRD parsed successfully');
// Extract task count from output
const taskCountMatch =
parsePrdResult.stdout.match(/(\d+) tasks? created/i);
if (taskCountMatch) {
logger.info(`Created ${taskCountMatch[1]} tasks from PRD`);
}
} else {
throw new Error(`PRD parsing failed: ${parsePrdResult.stderr}`);
}
// Step 6: Run complexity analysis
logger.step('Running complexity analysis on parsed tasks');
// Ensure reports directory exists
const reportsDir = join(testDir, '.taskmaster/reports');
if (!existsSync(reportsDir)) {
mkdirSync(reportsDir, { recursive: true });
}
const analyzeResult = await helpers.taskMaster(
'analyze-complexity',
[
'--research',
'--output',
'.taskmaster/reports/task-complexity-report.json'
],
{
cwd: testDir,
timeout: 240000
}
);
if (analyzeResult.exitCode === 0) {
testResults.steps.analyzeComplexity = true;
logger.success('Complexity analysis completed');
// Extract complexity information from output
const complexityMatch = analyzeResult.stdout.match(
/Total Complexity Score: ([\d.]+)/
);
if (complexityMatch) {
logger.info(`Total complexity score: ${complexityMatch[1]}`);
}
} else {
throw new Error(`Complexity analysis failed: ${analyzeResult.stderr}`);
}
// Step 7: Generate complexity report
logger.step('Generating complexity report');
const reportResult = await helpers.taskMaster('complexity-report', [], {
cwd: testDir,
timeout: 60000
});
if (reportResult.exitCode === 0) {
testResults.steps.generateReport = true;
logger.success('Complexity report generated');
// Check if complexity report file was created (not needed since complexity-report reads from the standard location)
const reportPath = join(
testDir,
'.taskmaster/reports/task-complexity-report.json'
);
if (helpers.fileExists(reportPath)) {
testResults.complexityReport = helpers.readJson(reportPath);
logger.info('Complexity report saved to task-complexity-report.json');
// Log summary if available
if (
testResults.complexityReport &&
testResults.complexityReport.summary
) {
const summary = testResults.complexityReport.summary;
logger.info(`Tasks analyzed: ${summary.totalTasks || 0}`);
logger.info(`Average complexity: ${summary.averageComplexity || 0}`);
}
}
} else {
logger.warning(
`Complexity report generation had issues: ${reportResult.stderr}`
);
// Don't fail the test for report generation issues
testResults.steps.generateReport = true;
}
// Verify tasks.json was created
const tasksJsonPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (helpers.fileExists(tasksJsonPath)) {
const taskCount = helpers.getTaskCount(tasksJsonPath);
logger.info(`Verified tasks.json exists with ${taskCount} tasks`);
} else {
throw new Error('tasks.json was not created');
}
// All steps completed successfully
testResults.status = 'success';
logger.success('Setup test completed successfully');
} catch (error) {
testResults.status = 'failed';
testResults.errors.push(error.message);
logger.error(`Setup test failed: ${error.message}`);
// Log which steps completed
logger.info('Completed steps:');
Object.entries(testResults.steps).forEach(([step, completed]) => {
if (completed) {
logger.info(`${step}`);
} else {
logger.info(`${step}`);
}
});
}
// Flush logs before returning
logger.flush();
return testResults;
}
// Export default for direct execution
export default runSetupTest;