diff --git a/extensions/vscode/justfile b/extensions/vscode/justfile index 25a27a6cb..542e096fc 100644 --- a/extensions/vscode/justfile +++ b/extensions/vscode/justfile @@ -213,7 +213,7 @@ run *args: npm run {{ args }} -# Executes npm test +# Executes npm test and npm run test-unit test: #!/usr/bin/env bash set -eou pipefail @@ -225,6 +225,8 @@ test: npm test fi + npm run test-unit + uninstall editor="code": #!/usr/bin/env bash diff --git a/extensions/vscode/package-lock.json b/extensions/vscode/package-lock.json index feaca229e..4c244d8a0 100644 --- a/extensions/vscode/package-lock.json +++ b/extensions/vscode/package-lock.json @@ -39,7 +39,8 @@ "eslint-config-prettier": "^9.1.0", "glob": "^10.3.3", "mocha": "^10.2.0", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "vitest": "^2.1.1" }, "engines": { "vscode": "^1.87.0" @@ -603,6 +604,12 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -675,6 +682,214 @@ "node": ">=14" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz", + "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz", + "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz", + "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz", + "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz", + "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz", + "integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz", + "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz", + "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz", + "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz", + "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz", + "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz", + "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz", + "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz", + "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz", + "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz", + "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -762,6 +977,12 @@ "node": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, "node_modules/@types/eventsource": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", @@ -1001,6 +1222,113 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vitest/expect": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "dev": true, + "dependencies": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.1", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", + "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.1", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vscode/codicons": { "version": "0.0.36", "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.36.tgz", @@ -1206,6 +1534,15 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/async-mutex": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", @@ -1379,6 +1716,15 @@ "node": ">= 6" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "16.1.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", @@ -1491,6 +1837,22 @@ "node": ">= 0.8.0" } }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1507,6 +1869,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1752,6 +2123,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2080,6 +2460,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2429,6 +2818,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-port": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", @@ -2971,12 +3369,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/make-fetch-happen": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", @@ -3423,6 +3839,24 @@ "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3827,10 +4261,25 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true }, "node_modules/picomatch": { @@ -3845,6 +4294,34 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4080,6 +4557,47 @@ "node": "*" } }, + "node_modules/rollup": { + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz", + "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.3", + "@rollup/rollup-android-arm64": "4.21.3", + "@rollup/rollup-darwin-arm64": "4.21.3", + "@rollup/rollup-darwin-x64": "4.21.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.3", + "@rollup/rollup-linux-arm-musleabihf": "4.21.3", + "@rollup/rollup-linux-arm64-gnu": "4.21.3", + "@rollup/rollup-linux-arm64-musl": "4.21.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3", + "@rollup/rollup-linux-riscv64-gnu": "4.21.3", + "@rollup/rollup-linux-s390x-gnu": "4.21.3", + "@rollup/rollup-linux-x64-gnu": "4.21.3", + "@rollup/rollup-linux-x64-musl": "4.21.3", + "@rollup/rollup-win32-arm64-msvc": "4.21.3", + "@rollup/rollup-win32-ia32-msvc": "4.21.3", + "@rollup/rollup-win32-x64-msvc": "4.21.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4176,6 +4694,12 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4265,6 +4789,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -4295,6 +4828,18 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, "node_modules/stdin-discarder": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", @@ -4559,6 +5104,45 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4728,6 +5312,150 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/vite": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.1", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/vscode-uri": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", @@ -4748,6 +5476,22 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index e75cd569a..7633ac673 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -494,7 +494,8 @@ "build-font": "fantasticon", "lint": "eslint src --ext ts --max-warnings 0", "compile": "tsc -p ./", - "test": "npm run compile && node ./out/test/runTest.js" + "test": "npm run compile && node ./out/test/runTest.js", + "test-unit": "vitest run" }, "devDependencies": { "@twbs/fantasticon": "^3.0.0", @@ -513,7 +514,8 @@ "eslint-config-prettier": "^9.1.0", "glob": "^10.3.3", "mocha": "^10.2.0", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "vitest": "^2.1.1" }, "dependencies": { "@hypersphere/omnibus": "0.1.6", diff --git a/extensions/vscode/src/api/resources/Files.ts b/extensions/vscode/src/api/resources/Files.ts index 098587855..32dd8f8a6 100644 --- a/extensions/vscode/src/api/resources/Files.ts +++ b/extensions/vscode/src/api/resources/Files.ts @@ -22,6 +22,7 @@ export class Files { // Returns: // 200 - success + // 400 - configuration is invalid // 404 - configuration does not exist // 422 - configuration files list contains invalid patterns // 500 - internal server error diff --git a/extensions/vscode/src/utils/errorTypes.test.ts b/extensions/vscode/src/utils/errorTypes.test.ts new file mode 100644 index 000000000..60208e9e6 --- /dev/null +++ b/extensions/vscode/src/utils/errorTypes.test.ts @@ -0,0 +1,234 @@ +// Copyright (C) 2024 by Posit Software, PBC. + +import { describe, expect, test } from "vitest"; +import { AxiosError, AxiosHeaders } from "axios"; +import { + axiosErrorWithJson, + ErrUnknown, + isErrUnknown, + errUnknownMessage, + isErrResourceNotFound, + ErrInvalidTOMLFile, + isErrInvalidTOMLFile, + errInvalidTOMLMessage, + ErrUnknownTOMLKey, + isErrUnknownTOMLKey, + errUnknownTOMLKeyMessage, + isErrInvalidConfigFile, + resolveAgentJsonErrorMsg, +} from "./errorTypes"; + +const mkAxiosJsonErr = (data: Record) => { + return new AxiosError(undefined, undefined, undefined, undefined, { + status: 0, + statusText: "", + headers: new AxiosHeaders(), + config: { headers: new AxiosHeaders() }, + data, + }); +}; + +describe("ErrUnknown", () => { + test("isErrUnknown", () => { + let result = isErrUnknown( + mkAxiosJsonErr({ + code: "unknown", + }), + ); + + expect(result).toBe(true); + + result = isErrUnknown( + mkAxiosJsonErr({ + code: "bricks_raining", + }), + ); + + expect(result).toBe(false); + }); + + test("errUnknownMessage", () => { + const err = mkAxiosJsonErr({ + code: "unknown", + details: { + error: "oh nooo!", + data: { + trace: "a > b > c > d", + }, + }, + }); + + const msg = errUnknownMessage(err as axiosErrorWithJson); + expect(msg).toBe( + "Unknown publisher agent error: oh nooo!, trace=a > b > c > d", + ); + }); +}); + +describe("ErrResourceNotFound", () => { + test("isErrResourceNotFound", () => { + let result = isErrResourceNotFound( + mkAxiosJsonErr({ + code: "resourceNotFound", + }), + ); + + expect(result).toBe(true); + + result = isErrResourceNotFound( + mkAxiosJsonErr({ + code: "bricks_raining", + }), + ); + + expect(result).toBe(false); + }); +}); + +describe("ErrInvalidTOMLFile", () => { + test("isErrInvalidTOMLFile", () => { + let result = isErrInvalidTOMLFile( + mkAxiosJsonErr({ + code: "invalidTOML", + }), + ); + + expect(result).toBe(true); + + result = isErrInvalidTOMLFile( + mkAxiosJsonErr({ + code: "bricks_raining", + }), + ); + + expect(result).toBe(false); + }); + + test("errInvalidTOMLMessage", () => { + const err = mkAxiosJsonErr({ + code: "invalidTOML", + details: { + filename: "/directory/configuration-lkdg.toml", + line: 5, + column: 5, + }, + }); + + const msg = errInvalidTOMLMessage( + err as axiosErrorWithJson, + ); + expect(msg).toBe( + "Invalid TOML file /directory/configuration-lkdg.toml:5:5", + ); + }); +}); + +describe("ErrUnknownTOMLKey", () => { + test("isErrUnknownTOMLKey", () => { + let result = isErrUnknownTOMLKey( + mkAxiosJsonErr({ + code: "unknownTOMLKey", + }), + ); + + expect(result).toBe(true); + + result = isErrUnknownTOMLKey( + mkAxiosJsonErr({ + code: "bricks_raining", + }), + ); + + expect(result).toBe(false); + }); + + test("errUnknownTOMLKeyMessage", () => { + const err = mkAxiosJsonErr({ + code: "unknownTOMLKey", + details: { + filename: "/directory/configuration-lkdg.toml", + line: 7, + column: 1, + key: "shortcut_key", + }, + }); + + const msg = errUnknownTOMLKeyMessage( + err as axiosErrorWithJson, + ); + expect(msg).toBe( + `Unknown field present in configuration file /directory/configuration-lkdg.toml:7:1 - unknown key "shortcut_key"`, + ); + }); +}); + +describe("ErrInvalidConfigFiles", () => { + test("isErrInvalidConfigFile", () => { + let result = isErrInvalidConfigFile( + mkAxiosJsonErr({ + code: "invalidConfigFile", + }), + ); + + expect(result).toBe(true); + + result = isErrInvalidConfigFile( + mkAxiosJsonErr({ + code: "bricks_raining", + }), + ); + + expect(result).toBe(false); + }); +}); + +describe("resolveAgentJsonErrorMsg", () => { + test("returns proper message based on the provided error", () => { + let msg = resolveAgentJsonErrorMsg( + mkAxiosJsonErr({ + code: "unknown", + details: { + error: "oh nooo!", + data: { + trace: "a > b > c > d", + }, + }, + }) as axiosErrorWithJson, + ); + + expect(msg).toBe( + "Unknown publisher agent error: oh nooo!, trace=a > b > c > d", + ); + + msg = resolveAgentJsonErrorMsg( + mkAxiosJsonErr({ + code: "invalidTOML", + details: { + filename: "/directory/configuration-lkdg.toml", + line: 5, + column: 5, + }, + }) as axiosErrorWithJson, + ); + + expect(msg).toBe( + "Invalid TOML file /directory/configuration-lkdg.toml:5:5", + ); + + msg = resolveAgentJsonErrorMsg( + mkAxiosJsonErr({ + code: "unknownTOMLKey", + details: { + filename: "/directory/configuration-lkdg.toml", + line: 7, + column: 1, + key: "shortcut_key", + }, + }) as axiosErrorWithJson, + ); + + expect(msg).toBe( + `Unknown field present in configuration file /directory/configuration-lkdg.toml:7:1 - unknown key "shortcut_key"`, + ); + }); +}); diff --git a/extensions/vscode/src/utils/errorTypes.ts b/extensions/vscode/src/utils/errorTypes.ts new file mode 100644 index 000000000..df3e9f5fd --- /dev/null +++ b/extensions/vscode/src/utils/errorTypes.ts @@ -0,0 +1,124 @@ +// Copyright (C) 2024 by Posit Software, PBC. + +import { AxiosError, AxiosResponse, isAxiosError } from "axios"; + +type ErrorCode = + | "unknown" + | "resourceNotFound" + | "invalidTOML" + | "unknownTOMLKey" + | "invalidConfigFile"; + +export type axiosErrorWithJson = + AxiosError & { + response: AxiosResponse; + }; + +export const isAxiosErrorWithJson = ( + err: unknown, +): err is axiosErrorWithJson => { + if (isAxiosError(err)) { + return err.response?.data && err.response.data.code; + } + return false; +}; + +const isOfErrorType = (err: axiosErrorWithJson, code: ErrorCode): boolean => { + return err.response.data.code === code; +}; + +type MkErrorDataType> = { + code: T; + details: D; +}; + +const mkErrorTypeGuard = (code: ErrorCode) => { + return (err: unknown): err is axiosErrorWithJson => { + return isAxiosErrorWithJson(err) && isOfErrorType(err, code); + }; +}; + +// Unknown Errors +export type ErrUnknown = MkErrorDataType< + "unknown", + { + error: string; + data: Record; + } +>; +export const isErrUnknown = mkErrorTypeGuard("unknown"); +export const errUnknownMessage = (err: axiosErrorWithJson) => { + const { error, data } = err.response.data.details; + let msg = `Unknown publisher agent error: ${error}`; + Object.keys(data).forEach((key) => { + msg += `, ${key}=${data[key]}`; + }); + return msg; +}; + +// Resource not found +export type ErrResourceNotFound = MkErrorDataType< + "resourceNotFound", + { resource: string } +>; +export const isErrResourceNotFound = + mkErrorTypeGuard("resourceNotFound"); + +// Invalid TOML file(s) +export type ErrInvalidTOMLFile = MkErrorDataType< + "invalidTOML", + { + filename: string; + line: number; + column: number; + } +>; +export const isErrInvalidTOMLFile = + mkErrorTypeGuard("invalidTOML"); +export const errInvalidTOMLMessage = ( + err: axiosErrorWithJson, +) => { + const { filename, line, column } = err.response.data.details; + return `Invalid TOML file ${filename}:${line}:${column}`; +}; + +// Unknown key within a TOML file +export type ErrUnknownTOMLKey = MkErrorDataType< + "unknownTOMLKey", + { + filename: string; + line: number; + column: number; + key: string; + } +>; +export const isErrUnknownTOMLKey = + mkErrorTypeGuard("unknownTOMLKey"); +export const errUnknownTOMLKeyMessage = ( + err: axiosErrorWithJson, +) => { + const { filename, line, column, key } = err.response.data.details; + return `Unknown field present in configuration file ${filename}:${line}:${column} - unknown key "${key}"`; +}; + +// Invalid configuration file(s) +export type ErrInvalidConfigFiles = MkErrorDataType< + "invalidConfigFile", + { filename: string } +>; +export const isErrInvalidConfigFile = + mkErrorTypeGuard("invalidConfigFile"); + +// Tries to match an Axios error that comes with an identifiable Json structured data +// defaulting to be ErrUnknown message when +export function resolveAgentJsonErrorMsg(err: axiosErrorWithJson) { + if (isErrUnknownTOMLKey(err)) { + return errUnknownTOMLKeyMessage(err); + } + + if (isErrInvalidTOMLFile(err)) { + return errInvalidTOMLMessage(err); + } + + return errUnknownMessage(err as axiosErrorWithJson); +} diff --git a/extensions/vscode/src/utils/errors.test.ts b/extensions/vscode/src/utils/errors.test.ts new file mode 100644 index 000000000..8da7c2759 --- /dev/null +++ b/extensions/vscode/src/utils/errors.test.ts @@ -0,0 +1,95 @@ +// Copyright (C) 2024 by Posit Software, PBC. + +import { describe, expect, test } from "vitest"; +import { AxiosError, AxiosHeaders } from "axios"; +import { getSummaryStringFromError } from "./errors"; + +const mkAxiosJsonErr = (data: Record) => { + return new AxiosError(undefined, undefined, undefined, undefined, { + status: 0, + statusText: "", + headers: new AxiosHeaders(), + config: { headers: new AxiosHeaders() }, + data, + }); +}; + +describe("getSummaryStringFromError", () => { + describe("known JSON errors", () => { + test("returns a user friendly message", () => { + // Just testing the summary for a couple errors, the whole error types messages matrix should be tested at errorTypes.test.ts + let summary = getSummaryStringFromError( + "callerMethodHere", + mkAxiosJsonErr({ + code: "invalidTOML", + details: { + filename: "/directory/configuration-lkdg.toml", + line: 5, + column: 5, + }, + }), + ); + + expect(summary).toBe( + "Invalid TOML file /directory/configuration-lkdg.toml:5:5", + ); + + summary = getSummaryStringFromError( + "callerMethodHere", + mkAxiosJsonErr({ + code: "unknownTOMLKey", + details: { + filename: "/directory/configuration-lkdg.toml", + line: 7, + column: 1, + key: "shortcut_key", + }, + }), + ); + + expect(summary).toBe( + `Unknown field present in configuration file /directory/configuration-lkdg.toml:7:1 - unknown key "shortcut_key"`, + ); + }); + }); + + describe("unknown or unregistered errors", () => { + test("returns a summary of available data", () => { + let summary = getSummaryStringFromError( + "callerMethodHere", + new AxiosError(undefined, undefined, undefined, undefined, { + status: 400, + statusText: "Bad Request", + headers: new AxiosHeaders(), + config: { headers: new AxiosHeaders(), baseURL: "localhost:9874" }, + data: undefined, + }), + ); + + expect(summary).toBe( + "An error has occurred at callerMethodHere, Status=400, StatusText=Bad Request", + ); + + summary = getSummaryStringFromError( + "callerMethodHere", + new AxiosError( + "Bricks are falling", + "CODE_WHOOPS", + undefined, + undefined, + { + status: 400, + statusText: "Bad Request", + headers: new AxiosHeaders(), + config: { headers: new AxiosHeaders(), baseURL: "localhost:9874" }, + data: undefined, + }, + ), + ); + + expect(summary).toBe( + "An error has occurred at callerMethodHere, Status=400, StatusText=Bad Request, Code=CODE_WHOOPS, Msg=Bricks are falling", + ); + }); + }); +}); diff --git a/extensions/vscode/src/utils/errors.ts b/extensions/vscode/src/utils/errors.ts index c7ec64980..e3646aab7 100644 --- a/extensions/vscode/src/utils/errors.ts +++ b/extensions/vscode/src/utils/errors.ts @@ -1,6 +1,7 @@ // Copyright (C) 2023 by Posit Software, PBC. import axios from "axios"; +import { isAxiosErrorWithJson, resolveAgentJsonErrorMsg } from "./errorTypes"; export type ErrorMessage = string[]; export type ErrorMessages = ErrorMessage[]; @@ -49,8 +50,15 @@ export const getAPIURLFromError = (error: unknown) => { return undefined; }; +// When the error is a known JSON agent error it returns it's message. +// Otherwise, a tracing message is returned to help diagnose. export const getSummaryStringFromError = (location: string, error: unknown) => { let msg = `An error has occurred at ${location}`; + + if (isAxiosErrorWithJson(error)) { + return resolveAgentJsonErrorMsg(error); + } + const summary = getSummaryFromError(error); if (summary) { if (summary.status) { diff --git a/extensions/vscode/tsconfig.json b/extensions/vscode/tsconfig.json index 4a88dfb8f..7eb8da47c 100644 --- a/extensions/vscode/tsconfig.json +++ b/extensions/vscode/tsconfig.json @@ -15,8 +15,10 @@ "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "paths": { - "src/*": ["./src/*"] + "src/*": ["./src/*"], + // workaround for: https://github.com/rollup/rollup/issues/5199 + "rollup/parseAst": ["./node_modules/rollup/dist/parseAst"] } }, - "exclude": ["node_modules", "webviews"] + "exclude": ["node_modules", "webviews", "vitest.config.ts"] } diff --git a/extensions/vscode/vitest.config.ts b/extensions/vscode/vitest.config.ts new file mode 100644 index 000000000..85e8c92a0 --- /dev/null +++ b/extensions/vscode/vitest.config.ts @@ -0,0 +1,11 @@ +/// +import { defineConfig } from "vite"; + +// Configuration to run unit tests under the ./src dir +// Excluding vscode extension tests suite +export default defineConfig({ + test: { + include: ["./src/**/*.{test,spec}.?(c|m)[jt]s?(x)"], + exclude: ["./src/test/**"], + }, +}); diff --git a/internal/publish/publish.go b/internal/publish/publish.go index bb100be74..e819f14ab 100644 --- a/internal/publish/publish.go +++ b/internal/publish/publish.go @@ -132,7 +132,7 @@ func (p *defaultPublisher) isDeployed() bool { func (p *defaultPublisher) emitErrorEvents(err error) { agentErr, ok := err.(*types.AgentError) if !ok { - agentErr = types.NewAgentError(types.UnknownErrorCode, err, nil) + agentErr = types.NewAgentError(types.ErrorUnknown, err, nil) } dashboardURL := "" directURL := "" diff --git a/internal/services/api/api_errors.go b/internal/services/api/api_errors.go new file mode 100644 index 000000000..cea58de6c --- /dev/null +++ b/internal/services/api/api_errors.go @@ -0,0 +1,67 @@ +package api + +// Copyright (C) 2024 by Posit Software, PBC. + +import ( + "net/http" + + "github.com/posit-dev/publisher/internal/types" +) + +type APIError interface { + JSONResponse(http.ResponseWriter) +} + +type UnknownTOMLKeyDetails struct { + Filename string `json:"filename"` + Line int `json:"line"` + Column int `json:"column"` + Key string `json:"key"` +} + +type APIErrorUnknownTOMLKeyDetails struct { + Code types.ErrorCode `json:"code"` + Details UnknownTOMLKeyDetails `json:"details"` +} + +func APIErrorUnknownTOMLKeyFromAgentError(aerr types.AgentError) APIErrorUnknownTOMLKeyDetails { + return APIErrorUnknownTOMLKeyDetails{ + Code: types.ErrorUnknownTOMLKey, + Details: UnknownTOMLKeyDetails{ + Filename: aerr.Data["file"].(string), + Key: aerr.Data["key"].(string), + Line: aerr.Data["line"].(int), + Column: aerr.Data["column"].(int), + }, + } +} + +func (apierr *APIErrorUnknownTOMLKeyDetails) JSONResponse(w http.ResponseWriter) { + JsonResult(w, http.StatusBadRequest, apierr) +} + +type InvalidTOMLFileDetails struct { + Filename string `json:"filename"` + Line int `json:"line"` + Column int `json:"column"` +} + +type APIErrorInvalidTOMLFileDetails struct { + Code types.ErrorCode `json:"code"` + Details InvalidTOMLFileDetails `json:"details"` +} + +func APIErrorInvalidTOMLFileFromAgentError(aerr types.AgentError) APIErrorInvalidTOMLFileDetails { + return APIErrorInvalidTOMLFileDetails{ + Code: types.ErrorInvalidTOML, + Details: InvalidTOMLFileDetails{ + Filename: aerr.Data["file"].(string), + Line: aerr.Data["line"].(int), + Column: aerr.Data["column"].(int), + }, + } +} + +func (apierr *APIErrorInvalidTOMLFileDetails) JSONResponse(w http.ResponseWriter) { + JsonResult(w, http.StatusBadRequest, apierr) +} diff --git a/internal/services/api/api_errors_test.go b/internal/services/api/api_errors_test.go new file mode 100644 index 000000000..e53d68aa0 --- /dev/null +++ b/internal/services/api/api_errors_test.go @@ -0,0 +1,71 @@ +package api + +// Copyright (C) 2024 by Posit Software, PBC. + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/posit-dev/publisher/internal/types" + "github.com/posit-dev/publisher/internal/util/utiltest" + "github.com/stretchr/testify/suite" +) + +type ApiErrorsSuite struct { + utiltest.Suite +} + +func TestApiErrorsSuite(t *testing.T) { + suite.Run(t, new(ApiErrorsSuite)) +} + +func (s *ApiErrorsSuite) TestAPIErrorUnknownTOMLKeyFromAgentError() { + agentErr := types.AgentError{ + Message: "Unknown field present in configuration file", + Code: types.ErrorUnknownTOMLKey, + Err: errors.New("unknown field error"), + Data: types.ErrorData{ + "file": "/project-a/configuration-avcd.toml", + "line": 3, + "column": 1, + "key": "shortcut", + }, + } + + rec := httptest.NewRecorder() + + apiError := APIErrorUnknownTOMLKeyFromAgentError(agentErr) + s.Equal(apiError.Code, types.ErrorUnknownTOMLKey) + + apiError.JSONResponse(rec) + + bodyRes := rec.Body.String() + s.Equal(http.StatusBadRequest, rec.Result().StatusCode) + s.Contains(bodyRes, `{"code":"unknownTOMLKey","details":{"filename":"/project-a/configuration-avcd.toml","line":3,"column":1,"key":"shortcut"}}`) +} + +func (s *ApiErrorsSuite) TestAPIErrorInvalidTOMLFileFromAgentError() { + agentErr := types.AgentError{ + Message: "Bad syntax", + Code: types.ErrorInvalidTOML, + Err: errors.New("unknown field error"), + Data: types.ErrorData{ + "file": "/project-a/configuration-avcd.toml", + "line": 3, + "column": 1, + }, + } + + rec := httptest.NewRecorder() + + apiError := APIErrorInvalidTOMLFileFromAgentError(agentErr) + s.Equal(apiError.Code, types.ErrorInvalidTOML) + + apiError.JSONResponse(rec) + + bodyRes := rec.Body.String() + s.Equal(http.StatusBadRequest, rec.Result().StatusCode) + s.Contains(bodyRes, `{"code":"invalidTOML","details":{"filename":"/project-a/configuration-avcd.toml","line":3,"column":1}}`) +} diff --git a/internal/services/api/api_helpers.go b/internal/services/api/api_helpers.go index f541908a4..27ed0d231 100644 --- a/internal/services/api/api_helpers.go +++ b/internal/services/api/api_helpers.go @@ -3,6 +3,7 @@ package api // Copyright (C) 2023 by Posit Software, PBC. import ( + "encoding/json" "errors" "fmt" "html" @@ -43,6 +44,12 @@ func NotFound(w http.ResponseWriter, log logging.Logger, err error) { http.Error(w, msg, http.StatusNotFound) } +func JsonResult(w http.ResponseWriter, status int, result any) { + w.Header().Set("content-type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(result) +} + var errProjectDirNotFound = errors.New("project directory not found") // ProjectDirFromRequest returns the project directory from the request query parameter "dir". diff --git a/internal/services/api/get_config_files.go b/internal/services/api/get_config_files.go index cefe1f6a5..78a9fbbf8 100644 --- a/internal/services/api/get_config_files.go +++ b/internal/services/api/get_config_files.go @@ -3,7 +3,6 @@ package api // Copyright (C) 2023 by Posit Software, PBC. import ( - "encoding/json" "errors" "io/fs" "net/http" @@ -13,9 +12,17 @@ import ( "github.com/posit-dev/publisher/internal/config" "github.com/posit-dev/publisher/internal/logging" "github.com/posit-dev/publisher/internal/services/api/files" + "github.com/posit-dev/publisher/internal/types" "github.com/posit-dev/publisher/internal/util" ) +type cfgFromFile func(path util.AbsolutePath) (*config.Config, error) +type cfgGetConfigPath func(base util.AbsolutePath, configName string) util.AbsolutePath + +// TODO: It would be better to have the config package methods as a provider pattern instead of plain functions +var configFromFile cfgFromFile = config.FromFile +var configGetConfigPath cfgGetConfigPath = config.GetConfigPath + func GetConfigFilesHandlerFunc(base util.AbsolutePath, filesService files.FilesService, log logging.Logger) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { name := mux.Vars(req)["name"] @@ -24,9 +31,23 @@ func GetConfigFilesHandlerFunc(base util.AbsolutePath, filesService files.FilesS // Response already returned by ProjectDirFromRequest return } - configPath := config.GetConfigPath(projectDir, name) - cfg, err := config.FromFile(configPath) + configPath := configGetConfigPath(projectDir, name) + cfg, err := configFromFile(configPath) if err != nil { + if aerr, ok := err.(*types.AgentError); ok { + if aerr.Code == types.ErrorUnknownTOMLKey { + apiErr := APIErrorUnknownTOMLKeyFromAgentError(*aerr) + apiErr.JSONResponse(w) + return + } + + if aerr.Code == types.ErrorInvalidTOML { + apiErr := APIErrorInvalidTOMLFileFromAgentError(*aerr) + apiErr.JSONResponse(w) + return + } + } + if errors.Is(err, fs.ErrNotExist) { http.NotFound(w, req) } else { @@ -52,7 +73,6 @@ func GetConfigFilesHandlerFunc(base util.AbsolutePath, filesService files.FilesS return } - w.Header().Set("content-type", "application/json") - json.NewEncoder(w).Encode(file) + JsonResult(w, http.StatusOK, file) } } diff --git a/internal/services/api/get_config_files_test.go b/internal/services/api/get_config_files_test.go index 429e6ccb9..352c779da 100644 --- a/internal/services/api/get_config_files_test.go +++ b/internal/services/api/get_config_files_test.go @@ -14,6 +14,7 @@ import ( "github.com/posit-dev/publisher/internal/config" "github.com/posit-dev/publisher/internal/logging" "github.com/posit-dev/publisher/internal/services/api/files" + "github.com/posit-dev/publisher/internal/types" "github.com/posit-dev/publisher/internal/util" "github.com/posit-dev/publisher/internal/util/utiltest" "github.com/spf13/afero" @@ -134,6 +135,101 @@ func (s *GetConfigFilesHandlerFuncSuite) TestHandlerFuncConfigNotFound() { s.Equal(http.StatusNotFound, rec.Result().StatusCode) } +func (s *GetConfigFilesHandlerFuncSuite) TestHandlerFuncConfigUnknownFields() { + // Mocking implementation config.FromFile + configFromFile = func(path util.AbsolutePath) (*config.Config, error) { + return nil, &types.AgentError{ + Message: "Unknown field present in configuration file", + Code: types.ErrorUnknownTOMLKey, + Err: errors.New("unknown field error"), + Data: types.ErrorData{ + "file": "/project-a/configuration-avcd.toml", + "line": 3, + "column": 1, + "key": "shortcut", + }, + } + } + + defer func() { + // Be sure to revert config from file implementation + configFromFile = config.FromFile + }() + + afs := afero.NewMemMapFs() + base, err := util.Getwd(afs) + s.NoError(err) + + cfg := config.New() + cfg.Type = config.ContentTypeHTML + err = cfg.WriteFile(config.GetConfigPath(base, "myConfig")) + s.NoError(err) + + filesService := new(MockFilesService) + + h := GetConfigFilesHandlerFunc(base, filesService, s.log) + + rec := httptest.NewRecorder() + + req, err := http.NewRequest("GET", "", nil) + s.NoError(err) + req = mux.SetURLVars(req, map[string]string{"name": "myConfig"}) + + h(rec, req) + + bodyRes := rec.Body.String() + s.NoError(err) + s.Equal(http.StatusBadRequest, rec.Result().StatusCode) + s.Contains(bodyRes, `{"code":"unknownTOMLKey","details":{"filename":"/project-a/configuration-avcd.toml","line":3,"column":1,"key":"shortcut"}}`) +} + +func (s *GetConfigFilesHandlerFuncSuite) TestHandlerFuncInvalidTOML() { + // Mocking implementation config.FromFile + configFromFile = func(path util.AbsolutePath) (*config.Config, error) { + return nil, &types.AgentError{ + Message: "Bad Syntax", + Code: types.ErrorInvalidTOML, + Err: errors.New("unknown field error"), + Data: types.ErrorData{ + "file": "/project-a/configuration-avcd.toml", + "line": 3, + "column": 1, + }, + } + } + + defer func() { + // Be sure to revert config from file implementation + configFromFile = config.FromFile + }() + + afs := afero.NewMemMapFs() + base, err := util.Getwd(afs) + s.NoError(err) + + cfg := config.New() + cfg.Type = config.ContentTypeHTML + err = cfg.WriteFile(config.GetConfigPath(base, "myConfig")) + s.NoError(err) + + filesService := new(MockFilesService) + + h := GetConfigFilesHandlerFunc(base, filesService, s.log) + + rec := httptest.NewRecorder() + + req, err := http.NewRequest("GET", "", nil) + s.NoError(err) + req = mux.SetURLVars(req, map[string]string{"name": "myConfig"}) + + h(rec, req) + + bodyRes := rec.Body.String() + s.NoError(err) + s.Equal(http.StatusBadRequest, rec.Result().StatusCode) + s.Contains(bodyRes, `{"code":"invalidTOML","details":{"filename":"/project-a/configuration-avcd.toml","line":3,"column":1}}`) +} + func (s *GetConfigFilesHandlerFuncSuite) TestHandlerFuncInvalidConfigFiles() { afs := afero.NewMemMapFs() base, err := util.Getwd(afs) diff --git a/internal/services/api/post_test_credentials_test.go b/internal/services/api/post_test_credentials_test.go index e85cbf855..6eee08636 100644 --- a/internal/services/api/post_test_credentials_test.go +++ b/internal/services/api/post_test_credentials_test.go @@ -124,5 +124,5 @@ func (s *PostTestCredentialsHandlerSuite) TestPostTestCredentialsHandlerFuncBadA s.NoError(err) s.Nil(response.User) s.NotNil(response.Error) - s.Equal(testError.Error(), response.Error.Message) + s.Equal("test error from TestAuthentication", response.Error.Message) } diff --git a/internal/types/error.go b/internal/types/error.go index 94e65e3a5..1e1e8ba17 100644 --- a/internal/types/error.go +++ b/internal/types/error.go @@ -10,6 +10,14 @@ type ErrorCode string type ErrorData map[string]any type Operation string +const ( + ErrorResourceNotFound ErrorCode = "resourceNotFound" + ErrorInvalidTOML ErrorCode = "invalidTOML" + ErrorUnknownTOMLKey ErrorCode = "unknownTOMLKey" + ErrorInvalidConfigFiles ErrorCode = "invalidConfigFiles" + ErrorUnknown ErrorCode = "unknown" +) + type EventableError interface { error SetOperation(op Operation) // Caller who receives an error calls SetOperation to attach context @@ -26,8 +34,6 @@ type AgentError struct { Data ErrorData `json:"data" toml:"data,omitempty"` } -const UnknownErrorCode ErrorCode = "unknown" - func AsAgentError(e error) *AgentError { if e == nil { return nil @@ -36,12 +42,13 @@ func AsAgentError(e error) *AgentError { if ok { return agentErr } - return NewAgentError(UnknownErrorCode, e, nil) + return NewAgentError(ErrorUnknown, e, nil) } func NewAgentError(code ErrorCode, err error, details any) *AgentError { data := make(ErrorData) msg := "" + if err != nil { msg = err.Error() } @@ -86,7 +93,7 @@ func (e *AgentError) Error() string { func OperationError(op Operation, err error) EventableError { e, ok := err.(EventableError) if !ok { - e = NewAgentError(UnknownErrorCode, err, nil) + e = NewAgentError(ErrorUnknown, err, nil) } e.SetOperation(op) return e diff --git a/internal/types/error_test.go b/internal/types/error_test.go index 269d73854..56c5ac0b5 100644 --- a/internal/types/error_test.go +++ b/internal/types/error_test.go @@ -56,3 +56,14 @@ func (s *ErrorSuite) TestAsAgentError() { agentErr := OperationError(Operation("testOp"), err) s.Equal(agentErr, AsAgentError(agentErr)) } + +func (s *ErrorSuite) TestNewAgentError() { + originalError := errors.New("shattered glass!") + aerr := NewAgentError(ErrorInvalidTOML, originalError, nil) + s.Equal(aerr, &AgentError{ + Message: "shattered glass!", + Code: ErrorInvalidTOML, + Err: originalError, + Data: make(ErrorData), + }) +} diff --git a/internal/util/toml.go b/internal/util/toml.go index 335a7f046..3fd6b74c8 100644 --- a/internal/util/toml.go +++ b/internal/util/toml.go @@ -56,9 +56,6 @@ func decodeErrFromTOMLErr(e *toml.DecodeError, path AbsolutePath) *DecodeError { } } -const invalidTOMLCode types.ErrorCode = "invalidTOML" -const unknownTOMLKeyCode types.ErrorCode = "unknownTOMLKey" - func ReadTOMLFile(path AbsolutePath, dest any) error { f, err := path.Open() if err != nil { @@ -70,13 +67,13 @@ func ReadTOMLFile(path AbsolutePath, dest any) error { decodeErr, ok := err.(*toml.DecodeError) if ok { e := decodeErrFromTOMLErr(decodeErr, path) - return types.NewAgentError(invalidTOMLCode, e, nil) + return types.NewAgentError(types.ErrorInvalidTOML, e, e) } strictErr, ok := err.(*toml.StrictMissingError) if ok { e := decodeErrFromTOMLErr(&strictErr.Errors[0], path) e.Problem = "unknown key" - return types.NewAgentError(unknownTOMLKeyCode, e, nil) + return types.NewAgentError(types.ErrorUnknownTOMLKey, e, e) } return err } diff --git a/internal/util/toml_test.go b/internal/util/toml_test.go index 6ed70d223..397bc8d68 100644 --- a/internal/util/toml_test.go +++ b/internal/util/toml_test.go @@ -40,7 +40,10 @@ func (s *TOMLSuite) TestReadTOMLFileBad() { err := ReadTOMLFile(path, &content) agentErr, ok := err.(*types.AgentError) s.True(ok) - s.Equal(invalidTOMLCode, agentErr.Code) + s.Equal(types.ErrorInvalidTOML, agentErr.Code) + s.Contains(agentErr.Data["file"], "bad.toml") + s.Equal(agentErr.Data["line"], 1) + s.Equal(agentErr.Data["column"], 5) } func (s *TOMLSuite) TestReadTOMLFileBadKey() { @@ -52,5 +55,10 @@ func (s *TOMLSuite) TestReadTOMLFileBadKey() { err := ReadTOMLFile(path, &content) agentErr, ok := err.(*types.AgentError) s.True(ok) - s.Equal(unknownTOMLKeyCode, agentErr.Code) + s.Equal(types.ErrorUnknownTOMLKey, agentErr.Code) + s.Contains(agentErr.Data["file"], "badkey.toml") + s.Equal(agentErr.Data["line"], 2) + s.Equal(agentErr.Data["column"], 1) + s.Equal(agentErr.Data["key"], "b") + s.Equal(agentErr.Data["problem"], "unknown key") }