wip: e2e tests improvements
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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
594
package-lock.json
generated
@@ -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",
|
||||
|
||||
10
package.json
10
package.json
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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`
|
||||
};
|
||||
}
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
284
tests/e2e/tests/mcp/expand-task-cli.test.js
Normal file
284
tests/e2e/tests/mcp/expand-task-cli.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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
|
||||
});
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user